2013-10-28

Google Maps API:gpxファイルを描画&編集する

同じドメイン(ここでは同じフォルダ)にあるgpxファイル(拡張子はxml)を読み込む。
Start地点、Goal地点近傍のトラックポイントを消去する。

変更履歴:
・経路(トラック)に沿って、消去するようにした(今までは、経路に関係なく、近ければ消去していた)。(2015-11-08)
・オブジェクト指向化した(コンストラクタを作成した)。(2015-11-08)
・メインのコードをhtmlファイルに記載するようにした。(2015-11-08)

応用例(GpxClipper:gpxファイルを加工するウェブアプリ)

◆html

70,71行目:ボタンのクリックで gpx_clip() を実行する。
19行目以降:xmlファイル(sample.xml)の読み込み。
29行目:コンストラクタGpxClipを用いてインスタンスgpxclipを作成する。
36行目:gpxclipを初期化する。
37行目:描画する。

xml ファイルの読み込みには、jQuery の $.ajax を使う。
29-37行目:ファイルを読み込んだ後に実行するスクリプト
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel=icon href=../_img/BlueSky_favicon.png sizes="16x16" type="image/png">
<title>gpxClipper</title>
<script src="http://maps.googleapis.com/maps/api/js?libraries=geometry&sensor=false"></script> 
<script src="jquery.js"></script>
<script src="gpxClipperTest.js"></script>
<script src="gmOverlayHtml.js"></script> 
<!--
http://www.atmarkit.co.jp/ait/articles/1112/16/news135.html

-->
<script>

$(function() {

 url_gpx = "sample.xml";
   // XMLファイル読み込み開始
   $.ajax({
     url: url_gpx,
     cache:false,
     dataType:"xml",
  error:  function(XMLHttpRequest, textStatus, errorThrown){
    alert('読み込みエラー!');
   },
     success:function(data){
   var gpxclip = new GpxClip({
    xml: data,
//    typeId: google.maps.MapTypeId.HYBRID,
//    pathInit: { weight: 1 },
    startButtonId: "start",
    goalButtonId: "goal"
   });
      gpxclip.init();
      gpxclip.draw();
     }
   });

 
});

</script></head>
<style type="text/css"> 
* {
 margin: 0;
 padding: 0;
}

html, body {
  height: 100%;
}

body {
 overflow: hidden;
 background-color: ivory; 
}

p {
 background-color: #99cc00
}

#map_canvas {
  height: 100%;
}
</style>

<body>
<input id="start" type="button" value="Startの周辺削除" onclick="gpx_clip(LatLngStart)" style="width:100px" >
<input id="goal" type="button" value="Goalの周辺削除" onclick="gpx_clip(LatLngGoal)" style="width:100px" >
<span>半径</span>
<input type="text" id="clip" value="100" size="3">
<span>(m) 以内の点を削除</span>
<div id="map_canvas"></div> 
</body>
</html>

◆コンストラクタ(GpxClip)

optionsとdefaultsにより、プロパティを設定する。
関数(メソッド)はprototypeで設定する。
var GpxClip = function(options) { // ver.2.1

 var defaults = {
  typeId: google.maps.MapTypeId.ROADMAP, // マップタイプ
             timeFontSize: "16px", // 時間表示のフォントサイズ
  period: 30, // 30分ごとに時間を表示
  pathInit: {    // 読み込んだxmlのトラックポイント
      color: "Blue",    // 線の色
      opacity: 0.5,    // 線の透過度
      weight: 5     // 線の幅
  },
   path: {     // クリップしたトラックポイント
   image: new google.maps.MarkerImage( // Markerのイメージ
    'images/point.png',    // marker image
         new google.maps.Size(16,16),  // marker size
         new google.maps.Point(0,0),  // marker origin
         new google.maps.Point(8,8)   // marker anchor
   )
  },
  start: {    // スタートポイント
   mark: {
    offsetX: -64,
    offsetY: -48,
    content: '<img src="images/start.png">'
   }
  },
  goal: {     // ゴールポイント
   mark: {
    offsetX: 0,
    offsetY: -48,
    content: '<img src="images/goal.png">'
   }
  }
 };
 $.extend(true, this, defaults, options); // デフォルトオプションと渡されたオプションのマージ

 this.period = this.period*60*1000; // 分→msに変換

};


初期処理(GpxClip.prototype.init)

