2019-11-29 16:53:50 +01:00
|
|
|
/*
|
|
|
|
* μlogger
|
|
|
|
*
|
|
|
|
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
|
|
|
*
|
|
|
|
* This is free software; you can redistribute it and/or modify it under
|
|
|
|
* the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful, but
|
|
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2019-12-19 22:20:55 +01:00
|
|
|
import { lang as $, config } from '../initializer.js';
|
2019-11-29 16:53:50 +01:00
|
|
|
import MapViewModel from '../mapviewmodel.js';
|
2019-12-05 22:46:58 +01:00
|
|
|
import uTrack from '../track.js';
|
2019-11-29 16:53:50 +01:00
|
|
|
import uUtils from '../utils.js';
|
|
|
|
|
|
|
|
// google maps
|
|
|
|
/**
|
|
|
|
* Google Maps API
|
|
|
|
* @class GoogleMapsApi
|
|
|
|
* @implements {MapViewModel.api}
|
|
|
|
*/
|
|
|
|
export default class GoogleMapsApi {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {MapViewModel} vm
|
|
|
|
*/
|
|
|
|
constructor(vm) {
|
|
|
|
/** @type {google.maps.Map} */
|
|
|
|
this.map = null;
|
|
|
|
/** @type {MapViewModel} */
|
|
|
|
this.viewModel = vm;
|
|
|
|
/** @type {google.maps.Polyline[]} */
|
|
|
|
this.polies = [];
|
|
|
|
/** @type {google.maps.Marker[]} */
|
|
|
|
this.markers = [];
|
|
|
|
/** @type {google.maps.InfoWindow} */
|
|
|
|
this.popup = null;
|
|
|
|
/** @type {number} */
|
|
|
|
this.timeoutHandle = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load and initialize api scripts
|
|
|
|
* @return {Promise<void, Error>}
|
|
|
|
*/
|
|
|
|
init() {
|
2019-12-05 22:46:58 +01:00
|
|
|
const params = `?${(config.gkey) ? `key=${config.gkey}&` : ''}callback=gm_loaded`;
|
2019-11-29 16:53:50 +01:00
|
|
|
const gmReady = Promise.all([
|
|
|
|
GoogleMapsApi.onScriptLoaded(),
|
|
|
|
uUtils.loadScript(`https://maps.googleapis.com/maps/api/js${params}`, 'mapapi_gmaps', GoogleMapsApi.loadTimeoutMs)
|
|
|
|
]);
|
|
|
|
return gmReady.then(() => this.initMap());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Listen to Google Maps callbacks
|
|
|
|
* @return {Promise<void, Error>}
|
|
|
|
*/
|
|
|
|
static onScriptLoaded() {
|
|
|
|
const timeout = uUtils.timeoutPromise(GoogleMapsApi.loadTimeoutMs);
|
|
|
|
const gmInitialize = new Promise((resolve, reject) => {
|
|
|
|
window.gm_loaded = () => {
|
|
|
|
GoogleMapsApi.gmInitialized = true;
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
window.gm_authFailure = () => {
|
|
|
|
GoogleMapsApi.authError = true;
|
2019-12-24 14:50:25 +01:00
|
|
|
let message = $._('apifailure', 'Google Maps');
|
2019-12-19 22:20:55 +01:00
|
|
|
message += '<br><br>' + $._('gmauthfailure');
|
|
|
|
message += '<br><br>' + $._('gmapilink');
|
2019-11-29 16:53:50 +01:00
|
|
|
if (GoogleMapsApi.gmInitialized) {
|
|
|
|
alert(message);
|
|
|
|
}
|
|
|
|
reject(new Error(message));
|
|
|
|
};
|
|
|
|
if (GoogleMapsApi.authError) {
|
|
|
|
window.gm_authFailure();
|
|
|
|
}
|
|
|
|
if (GoogleMapsApi.gmInitialized) {
|
|
|
|
window.gm_loaded();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return Promise.race([ gmInitialize, timeout ]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start map engine when loaded
|
|
|
|
*/
|
|
|
|
initMap() {
|
|
|
|
const mapOptions = {
|
|
|
|
center: new google.maps.LatLng(config.initLatitude, config.initLongitude),
|
|
|
|
zoom: 8,
|
|
|
|
mapTypeId: google.maps.MapTypeId.TERRAIN,
|
|
|
|
scaleControl: true,
|
|
|
|
controlSize: 30
|
|
|
|
};
|
|
|
|
// noinspection JSCheckFunctionSignatures
|
|
|
|
this.map = new google.maps.Map(this.viewModel.mapElement, mapOptions);
|
|
|
|
this.popup = new google.maps.InfoWindow();
|
|
|
|
this.popup.addListener('closeclick', () => {
|
|
|
|
this.popupClose();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up API
|
|
|
|
*/
|
|
|
|
cleanup() {
|
|
|
|
this.polies.length = 0;
|
|
|
|
this.markers.length = 0;
|
|
|
|
this.popup = null;
|
|
|
|
if (this.map && this.map.getDiv()) {
|
|
|
|
this.map.getDiv().innerHTML = '';
|
|
|
|
}
|
|
|
|
this.map = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Display track
|
2019-12-05 22:46:58 +01:00
|
|
|
* @param {uPositionSet} track
|
2019-11-29 16:53:50 +01:00
|
|
|
* @param {boolean} update Should fit bounds if true
|
|
|
|
*/
|
|
|
|
displayTrack(track, update) {
|
2019-12-03 21:55:33 +01:00
|
|
|
if (!track || !track.hasPositions) {
|
2019-11-29 16:53:50 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
// init polyline
|
|
|
|
const polyOptions = {
|
|
|
|
strokeColor: config.strokeColor,
|
|
|
|
strokeOpacity: config.strokeOpacity,
|
|
|
|
strokeWeight: config.strokeWeight
|
|
|
|
};
|
|
|
|
// noinspection JSCheckFunctionSignatures
|
2019-12-03 21:55:33 +01:00
|
|
|
let poly;
|
2019-11-29 16:53:50 +01:00
|
|
|
const latlngbounds = new google.maps.LatLngBounds();
|
2019-12-03 21:55:33 +01:00
|
|
|
if (this.polies.length) {
|
|
|
|
poly = this.polies[0];
|
|
|
|
for (let i = 0; i < this.markers.length; i++) {
|
|
|
|
latlngbounds.extend(this.markers[i].getPosition());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
poly = new google.maps.Polyline(polyOptions);
|
|
|
|
poly.setMap(this.map);
|
|
|
|
this.polies.push(poly);
|
|
|
|
}
|
|
|
|
const path = poly.getPath();
|
|
|
|
const start = this.markers.length;
|
|
|
|
for (let i = start; i < track.length; i++) {
|
2019-11-29 16:53:50 +01:00
|
|
|
// set marker
|
2019-12-03 21:55:33 +01:00
|
|
|
this.setMarker(i, track);
|
2019-11-29 16:53:50 +01:00
|
|
|
// update polyline
|
2019-12-03 21:55:33 +01:00
|
|
|
const position = track.positions[i];
|
2019-11-29 16:53:50 +01:00
|
|
|
const coordinates = new google.maps.LatLng(position.latitude, position.longitude);
|
2019-12-05 22:46:58 +01:00
|
|
|
if (track instanceof uTrack) {
|
2019-11-29 16:53:50 +01:00
|
|
|
path.push(coordinates);
|
|
|
|
}
|
|
|
|
latlngbounds.extend(coordinates);
|
|
|
|
}
|
|
|
|
if (update) {
|
|
|
|
this.map.fitBounds(latlngbounds);
|
2019-12-03 21:55:33 +01:00
|
|
|
if (track.length === 1) {
|
2019-11-29 16:53:50 +01:00
|
|
|
// only one point, zoom out
|
|
|
|
const zListener =
|
|
|
|
google.maps.event.addListenerOnce(this.map, 'bounds_changed', function () {
|
|
|
|
if (this.getZoom()) {
|
|
|
|
this.setZoom(15);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
setTimeout(function () {
|
2019-12-05 22:46:58 +01:00
|
|
|
google.maps.event.removeListener(zListener);
|
2019-11-29 16:53:50 +01:00
|
|
|
}, 2000);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear map
|
|
|
|
*/
|
|
|
|
clearMap() {
|
|
|
|
if (this.polies) {
|
|
|
|
for (let i = 0; i < this.polies.length; i++) {
|
|
|
|
this.polies[i].setMap(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.markers) {
|
|
|
|
for (let i = 0; i < this.markers.length; i++) {
|
|
|
|
this.markers[i].setMap(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (this.popup.getMap()) {
|
|
|
|
this.popupClose();
|
|
|
|
}
|
|
|
|
this.popup.setContent('');
|
|
|
|
this.markers.length = 0;
|
|
|
|
this.polies.length = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} fill Fill color
|
|
|
|
* @param {boolean} isLarge Is large icon
|
|
|
|
* @param {boolean} isExtra Is styled with extra mark
|
|
|
|
* @return {google.maps.Icon}
|
|
|
|
*/
|
|
|
|
static getMarkerIcon(fill, isLarge, isExtra) {
|
|
|
|
// noinspection JSValidateTypes
|
|
|
|
return {
|
|
|
|
anchor: new google.maps.Point(15, 35),
|
|
|
|
url: MapViewModel.getSvgSrc(fill, isLarge, isExtra)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set marker
|
2019-12-05 22:46:58 +01:00
|
|
|
* @param {uPositionSet} track
|
2019-11-29 16:53:50 +01:00
|
|
|
* @param {number} id
|
|
|
|
*/
|
|
|
|
setMarker(id, track) {
|
|
|
|
// marker
|
|
|
|
const position = track.positions[id];
|
|
|
|
// noinspection JSCheckFunctionSignatures
|
|
|
|
const marker = new google.maps.Marker({
|
|
|
|
position: new google.maps.LatLng(position.latitude, position.longitude),
|
|
|
|
title: (new Date(position.timestamp * 1000)).toLocaleString(),
|
|
|
|
map: this.map
|
|
|
|
});
|
|
|
|
const isExtra = position.hasComment() || position.hasImage();
|
|
|
|
let icon;
|
2019-12-05 22:46:58 +01:00
|
|
|
if (track.isLastPosition(id)) {
|
2019-11-29 16:53:50 +01:00
|
|
|
icon = GoogleMapsApi.getMarkerIcon(config.colorStop, true, isExtra);
|
2019-12-05 22:46:58 +01:00
|
|
|
} else if (track.isFirstPosition(id)) {
|
2019-11-29 16:53:50 +01:00
|
|
|
icon = GoogleMapsApi.getMarkerIcon(config.colorStart, true, isExtra);
|
|
|
|
} else {
|
|
|
|
icon = GoogleMapsApi.getMarkerIcon(isExtra ? config.colorExtra : config.colorNormal, false, isExtra);
|
|
|
|
}
|
|
|
|
marker.setIcon(icon);
|
|
|
|
|
|
|
|
marker.addListener('click', () => {
|
|
|
|
this.popupOpen(id, marker);
|
|
|
|
});
|
|
|
|
marker.addListener('mouseover', () => {
|
|
|
|
this.viewModel.model.markerOver = id;
|
|
|
|
});
|
|
|
|
marker.addListener('mouseout', () => {
|
|
|
|
this.viewModel.model.markerOver = null;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.markers.push(marker);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open popup on marker with given id
|
|
|
|
* @param {number} id
|
|
|
|
* @param {google.maps.Marker} marker
|
|
|
|
*/
|
|
|
|
popupOpen(id, marker) {
|
|
|
|
this.popup.setContent(this.viewModel.getPopupHtml(id));
|
|
|
|
this.popup.open(this.map, marker);
|
|
|
|
this.viewModel.model.markerSelect = id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Close popup
|
|
|
|
*/
|
|
|
|
popupClose() {
|
|
|
|
this.viewModel.model.markerSelect = null;
|
|
|
|
this.popup.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Animate marker
|
|
|
|
* @param id Marker sequential id
|
|
|
|
*/
|
|
|
|
animateMarker(id) {
|
|
|
|
if (this.popup.getMap()) {
|
|
|
|
this.popupClose();
|
|
|
|
clearTimeout(this.timeoutHandle);
|
|
|
|
}
|
|
|
|
const icon = this.markers[id].getIcon();
|
|
|
|
this.markers[id].setIcon(GoogleMapsApi.getMarkerIcon(config.colorHilite, false, false));
|
|
|
|
this.markers[id].setAnimation(google.maps.Animation.BOUNCE);
|
|
|
|
this.timeoutHandle = setTimeout(() => {
|
|
|
|
this.markers[id].setIcon(icon);
|
|
|
|
this.markers[id].setAnimation(null);
|
|
|
|
}, 2000);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get map bounds
|
|
|
|
* @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
|
|
|
*/
|
|
|
|
getBounds() {
|
|
|
|
const bounds = this.map.getBounds();
|
|
|
|
const lat_sw = bounds.getSouthWest().lat();
|
|
|
|
const lon_sw = bounds.getSouthWest().lng();
|
|
|
|
const lat_ne = bounds.getNorthEast().lat();
|
|
|
|
const lon_ne = bounds.getNorthEast().lng();
|
|
|
|
return [ lon_sw, lat_sw, lon_ne, lat_ne ];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom to track extent
|
|
|
|
*/
|
|
|
|
zoomToExtent() {
|
|
|
|
const bounds = new google.maps.LatLngBounds();
|
|
|
|
for (let i = 0; i < this.markers.length; i++) {
|
|
|
|
bounds.extend(this.markers[i].getPosition());
|
|
|
|
}
|
|
|
|
this.map.fitBounds(bounds);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Zoom to bounds
|
|
|
|
* @param {number[]} bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
|
|
|
*/
|
|
|
|
zoomToBounds(bounds) {
|
|
|
|
const sw = new google.maps.LatLng(bounds[1], bounds[0]);
|
|
|
|
const ne = new google.maps.LatLng(bounds[3], bounds[2]);
|
|
|
|
const latLngBounds = new google.maps.LatLngBounds(sw, ne);
|
|
|
|
this.map.fitBounds(latLngBounds);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update size
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
|
|
updateSize() {
|
|
|
|
// ignore for google API
|
|
|
|
}
|
|
|
|
|
|
|
|
static get loadTimeoutMs() {
|
|
|
|
return 10000;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @type {boolean} */
|
|
|
|
GoogleMapsApi.authError = false;
|
|
|
|
/** @type {boolean} */
|
|
|
|
GoogleMapsApi.gmInitialized = false;
|