/*
* μ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(MapViewModel)} init
* @property {function} cleanup
* @property {function(uTrack, boolean)} displayTrack
* @property {function} clearMap
* @property {function(number)} animateMarker
* @property {function} getBounds
* @property {function} zoomToExtent
* @property {function} zoomToBounds
* @property {function} updateSize
* @property {function} updateState
*/
/**
* @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
});
this.model.onMenuToggle = () => this.onMapResize();
this.state = state;
/** @type HTMLElement */
this.mapElement = document.querySelector('#map-canvas');
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.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.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;
}
}
});
}
/**
* @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.api.displayTrack(track, update)
.finally(() => this.state.jobStop());
}
/**
* 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(`