5-11行:出力用のxmlを作成する。
13-50行:xmlファイルから緯度(Lat)、経度(Lng)、日時を取り込んで、初期のポイント(self.pathInit.point)、クリップ後のポイント(self.path.point)を初期化する。
28行:marker の一括消去用の配列を定義する。
30-35:Google Map を描画。
52-56行:Start地点、Goal地点を初期化する。
58-64行:トラックを描画。
66-73行:ボタンの設定。
GpxClip.prototype.init = function(){

 var self = this;

//  $(self.xml).find("name").each(function(){
//  $(this).text("gpxClipper");
//  });
//         <name>タグのテキストが日本語の場合エラーがでるので、すべてgpxClipperに変更するようにした。 2014-12-14
//         問題なくなっていたので、コメントアウトした 2016-05-13
//         日本語があるとUTF-8、ないとShift-JISで保存されるようである。
var serializer = new XMLSerializer();
 var xmlString = serializer.serializeToString(self.xml);
 setBlobUrl("download", xmlString);

  self.path.point = [];
 self.pathInit.point = [];

  $(self.xml).find("trkpt").each(function(i){
   var Lat = this.getAttribute("lat");
   var Lng = this.getAttribute("lon");
  var LatLng = new google.maps.LatLng(Lat,Lng);
  var date = new Date($(this).find("time").text());
  self.pathInit.point.push(LatLng);
  self.path.point[i]  = {
   latlng: LatLng,
   date: date,
  }
 });

    self.markerList = new google.maps.MVCArray();

 var layout = new GmLayout('map_canvas', self.path.point);
  self.map = new google.maps.Map(document.getElementById('map_canvas'),{
     zoom: layout.zoom,
     center: layout.center,
       mapTypeId: self.typeId
 });

  $(self.xml).find("trkpt").each(function(i){
  self.path.point[i].marker = new google.maps.Marker({
     position: self.path.point[i].latlng,
     map: self.map,
   icon: self.path.image
  });
  self.path.point[i].infobox = self.setInfobox (self.path.point[i]);
 });

 self.path.point[0].distance = 0;
 for ( i=1; i<self.path.point.length; i++) {
  self.path.point[i].distance = self.path.point[i-1].distance 
   + google.maps.geometry.spherical.computeDistanceBetween(self.path.point[i].latlng, self.path.point[i-1].latlng);
 }

 self.start.distance = self.path.point[0].distance;
 self.goal.distance = self.path.point[self.path.point.length-1].distance;

 self.start.index = 0;
 self.goal.index = self.path.point.length-1;

 var flightPath = new google.maps.Polyline({
       path: self.pathInit.point,
     strokeColor: self.pathInit.color,
     strokeOpacity: self.pathInit.opacity,
     strokeWeight: self.pathInit.weight
   });
  flightPath.setMap(self.map);

 $('#'+self.startButtonId).on( "click", function(){ 
  self.distanceClip = $("#clip").val();
  self.clipStart();
 });
 $('#'+self.goalButtonId).on( "click", function(){ 
  self.distanceClip = $("#clip").val();
  self.clipGoal();
 });

}


描画(GpxClip.prototype.draw)

gpxファイルをGoogle Map 上に描画する。
GpxClip.prototype.draw = function (){

 var self = this;

 // トラックポイントのマークを表示
 var trkptLength = $(self.xml).find("trkpt").length;
 for ( i=self.start.index; i<self.goal.index+1; i++) {
    self.path.point[i].marker.setMap(self.map);
  self.markerList.push(self.path.point[i].marker);
  self.setMarkerInfo(self.path.point[i].marker, self.path.point[i].infobox) ;
 }

 // Startのマークを表示
 var i = self.start.index;
 var infobox = self.setInfobox (self.path.point[i]);
 infobox.setMap(self.map);
 self.markerList.push(infobox);
    var mark = new GMOverlayHtml({
  map: self.map, 
  position: self.path.point[i].latlng,
  offsetX: self.start.mark.offsetX,
  offsetY: self.start.mark.offsetY,
  content: self.start.mark.content
 });
 mark.setMap(self.map);
 self.markerList.push(mark);

   var min = self.path.point[i].date.getTime() % self.period;
   var nextTime = (self.path.point[i].date.getTime() - min) + self.period;

 for ( i = 0; i < self.path.point.length; i++ ) {
    if(self.path.point[i].date.getTime() > nextTime){
   var infobox = self.setInfobox (self.path.point[i]);
   infobox.setMap(self.map);
     var min = self.path.point[i].date.getTime() % self.period;
     var nextTime = (self.path.point[i].date.getTime() - min) + self.period;
    }
 }

 // Goalのマークを表示
 var i = self.goal.index;
 var infobox = self.setInfobox (self.path.point[i]);
 infobox.setMap(self.map);
 self.markerList.push(infobox);
    var mark = new GMOverlayHtml({
  map: self.map, 
  position: self.path.point[i].latlng,
  offsetX: self.goal.mark.offsetX,
  offsetY: self.goal.mark.offsetY,
  content: self.goal.mark.content
 });
 mark.setMap(self.map);
 self.markerList.push(mark);

}


