はじめに(経緯とか)
日本国内滞在中の外国人向けのボランティアサイトです。観光地の通訳ガイドやったりするのがメインなんですが、日本に留学して生きてる学生の語学学習支援とかもやってます。
数年前にリニューアルしました。サイト自体はWordPress入れて構築したんですが、アーキテクトっぽい立場で全体の構成とか考えたり、構築メンバの教育したり、必要なプログラミングしたりをやってました。
で、そのサイトにGoogle Maps Api使ってガイドコースを表示する機能つけたんですが、最近ちょっと修正する必要があって久しぶり見たら自分でやったことだいぶ忘れていたりして・・・
ということで、やったことを記録しておこうかと。せっかくなので、公開できる記事にしてみようと思った次第。文章があんまり得意じゃないし、今まで発信をしたことがあんまりないのでその練習を兼ねてます。また、Qiitaに書いておけば同じようなことしたい人(いるかどうかわかりませんが)がいたら何らかの助けになるんじゃないかと思って書いてみました。
参考サイト
- ボランティアのサイトです
- Guided Tourから各コース詳細に飛ぶとコースの説明の最後にコースマップが載ってます。
- 本家のGoogle Maps Javascript APIのページです。
- ソース類はこちらにおいてあります。
- WordPress側の話はこちら
- githubにあげる際にAPIキーをそのまま上げるの気持ち悪いので差し替えるやつもpythonで作りました
こんな感じのマップを作ります
- 実際はWordPressで構築したサイトに載せていて、PHPでルートの情報をDBから引っ張ってきてHTMLのFormに展開していますが、PHP部分の解説はしてないです。別途書くつもりにではいます。
Google Maps Apiの準備
- APIキーが必要なんですが、あちこち説明があると思うのでここでは割愛
HTML
Mapを表示する場所とAPI呼び出し
<div id="map"></div>
<script
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&language=en&callback=initMap&libraries=&v=weekly"
async>
</script>
- YOUR_API_KEYを取得したAPIキーに置換して使う
- initMap関数がコールバックとしてGoogle Mapsから呼ばれるので処理はこの中に実装する
JavaScriptとスタイルシート呼び出すところ
<head>
<link rel="stylesheet" type="text/css" href="css/style.css" />
<script src="js/maps.js"></script>
</head>
StyleSheet(CSS)
#map {
height: 450px;
}
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
- #mapのheightは指定しないと表示されないので注意
ルートの指定はformタグに埋め込みます
<form id="">
<input type="hidden" id="count" value="4" />
<input type="hidden" id="p0-label" value="Start" />
<input type="hidden" id="p0-title" value="Kamakura Station" />
<input type="hidden" id="p0-latlng" value="35.319264, 139.550004" />
<input type="hidden" id="p0-description" value="Tour starts from here" />
<input type="hidden" id="p1-label" value="1" />
<input type="hidden" id="p1-title" value="Tsurugaoka Hachimangu Shrine" />
<input type="hidden" id="p1-latlng" value="35.326074, 139.556485" />
<input type="hidden" id="p1-description"
value="Built as a tutelary shrine for the Genji family in 1191, the most gorgeous shrine in Kamakura attracts millions of visitors for its history, culture, architecture and various traditional events." />
<input type="hidden" id="p2-label" value="2" />
<input type="hidden" id="p2-title" value="Hase-dera Temple" />
<input type="hidden" id="p2-latlng" value="35.312416, 139.533042" />
<input type="hidden" id="p2-description"
value="This legendary old temple boasts of its large wooden gilded statue of Eleven-faced Goddess of Mercy, a beautiful Japanese-style garden and the nice view of the ocean from a terrace." />
<input type="hidden" id="p3-label" value="3" />
<input type="hidden" id="p3-title" value="the Great Buddha" />
<input type="hidden" id="p3-latlng" value="35.315858, 139.535371" />
<input type="hidden" id="p3-description"
value="The Great Buddha, a National Treasure and a Symbol of Kamakura, was cast in bronze in the middle of the 13th C. The hollow interior of the statue is open to the public." />
</form>
構造
- inputタグをtype="hidden"で表示されないようにして、idとvalueを使ってキーバリュー型っぽく使います。
- count以外のidはpn-xxxxxの形をしています。nには0~count-1の数値が入ります。xxxxx部分にはlabel,titleといった種別が設定されます。
- 以下はid値でその意味を解説
count
- 地点数を設定します。
pn-label(nは0~count-1の整数)
- マップのマーカーに”Start"とか"1"とか文字がついていますが、その文字を設定します。
- 後で解説しますが、value="-b-"とすると経路分割にします。
pn-latlng
- 経度、緯度の情報を実数、カンマ区切りで設定します。
pn-title
- マーカをクリックするとポップアップで地点の名称と説明が表示されるんですが、その名称を設定します。
pn-description
- ポップアップ時の説明文を設定します。
オプションで設定できる項目
pn-marker
- value="0"とすると、マーカを生成しません。
- ウェイポイントというか、マーカはいらないんだけど、こっち通ってほしいときに使います。
pn-skip
- value="1"とすると、経路から除外されます。
- マーカはほしいんだけど、経路と指定は外してほしいとき用です。
travelmode
- ルート案内のモード設定
値 | モード |
---|---|
WALKING | 徒歩(既定値) |
DRIVING | 車 |
BICYCLING | 自転車 |
TRANSIT | 交通機関 |
mapTypeId
- 地図の表示形式を設定
値 | モード |
---|---|
ROADMAP | 道路や建物などが表示される地図です(既定値) |
SATELLITE | 衛星写真を使った地図です |
HYBRID | ROADMAPとSATELLITEの複合した地図です |
TERRAIN | 地形情報を使った地図です |
JavaScript
本題です。
クラス定義
// 場所クラス定義
var Place = function (location, title, description, label) {
// 位置情報
this.location = location;
// タイトル
this.title = title;
// 説明
this.description = description;
// マーカのラベル
this.label = label;
}
- HTMLのformで定義されている地点の情報を格納します。
- マーカにしたり、ウェイポイントにしたり、ポップアップに表示する情報をもたせています。
//コースクラス
var Course = function (places, routes, courseOption) {
// マーカを配置する場所
this.places = places;
// ルート表示に使用する地点
this.routes = routes;
// ズームサイズ(既定:15)
this.zoom = (courseOption === undefined || courseOption.zoom === undefined) ? 15 : courseOption.zoom;
// ルート検索モード(既定:徒歩)
this.travelmode = (courseOption === undefined || courseOption.travelmode === undefined) ? 'WALKING' : courseOption.travelmode;
// 地図タイプ(既定:ロードマップ)
this.mapTypeId = (courseOption === undefined || courseOption.mapTypeId === undefined) ? undefined : courseOption.mapTypeId;
};
- こちらはマップの設定や経路、地点の情報をもたせています。
サブ関数群
マーカ作成処理
// マーカー作成処理
function setMarker(map, place, infowindow) {
// マーカー
var marker = new google.maps.Marker({
position: place.location,
map: map,
label: place.label,
title: place.title
});
// イベントで説明ウィンドウ表示
google.maps.event.addListener(marker, 'click', function () {
infowindow.setContent('<div><strong>' + place.title + '</strong><br />' + place.description + '</div>');
infowindow.open(map, this);
});
marker.setMap(map);
}
- 引数のmap,infowindowは後述のinitMapの中で生成しています。
- 引数のplaceは上のplaceクラスのオブジェクトです。
- google maps apiでMakerを生成、イベントリスナに説明ウィンドウ=infowindowを設定してmapに設定しています。
ルート作成処理
// ルート作成処理
function setDirection(map, course, index, bounds) {
places = course.routes[index];
//ルート用のレイヤ
var directionsDisplay = new google.maps.DirectionsRenderer({
map: map,
suppressMarkers: true,
preserveViewport: true
});
//経由ポイント生成
var waypoints = [];
for (i = 1; i < places.length - 1; i++) {
waypoints.push({
location: places[i].location,
stopover: true
});
}
//ルート用のリクエストオブジェクト
var request = {
destination: places[places.length - 1].location,
origin: places[0].location,
waypoints: waypoints,
travelMode: course.travelmode
};
// Pass the directions request to the directions service.
var directionsService = new google.maps.DirectionsService();
directionsService.route(request, function (response, status) {
if (status == 'OK') {
// Display the route on the map.
directionsDisplay.setDirections(response);
bounds.union(response.routes[0].bounds);
map.fitBounds(bounds);
}
});
}
- Directions Apiを使ってルートを作成する処理です。
- courseは上のコース・クラスのオブジェクト、map,boundsはinitMapで生成して渡されてきます。indexはcourse.routesが配列になっているのでそのインデックスを指定します。通常は0です。
- routesには通る地点がリストで格納されているので、リスト先頭が出発点(origin)、リストの最後が目的地(distination)、その他は経由地(waypoint)として扱います。
- bounds.unionは最後にマップのズーム値をfitBoundsで自動設定するのに使っています。
HTMLのFormから経路情報を取得してCourseオブジェクト生成する
// formの情報から地図作成に使用する情報を取得
function getCourseFromPage() {
// 登録地点数を取得
var count = getIntVal('count');
var places = new Array();
var route = new Array();
var routes = new Array();
for (var i = 0; i < count; i++) {
var pri = 'p' + i + '-';
var latlngtext = getVal(pri + 'latlng');
var title = getVal(pri + 'title');
var desc = getVal(pri + 'description');
var label = getVal(pri + 'label');
var skip = getVal(pri + 'skip');
var marker = getVal(pri + 'marker');
var p = new Place(getLatLngFromString(latlngtext), title, desc, label);
if (marker == undefined) {
places.push(p);
}
if (skip == undefined) {
route.push(p);
}
// 2021/03/21 ルートのラベルに'-b-'を含む場合、ルートを分割するようにしてみた
// Routeテーブル上はmarker=0,skip=1としておけば無視される地点になる
if(label.includes("-b-")){
routes.push(route);
route = new Array();
}
}
routes.push(route);
var opt = {};
opt.travelmode = getVal('travelmode');
opt.mapTypeId = getVal('mapTypeId');
opt.zoom = getIntVal('zoom');
return new Course(places, routes, opt);
}
- countで地点数を取得し、ループ回して
- inputタグから情報とって
- Placeオブジェクト生成、Markerにする場合(makerが定義されない)はplacesリストに、経路案内対象(skipが定義されてない) 場合はrouteリストに追加していきます
- labelに"-b-"があったらrouteをroutesリストに追加して新たなrouteを生成します。ルート分割は後で解説予定
inputタグから値取得する関数
// 文字列から経緯度オブジェクト生成
function getLatLngFromString(ll) {
var latlng = ll.split(/, ?/)
return new google.maps.LatLng(parseFloat(latlng[0]), parseFloat(latlng[1]));
}
// inputタグのvalue属性を取得
function getVal(id) {
var element = document.getElementById(id);
if (element == null) return undefined;
var value = element.getAttribute('value');
return value;
}
// inputタグのValue属性を数値変換
function getIntVal(id) {
var text = getVal(id);
if (text == undefined) return undefined;
return Number(text);
}
- HTMLのinputタグには文字列、数値、経度緯度が格納されているので、種類ごとに取得関数を作ってあります。
Google Maps ApiからコールバックされるinitMap処理
var MAP_ELEMENT = "map";
// google map javascript apiから呼ばれるコールバック関数
function initMap() {
//Mapを埋め込むエレメントを検索
element = document.getElementById(MAP_ELEMENT);
if (element == null) return;
//エレメントのタイトル属性からコースを特定
var course = getCourseFromPage();
if (course == null) return;
//縮尺、中央を設定、マウスホイールによる拡大縮小を抑止
var mapOptions = {
zoom: course.zoom,
scrollwheel: false,
center: course.places[0].location,
mapTypeId: course.mapTypeId,
styles: [{
elementType: 'geometry',
stylers: [{
color: '#ebe3cd'
}]
}, {
elementType: 'labels.text.fill',
stylers: [{
color: '#523735'
}]
}, {
elementType: 'labels.text.stroke',
stylers: [{
color: '#f5f1e6'
}]
}, {
featureType: 'administrative',
elementType: 'geometry.stroke',
stylers: [{
color: '#c9b2a6'
}]
}, {
featureType: 'administrative.land_parcel',
elementType: 'geometry.stroke',
stylers: [{
color: '#dcd2be'
}]
}, {
featureType: 'administrative.land_parcel',
elementType: 'labels.text.fill',
stylers: [{
color: '#ae9e90'
}]
}, {
featureType: 'landscape.natural',
elementType: 'geometry',
stylers: [{
color: '#dfd2ae'
}]
}, {
featureType: 'poi',
elementType: 'geometry',
stylers: [{
color: '#dfd2ae'
}]
}, {
featureType: 'poi',
elementType: 'labels.text.fill',
stylers: [{
color: '#93817c'
}]
}, {
featureType: 'poi.park',
elementType: 'geometry.fill',
stylers: [{
color: '#a5b076'
}]
}, {
featureType: 'poi.park',
elementType: 'labels.text.fill',
stylers: [{
color: '#447530'
}]
}, {
featureType: 'road',
elementType: 'geometry',
stylers: [{
color: '#f5f1e6'
}]
}, {
featureType: 'road.arterial',
elementType: 'geometry',
stylers: [{
color: '#fdfcf8'
}]
}, {
featureType: 'road.highway',
elementType: 'geometry',
stylers: [{
color: '#f8c967'
}]
}, {
featureType: 'road.highway',
elementType: 'geometry.stroke',
stylers: [{
color: '#e9bc62'
}]
}, {
featureType: 'road.highway.controlled_access',
elementType: 'geometry',
stylers: [{
color: '#e98d58'
}]
}, {
featureType: 'road.highway.controlled_access',
elementType: 'geometry.stroke',
stylers: [{
color: '#db8555'
}]
}, {
featureType: 'road.local',
elementType: 'labels.text.fill',
stylers: [{
color: '#806b63'
}]
}, {
featureType: 'transit.line',
elementType: 'geometry',
stylers: [{
color: '#dfd2ae'
}]
}, {
featureType: 'transit.line',
elementType: 'labels.text.fill',
stylers: [{
color: '#8f7d77'
}]
}, {
featureType: 'transit.line',
elementType: 'labels.text.stroke',
stylers: [{
color: '#ebe3cd'
}]
}, {
featureType: 'transit.station',
elementType: 'geometry',
stylers: [{
color: '#dfd2ae'
}]
}, {
featureType: 'water',
elementType: 'geometry.fill',
stylers: [{
color: '#b9d3c2'
}]
}, {
featureType: 'water',
elementType: 'labels.text.fill',
stylers: [{
color: '#92998d'
}]
}]
};
//マップオブジェクト生成
var map = new google.maps.Map(element, mapOptions);
var bounds = new google.maps.LatLngBounds();
//情報ウィンドウ
var infowindow = new google.maps.InfoWindow();
infowindow.setOptions({
maxWidth: 200
})
//マーカーを生成
for (var i = 0; i < course.places.length; i++) {
setMarker(map, course.places[i], infowindow);
}
//ルートの作成
if (course.routes.length > 0) {
for(var i = 0; i< course.routes.length; i++){
setDirection(map, course, i, bounds);
}
}
}
- elementにマップを埋め込む
<div>
タグを取得 - コース情報取得
- mapOptionsは色味を設定するために使っています。こいつはなくても処理にはあんまり関係ないです。
- map:地図オブジェクト
- bounds:後で表示領域を調整するためのオブジェクト
- infoWindow:Markerクリック時にポップアップする情報ウィンドウ
- courseオブジェクトのplacesからMarker生成
- ルートの生成。routesでfor文回してますが、ルート分割されている場合があるのでループにしています。
ルート分割について
今回の修正の発端ですが、横須賀のコースを新設することになってルートマップを追加したんですね。
そしたら、かなりイケてない感じの地図が出来上がりました。
ちょっとわかりにくいんですが、3から4に行くところで敷地内を突っ切って行けるはずなのですが、なぜかGoogleさんがそのルートを示してくれず、一度Start地点付近まで戻って遠回りするルートが出てしまいます。拡大するとこんな感じ
Googleマップで検索してみても同じように変な感じになります。
どうも、このYOKOSUKA軍港めぐりって施設が海底に沈んでいるとでも認識しているかのようです。
途中にWaipoint入れてもどうにもならないので、一本のルートで示すのを諦め、分割して2本以上のルートを一つの地図に表示できるように改造してみようと思いました。
別途構築しているWordPressのPHPとかDB構造とか変更したくないので、JavaScriptの世界だけで対応したかったため、
- Courseクラスのroutesをリスト化
- pn-labelに"-b-"って入ってたらそこでルート区切って新しいルートにする
- pn-marker、pn-skipを同時設定することでMarkerも生成されないし、ルートにも影響できないinputタグが作れる
なんて感じにしました。結果的にちょっとマシな感じに