/*
* μ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 .
*/
import { lang as $, auth, config } from './initializer.js';
import GoogleMapsApi from './mapapi/api_gmaps.js';
import OpenLayersApi from './mapapi/api_openlayers.js';
import PositionDialogModel from './positiondialogmodel.js';
import ViewModel from './viewmodel.js';
import uAlert from './alert.js';
import uDialog from './dialog.js';
import uObserve from './observe.js';
import uUtils from './utils.js';
/**
* @typedef {Object} MapViewModel.api
* @interface
* @memberOf MapViewModel
* @type {Object}
* @property {function(number)} animateMarker
* @property {function(uTrack, boolean)} displayTrack
* @property {function} cleanup
* @property {function} clearMap
* @property {function} getBounds
* @property {function(MapViewModel)} init
* @property {function} setTrackDefaultStyle
* @property {function(uTrack, string, Object)} setTrackGradientStyle
* @property {function} updateSize
* @property {function} updateState
* @property {function} zoomToBounds
* @property {function} zoomToExtent
*/
/**
* @typedef {Object} MapParams
* @property {number[]} center
* @property {number} zoom
* @property {number} rotation
*/
/**
* @class MapViewModel
*/
export default class MapViewModel extends ViewModel {
/**
* @param {uState} state
*/
constructor(state) {
super({
/** @type {?number} */
markerOver: null,
/** @type {?number} */
markerSelect: null,
// click handler
onMenuToggle: null,
speedVisible: false,
altitudeVisible: false
});
this.state = state;
/** @type HTMLElement */
this.mapElement = document.querySelector('#map-canvas');
/** @type HTMLInputElement */
this.speedEl = this.getBoundElement('speedVisible');
/** @type HTMLInputElement */
this.altitudeEl = this.getBoundElement('altitudeVisible');
/** @type HTMLElement */
this.styleEl = this.getBoundElement('trackColor');
this.savedBounds = null;
this.api = null;
}
/**
* @return {MapViewModel}
*/
init() {
this.bindAll();
this.setObservers();
return this;
}
/**
* Dynamic change of map api
* @param {string} apiName API name
*/
loadMapAPI(apiName) {
let mapApi = this.api;
this.api = null;
if (mapApi) {
try {
this.savedBounds = mapApi.getBounds();
} catch (e) {
this.savedBounds = null;
}
mapApi.cleanup();
}
mapApi = this.getApi(apiName);
mapApi.init()
.then(() => {
this.api = mapApi;
this.onReady();
})
.catch((e) => {
let txt = $._('apifailure', apiName);
if (e && e.message) {
txt += ` (${e.message})`;
}
uAlert.error(txt, e);
config.mapApi = (apiName === 'gmaps') ? 'openlayers' : 'gmaps';
});
}
/**
* @param {string} apiName
* @return {OpenLayersApi|GoogleMapsApi}
*/
getApi(apiName) {
return apiName === 'gmaps' ? new GoogleMapsApi(this) : new OpenLayersApi(this);
}
onReady() {
if (this.state.currentTrack) {
let update = true;
if (this.savedBounds) {
this.api.zoomToBounds(this.savedBounds);
update = false;
}
this.displayTrack(this.state.currentTrack, update);
}
}
setObservers() {
config.onChanged('mapApi', (mapApi) => {
this.loadMapAPI(mapApi);
this.toggleStyleOptions();
this.toggleStyleMenu();
});
this.state.onChanged('currentTrack', (track) => {
if (!this.api) {
return;
}
this.api.clearMap();
if (track) {
uObserve.observe(track, 'positions', () => {
this.displayTrack(track, false);
this.api.zoomToExtent();
});
this.displayTrack(track, true);
}
this.toggleStyleOptions();
});
this.state.onChanged('history', () => {
const history = this.state.history;
if (this.api && history && !history.trackId) {
if (history.mapApi) {
config.mapApi = history.mapApi;
} else {
if (history.mapParams) {
this.api.updateState(history.mapParams);
} else {
this.api.zoomToExtent();
}
this.state.history = null;
}
}
});
this.model.onMenuToggle = () => this.onMapResize();
this.onChanged('speedVisible', (visible) => {
if (visible) {
this.model.altitudeVisible = false;
}
this.setTrackStyle();
});
this.onChanged('altitudeVisible', (visible) => {
if (visible) {
this.model.speedVisible = false;
}
this.setTrackStyle();
});
}
/**
* @param {uTrack} track Track to display
* @param {boolean} update Should update map view
*/
displayTrack(track, update) {
this.state.jobStart();
if (update && this.state.history && this.state.history.mapParams) {
this.api.updateState(this.state.history.mapParams);
update = false;
}
this.state.history = null;
this.setTrackStyle();
this.api.displayTrack(track, update)
.finally(() => this.state.jobStop());
}
onMapResize() {
if (this.api) {
this.api.updateSize();
}
}
toggleStyleOptions() {
const track = this.state.currentTrack;
this.speedEl.disabled = !track || !track.hasSpeeds || track.length <= 1;
this.altitudeEl.disabled = !track || !track.hasAltitudes || track.length <= 1;
}
toggleStyleMenu() {
if (config.mapApi === 'openlayers') {
this.styleEl.style.display = 'block';
} else {
this.styleEl.style.display = 'none';
}
}
setTrackStyle() {
const track = this.state.currentTrack;
if (!this.api || !track) {
return;
}
if (this.model.speedVisible && track.hasSpeeds) {
this.setSpeedStyle();
} else if (this.model.altitudeVisible && track.hasAltitudes) {
this.setAltitudeStyle();
} else {
this.api.setTrackDefaultStyle();
}
}
setSpeedStyle() {
const track = this.state.currentTrack;
const scale = {
minValue: 0,
maxValue: track.maxSpeed,
minColor: [ 0, 255, 0 ],
maxColor: [ 255, 0, 0 ]
};
this.api.setTrackGradientStyle(track, 'speed', scale);
}
setAltitudeStyle() {
const track = this.state.currentTrack;
const scale = {
minValue: track.minAltitude,
maxValue: track.maxAltitude,
minColor: [ 0, 255, 0 ],
maxColor: [ 255, 0, 0 ]
};
this.api.setTrackGradientStyle(track, 'altitude', scale);
}
/**
* Get popup html
* @param {number} id Position index
* @returns {HTMLDivElement}
*/
getPopupElement(id) {
const pos = this.state.currentTrack.positions[id];
const count = this.state.currentTrack.length;
const user = this.state.currentTrack.user;
const isEditable = auth.user && (auth.isAdmin || auth.user.id === user.id);
let date = '–––';
let time = '–––';
if (pos.timestamp > 0) {
const dateTime = uUtils.getTimeString(new Date(pos.timestamp * 1000));
date = dateTime.date;
time = `${dateTime.time}${dateTime.zone}`;
}
let provider = '';
if (pos.provider === 'gps') {
provider = `
`;
} else if (pos.provider === 'network') {
provider = `
`;
}
let editLink = '';
if (isEditable) {
editLink = ``;
}
let stats = '';
if (!this.state.showLatest) {
stats =
`

${$.getLocaleDuration(pos.totalSeconds)}

${$.getLocaleSpeed(pos.totalSpeed, true)}

${$.getLocaleDistanceMajor(pos.totalMeters, true)}
`;
}
const html =
`
${(pos.hasComment()) ? `` : ''}
${(pos.hasImage()) ? `
` : ''}

${date}

${time}
${(pos.speed !== null) ? `

${$.getLocaleSpeed(pos.speed, true)}
` : ''}
${(pos.altitude !== null) ? `

${$.getLocaleAltitude(pos.altitude, true)}
` : ''}
${(pos.accuracy !== null) ? `

${$.getLocaleAccuracy(pos.accuracy, true)}${provider}
` : ''}
${(pos.bearing !== null) ? `

${pos.bearing}°
` : ''}

${$.getLocaleCoordinates(pos)}
${stats}
`;
const node = document.createElement('div');
node.setAttribute('id', 'popup');
node.innerHTML = html;
if (pos.hasImage()) {
const image = node.querySelector('#pimage img');
image.onclick = () => {
const modal = new uDialog(`
`);
const closeEl = modal.element.querySelector('#modal-close');
closeEl.onclick = () => modal.destroy();
modal.element.classList.add('image');
modal.show();
}
}
if (isEditable) {
const edit = node.querySelector('#editposition');
edit.onclick = () => {
const vm = new PositionDialogModel(this.state, id);
vm.init();
}
}
return node;
}
/**
* Get SVG marker path
* @param {boolean} isLarge Large marker with hole if true
* @return {string}
*/
static getMarkerPath(isLarge) {
const markerHole = 'M15,34.911c0,0,0.359-3.922,1.807-8.588c0.414-1.337,1.011-2.587,2.495-4.159' +
'c1.152-1.223,3.073-2.393,3.909-4.447c1.681-6.306-3.676-9.258-8.211-9.258c-4.536,0-9.893,2.952-8.211,9.258' +
'c0.836,2.055,2.756,3.225,3.91,4.447c1.484,1.572,2.08,2.822,2.495,4.159C14.64,30.989,15,34.911,15,34.911z M18,15.922' +
'c0,1.705-1.342,3.087-2.999,3.087c-1.657,0-3-1.382-3-3.087c0-1.704,1.343-3.086,3-3.086C16.658,12.836,18,14.218,18,15.922z';
const marker = 'M14.999,34.911c0,0,0.232-1.275,1.162-4.848c0.268-1.023,0.652-1.98,1.605-3.184' +
'c0.742-0.937,1.975-1.832,2.514-3.404c1.082-4.828-2.363-7.088-5.281-7.088c-2.915,0-6.361,2.26-5.278,7.088' +
'c0.538,1.572,1.771,2.468,2.514,3.404c0.953,1.203,1.337,2.16,1.604,3.184C14.77,33.635,14.999,34.911,14.999,34.911z';
return isLarge ? markerHole : marker;
}
/**
* Get marker extra mark
* @param {boolean} isLarge
* @return {string}
*/
static getMarkerExtra(isLarge) {
const offset1 = isLarge ? 'M26.074,13.517' : 'M23.328,20.715';
const offset2 = isLarge ? 'M28.232,10.942' : 'M25.486,18.141';
return `
`;
}
/**
* Get inline SVG source
* @param {string} fill
* @param {boolean=} isLarge
* @param {boolean=} isExtra
* @return {string}
*/
static getSvgSrc(fill, isLarge, isExtra) {
const svg = ``;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
}