その他のプロトタイプ

GpxClip.prototype.setInfobox = function (point) {

 var self = this;

 var date = point.date;
 var $content = $('<p></p>').css({
   fontSize: self.timeFontSize
  }).text(
   date.getHours() + ':' + date.getMinutesString()
  );
 var infobox = new GMOverlayHtml({
  map: self.map, 
  position: point.latlng,
  offsetX: 0,
  offsetY: -16,
  content: $content[0].outerHTML
 });

 return infobox;

}


// gpxの各点に時刻を表示(mouseover時)
GpxClip.prototype.setMarkerInfo = function (marker, infobox) {
 var self = this;
   google.maps.event.addListener(marker, 'mouseover', function() {
  infobox.setMap(self.map);
  self.markerList.push(infobox);
   });
   google.maps.event.addListener(marker, 'mouseout', function() {
    infobox.setMap(null);
   });
}


GpxClip.prototype.clipStart = function(){

 var self = this;

 var cnt=0;
  $(self.xml).find("trkpt").each(function(i){
  var distance = self.path.point[i+self.start.index].distance-self.start.distance;
  if ( distance < self.distanceClip ) {
   $(this).remove();
   cnt++;
  } 
 });
 self.start.index = self.start.index + cnt;
 self.start.distance = self.path.point[self.start.index].distance;

 var serializer = new XMLSerializer();
 var xmlString = serializer.serializeToString(self.xml);
 setBlobUrl("download", xmlString);

 self.markerList.forEach(function(marker, idx) {
      marker.setMap(null);
    });
 self.draw();

}

GpxClip.prototype.clipGoal = function(){

 var self = this;

 var cnt=0;
  $(self.xml).find("trkpt").each(function(i){
  var distance = self.goal.distance-self.path.point[i+self.start.index].distance;
  if ( distance < self.distanceClip ) {
   $(this).remove();
   cnt++;
  }
 });
 self.goal.index = self.goal.index - cnt;
 self.goal.distance = self.path.point[self.goal.index].distance;
 var serializer = new XMLSerializer();
 var xmlString = serializer.serializeToString(self.xml);
 setBlobUrl("download", xmlString);

 self.markerList.forEach(function(marker, idx) {
      marker.setMap(null);
    });
 self.draw();

}


Google Maps API のzoom, center を適当な値に設定する関数〔リンク〕


10-13行目:Math.max.apply(null,配列), Math.min.apply(null,配列) は配列の中から最大値、最小値を求める関数。
20-21行目:360°が 256×2zoom に対応していることを利用している〔リンク〕
function gmLayout(arrLatLng){

 var arrLat = new Array() ;
 var arrLng = new Array() ;
 for ( i = 0; i < arrLatLng.length; i++ ) {
  arrLat[i] = arrLatLng[i].lat();
  arrLng[i] = arrLatLng[i].lng();
 }

 var maxLat = Math.max.apply(null,arrLat);
 var maxLng = Math.max.apply(null,arrLng);
 var minLat = Math.min.apply(null,arrLat);
 var minLng = Math.min.apply(null,arrLng);

 var ctrLat = (maxLat+minLat)/2;
 var ctrLng = (maxLng+minLng)/2;

 var rangeLat = maxLat-minLat;
 var rangeLng = maxLng-minLng;
 var zoomLat = Math.floor( Math.log(360/rangeLat*$(document).height()/256) / Math.log(2) );
 var zoomLng = Math.floor( Math.log(360/rangeLng*$(document).width()/256) / Math.log(2) );

 this.zoom = Math.min ( zoomLat, zoomLng );
 this.center = new google.maps.LatLng(ctrLat,ctrLng);

}


分を2桁の文字列に変換するプロトタイプ〔リンク〕

Date.prototype.getMinutesString = function(){
 var minutes = this.getMinutes();
 if ( minutes < 10 ) {
  minutes = "0" + minutes;
 }
 return minutes;
}





以上