Add map api handlers
This commit is contained in:
parent
eda84b964c
commit
2086d20192
@ -1,333 +0,0 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import { config, lang } from '../constants.js';
|
||||
import uEvent from '../event.js';
|
||||
import uUI from '../ui.js';
|
||||
import uUtils from '../utils.js';
|
||||
|
||||
// google maps
|
||||
/**
|
||||
* Google Maps API module
|
||||
* @module gmApi
|
||||
* @implements {uMap.api}
|
||||
*/
|
||||
|
||||
/** @type {google.maps.Map} */
|
||||
let map = null;
|
||||
/** @type {uBinder} */
|
||||
let binder = null;
|
||||
/** @type {google.maps.Polyline[]} */
|
||||
const polies = [];
|
||||
/** @type {google.maps.Marker[]} */
|
||||
const markers = [];
|
||||
/** @type {google.maps.InfoWindow[]} */
|
||||
const popups = [];
|
||||
/** @type {google.maps.InfoWindow} */
|
||||
let openPopup = null;
|
||||
/** @type {google.maps.PolylineOptions} */
|
||||
let polyOptions = null;
|
||||
/** @type {google.maps.MapOptions} */
|
||||
let mapOptions = null;
|
||||
/** @type {number} */
|
||||
let timeoutHandle = 0;
|
||||
const name = 'gmaps';
|
||||
let isLoaded = false;
|
||||
let authError = false;
|
||||
|
||||
/**
|
||||
* Initialize map
|
||||
* @param {uBinder} b
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function init(b, el) {
|
||||
|
||||
binder = b;
|
||||
|
||||
const url = '//maps.googleapis.com/maps/api/js?' + ((config.gkey != null) ? ('key=' + config.gkey + '&') : '') + 'callback=gm_loaded';
|
||||
uUtils.addScript(url, 'mapapi_gmaps');
|
||||
if (!isLoaded) {
|
||||
throw new Error('Google Maps API not ready');
|
||||
}
|
||||
start(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start map engine when loaded
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function start(el) {
|
||||
if (authError) {
|
||||
window.gm_authFailure();
|
||||
return;
|
||||
}
|
||||
google.maps.visualRefresh = true;
|
||||
// noinspection JSValidateTypes
|
||||
polyOptions = {
|
||||
strokeColor: config.strokeColor,
|
||||
strokeOpacity: config.strokeOpacity,
|
||||
strokeWeight: config.strokeWeight
|
||||
};
|
||||
// noinspection JSValidateTypes
|
||||
mapOptions = {
|
||||
center: new google.maps.LatLng(config.init_latitude, config.init_longitude),
|
||||
zoom: 8,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
||||
scaleControl: true
|
||||
};
|
||||
map = new google.maps.Map(el, mapOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up API
|
||||
*/
|
||||
function cleanup() {
|
||||
polies.length = 0;
|
||||
markers.length = 0;
|
||||
popups.length = 0;
|
||||
polyOptions = null;
|
||||
mapOptions = null;
|
||||
openPopup = null;
|
||||
if (map && map.getDiv()) {
|
||||
map.getDiv().innerHTML = '';
|
||||
}
|
||||
map = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track
|
||||
* @param {uTrack} track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
*/
|
||||
function displayTrack(track, update) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
// init polyline
|
||||
const poly = new google.maps.Polyline(polyOptions);
|
||||
poly.setMap(map);
|
||||
const path = poly.getPath();
|
||||
const latlngbounds = new google.maps.LatLngBounds();
|
||||
let i = 0;
|
||||
for (const position of track.positions) {
|
||||
// set marker
|
||||
setMarker(i++, track);
|
||||
// update polyline
|
||||
const coordinates = new google.maps.LatLng(position.latitude, position.longitude);
|
||||
if (track.continuous) {
|
||||
path.push(coordinates);
|
||||
}
|
||||
latlngbounds.extend(coordinates);
|
||||
}
|
||||
if (update) {
|
||||
map.fitBounds(latlngbounds);
|
||||
if (i === 1) {
|
||||
// only one point, zoom out
|
||||
const zListener =
|
||||
google.maps.event.addListenerOnce(map, 'bounds_changed', function () {
|
||||
if (this.getZoom()) {
|
||||
this.setZoom(15);
|
||||
}
|
||||
});
|
||||
setTimeout(function () { google.maps.event.removeListener(zListener) }, 2000);
|
||||
}
|
||||
}
|
||||
polies.push(poly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear map
|
||||
*/
|
||||
function clearMap() {
|
||||
if (polies) {
|
||||
for (let i = 0; i < polies.length; i++) {
|
||||
polies[i].setMap(null);
|
||||
}
|
||||
}
|
||||
if (markers) {
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
// google.maps.event.removeListener(popups[i].listener);
|
||||
google.maps.event.clearInstanceListeners(popups[i]);
|
||||
popups[i].setMap(null);
|
||||
markers[i].setMap(null);
|
||||
}
|
||||
}
|
||||
markers.length = 0;
|
||||
polies.length = 0;
|
||||
popups.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}
|
||||
*/
|
||||
function getMarkerIcon(fill, isLarge, isExtra) {
|
||||
return {
|
||||
anchor: new google.maps.Point(15, 35),
|
||||
url: uUI.getSvgSrc(fill, isLarge, isExtra)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set marker
|
||||
* @param {uTrack} track
|
||||
* @param {number} id
|
||||
*/
|
||||
function setMarker(id, track) {
|
||||
// marker
|
||||
const position = track.positions[id];
|
||||
const posLen = track.length;
|
||||
// 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: map
|
||||
});
|
||||
const isExtra = position.hasComment() || position.hasImage();
|
||||
let icon = getMarkerIcon(isExtra ? config.colorExtra : config.colorNormal, false, isExtra);
|
||||
if (id === posLen - 1) {
|
||||
icon = getMarkerIcon(config.colorStop, true, isExtra);
|
||||
} else if (id === 0) {
|
||||
icon = getMarkerIcon(config.colorStart, true, isExtra);
|
||||
}
|
||||
marker.setIcon(icon);
|
||||
// popup
|
||||
const popup = new google.maps.InfoWindow();
|
||||
|
||||
marker.addListener('click',
|
||||
((i) => () => {
|
||||
popup.setContent(uUI.getPopupHtml(i));
|
||||
popup.open(map, marker);
|
||||
binder.dispatchEvent(uEvent.MARKER_SELECT, i);
|
||||
openPopup = popup;
|
||||
popup.addListener('closeclick', () => {
|
||||
binder.dispatchEvent(uEvent.MARKER_SELECT);
|
||||
google.maps.event.clearListeners(popup, 'closeclick');
|
||||
openPopup = null;
|
||||
});
|
||||
})(id));
|
||||
marker.addListener('mouseover',
|
||||
((i) => () => {
|
||||
binder.dispatchEvent(uEvent.MARKER_OVER, i);
|
||||
})(id));
|
||||
marker.addListener('mouseout',
|
||||
() => {
|
||||
binder.dispatchEvent(uEvent.MARKER_OVER);
|
||||
});
|
||||
|
||||
markers.push(marker);
|
||||
popups.push(popup);
|
||||
}
|
||||
|
||||
function animateMarker(id) {
|
||||
if (openPopup) {
|
||||
openPopup.close();
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
const icon = markers[id].getIcon();
|
||||
markers[id].setIcon(getMarkerIcon(config.colorHilite, false, false));
|
||||
markers[id].setAnimation(google.maps.Animation.BOUNCE);
|
||||
timeoutHandle = setTimeout(() => {
|
||||
markers[id].setIcon(icon);
|
||||
markers[id].setAnimation(null);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map bounds
|
||||
* eg. ((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707))
|
||||
* @returns {number[]} Bounds
|
||||
*/
|
||||
function getBounds() {
|
||||
const bounds = 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
|
||||
*/
|
||||
function zoomToExtent() {
|
||||
const latlngbounds = new google.maps.LatLngBounds();
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const coordinates = new google.maps.LatLng(markers[i].position.lat(), markers[i].position.lng());
|
||||
latlngbounds.extend(coordinates);
|
||||
}
|
||||
map.fitBounds(latlngbounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to bounds
|
||||
* @param {number[]} bounds
|
||||
*/
|
||||
function 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);
|
||||
map.fitBounds(latLngBounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update size
|
||||
*/
|
||||
function updateSize() {
|
||||
// ignore for google API
|
||||
}
|
||||
|
||||
function setAuthError() { authError = true; }
|
||||
function setLoaded() { isLoaded = true; }
|
||||
|
||||
export {
|
||||
name,
|
||||
init,
|
||||
cleanup,
|
||||
displayTrack,
|
||||
clearMap,
|
||||
animateMarker,
|
||||
getBounds,
|
||||
zoomToExtent,
|
||||
zoomToBounds,
|
||||
updateSize
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Callback for Google Maps API
|
||||
* It will be called when authentication fails
|
||||
*/
|
||||
window.gm_authFailure = function () {
|
||||
setAuthError();
|
||||
let message = uUtils.sprintf(lang.strings['apifailure'], 'Google Maps');
|
||||
message += '<br><br>' + lang.strings['gmauthfailure'];
|
||||
message += '<br><br>' + lang.strings['gmapilink'];
|
||||
uUI.resolveModal(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback for Google Maps API
|
||||
* It will be called when API is loaded
|
||||
*/
|
||||
window.gm_loaded = function () {
|
||||
setLoaded();
|
||||
};
|
@ -1,536 +0,0 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import { config } from '../constants.js';
|
||||
import uEvent from '../event.js';
|
||||
import uUI from '../ui.js';
|
||||
import uUtils from '../utils.js';
|
||||
|
||||
// openlayers 3+
|
||||
/**
|
||||
* OpenLayers API module
|
||||
* @module olApi
|
||||
* @implements {uMap.api}
|
||||
*/
|
||||
|
||||
/** @type {ol.Map} */
|
||||
let map = null;
|
||||
/** @type {uBinder} */
|
||||
let binder = null;
|
||||
/** @type {ol.layer.Vector} */
|
||||
let layerTrack = null;
|
||||
/** @type {ol.layer.Vector} */
|
||||
let layerMarkers = null;
|
||||
/** @type {ol.layer.Base} */
|
||||
let selectedLayer = null;
|
||||
/** @type {ol.style.Style|{}} */
|
||||
let olStyles = {};
|
||||
/** @type {string} */
|
||||
const name = 'openlayers';
|
||||
/** @type {?number} */
|
||||
let pointOver = null;
|
||||
|
||||
/**
|
||||
* Initialize map
|
||||
* @param {uBinder} b
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
function init(b, target) {
|
||||
|
||||
binder = b;
|
||||
|
||||
uUtils.addScript('//cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList', 'mapapi_openlayers_polyfill');
|
||||
uUtils.addScript('js/lib/ol.js', 'mapapi_openlayers');
|
||||
uUtils.addCss('css/ol.css', 'ol_css');
|
||||
|
||||
const controls = [
|
||||
new ol.control.Zoom(),
|
||||
new ol.control.Rotate(),
|
||||
new ol.control.ScaleLine(),
|
||||
new ol.control.ZoomToExtent({ label: getExtentImg() })
|
||||
];
|
||||
|
||||
const view = new ol.View({
|
||||
center: ol.proj.fromLonLat([ config.init_longitude, config.init_latitude ]),
|
||||
zoom: 8
|
||||
});
|
||||
|
||||
map = new ol.Map({
|
||||
target: target,
|
||||
controls: controls,
|
||||
view: view
|
||||
});
|
||||
|
||||
map.on('pointermove', (e) => {
|
||||
const feature = map.forEachFeatureAtPixel(e.pixel,
|
||||
(_feature, _layer) => {
|
||||
if (_layer.get('name') === 'Markers') {
|
||||
return _feature;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
// emit mouse over marker event
|
||||
/** @type {?number} */
|
||||
const id = feature ? feature.getId() : null;
|
||||
if (id !== pointOver) {
|
||||
binder.dispatchEvent(uEvent.MARKER_OVER);
|
||||
pointOver = id;
|
||||
if (id) {
|
||||
binder.dispatchEvent(uEvent.MARKER_OVER, id);
|
||||
}
|
||||
}
|
||||
// change mouse cursor when over marker
|
||||
if (feature) {
|
||||
map.getTargetElement().style.cursor = 'pointer';
|
||||
} else {
|
||||
map.getTargetElement().style.cursor = '';
|
||||
}
|
||||
});
|
||||
|
||||
initLayers();
|
||||
initStyles();
|
||||
initPopups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize map layers
|
||||
*/
|
||||
function initLayers() {
|
||||
// default layer: OpenStreetMap
|
||||
const osm = new ol.layer.Tile({
|
||||
name: 'OpenStreetMap',
|
||||
visible: true,
|
||||
source: new ol.source.OSM()
|
||||
});
|
||||
map.addLayer(osm);
|
||||
selectedLayer = osm;
|
||||
|
||||
// add extra tile layers
|
||||
for (const layerName in config.ol_layers) {
|
||||
if (config.ol_layers.hasOwnProperty(layerName)) {
|
||||
const layerUrl = config.ol_layers[layerName];
|
||||
const ol_layer = new ol.layer.Tile({
|
||||
name: layerName,
|
||||
visible: false,
|
||||
source: new ol.source.XYZ({
|
||||
url: layerUrl
|
||||
})
|
||||
});
|
||||
map.addLayer(ol_layer);
|
||||
}
|
||||
}
|
||||
|
||||
// add track and markers layers
|
||||
const lineStyle = new ol.style.Style({
|
||||
stroke: new ol.style.Stroke({
|
||||
color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
|
||||
width: config.strokeWeight
|
||||
})
|
||||
});
|
||||
layerTrack = new ol.layer.Vector({
|
||||
name: 'Track',
|
||||
type: 'data',
|
||||
source: new ol.source.Vector(),
|
||||
style: lineStyle
|
||||
});
|
||||
layerMarkers = new ol.layer.Vector({
|
||||
name: 'Markers',
|
||||
type: 'data',
|
||||
source: new ol.source.Vector()
|
||||
});
|
||||
map.addLayer(layerTrack);
|
||||
map.addLayer(layerMarkers);
|
||||
|
||||
initLayerSwitcher();
|
||||
}
|
||||
|
||||
function initStyles() {
|
||||
olStyles = {};
|
||||
const iconStart = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorStart, true)
|
||||
});
|
||||
const iconStop = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorStop, true)
|
||||
});
|
||||
const iconStartExtra = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorStart, true, true)
|
||||
});
|
||||
const iconStopExtra = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorStop, true, true)
|
||||
});
|
||||
const iconNormal = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
opacity: 0.7,
|
||||
src: uUI.getSvgSrc(config.colorNormal, false)
|
||||
});
|
||||
const iconExtra = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorExtra, false, true)
|
||||
});
|
||||
const iconHilite = new ol.style.Icon({
|
||||
anchor: [ 0.5, 1 ],
|
||||
src: uUI.getSvgSrc(config.colorHilite, false)
|
||||
});
|
||||
olStyles['start'] = new ol.style.Style({
|
||||
image: iconStart
|
||||
});
|
||||
olStyles['stop'] = new ol.style.Style({
|
||||
image: iconStop
|
||||
});
|
||||
olStyles['startExtra'] = new ol.style.Style({
|
||||
image: iconStartExtra
|
||||
});
|
||||
olStyles['stopExtra'] = new ol.style.Style({
|
||||
image: iconStopExtra
|
||||
});
|
||||
olStyles['normal'] = new ol.style.Style({
|
||||
image: iconNormal
|
||||
});
|
||||
olStyles['extra'] = new ol.style.Style({
|
||||
image: iconExtra
|
||||
});
|
||||
olStyles['hilite'] = new ol.style.Style({
|
||||
image: iconHilite
|
||||
});
|
||||
}
|
||||
|
||||
function initPopups() {
|
||||
const popupContainer = document.createElement('div');
|
||||
popupContainer.id = 'popup';
|
||||
popupContainer.className = 'ol-popup';
|
||||
document.body.appendChild(popupContainer);
|
||||
const popupCloser = document.createElement('a');
|
||||
popupCloser.id = 'popup-closer';
|
||||
popupCloser.className = 'ol-popup-closer';
|
||||
popupCloser.href = '#';
|
||||
popupContainer.appendChild(popupCloser);
|
||||
const popupContent = document.createElement('div');
|
||||
popupContent.id = 'popup-content';
|
||||
popupContainer.appendChild(popupContent);
|
||||
|
||||
const popup = new ol.Overlay({
|
||||
element: popupContainer,
|
||||
autoPan: true,
|
||||
autoPanAnimation: {
|
||||
duration: 250
|
||||
}
|
||||
});
|
||||
|
||||
popupCloser.onclick = () => {
|
||||
// eslint-disable-next-line no-undefined
|
||||
popup.setPosition(undefined);
|
||||
popupCloser.blur();
|
||||
return false;
|
||||
};
|
||||
|
||||
// add click handler to map to show popup
|
||||
map.on('click', (e) => {
|
||||
const coordinate = e.coordinate;
|
||||
const feature = map.forEachFeatureAtPixel(e.pixel,
|
||||
(_feature, _layer) => {
|
||||
if (_layer.get('name') === 'Markers') {
|
||||
return _feature;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (feature) {
|
||||
// popup show
|
||||
popup.setPosition(coordinate);
|
||||
popupContent.innerHTML = uUI.getPopupHtml(feature.getId());
|
||||
map.addOverlay(popup);
|
||||
binder.dispatchEvent(uEvent.MARKER_SELECT, feature.getId());
|
||||
} else {
|
||||
// popup destroy
|
||||
// eslint-disable-next-line no-undefined
|
||||
popup.setPosition(undefined);
|
||||
binder.dispatchEvent(uEvent.MARKER_SELECT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initLayerSwitcher() {
|
||||
const switcher = document.createElement('div');
|
||||
switcher.id = 'switcher';
|
||||
switcher.className = 'ol-control';
|
||||
document.body.appendChild(switcher);
|
||||
const switcherContent = document.createElement('div');
|
||||
switcherContent.id = 'switcher-content';
|
||||
switcherContent.className = 'ol-layerswitcher';
|
||||
switcher.appendChild(switcherContent);
|
||||
|
||||
map.getLayers().forEach((_layer) => {
|
||||
const layerLabel = document.createElement('label');
|
||||
layerLabel.innerHTML = _layer.get('name');
|
||||
switcherContent.appendChild(layerLabel);
|
||||
|
||||
const layerRadio = document.createElement('input');
|
||||
if (_layer.get('type') === 'data') {
|
||||
layerRadio.type = 'checkbox';
|
||||
layerLabel.className = 'ol-datalayer';
|
||||
} else {
|
||||
layerRadio.type = 'radio';
|
||||
}
|
||||
layerRadio.name = 'layer';
|
||||
layerRadio.value = _layer.get('name');
|
||||
layerRadio.onclick = switchLayer;
|
||||
if (_layer.getVisible()) {
|
||||
layerRadio.checked = true;
|
||||
}
|
||||
layerLabel.insertBefore(layerRadio, layerLabel.childNodes[0]);
|
||||
});
|
||||
|
||||
function switchLayer() {
|
||||
const targetName = this.value;
|
||||
map.getLayers().forEach((_layer) => {
|
||||
if (_layer.get('name') === targetName) {
|
||||
if (_layer.get('type') === 'data') {
|
||||
if (_layer.getVisible()) {
|
||||
_layer.setVisible(false);
|
||||
} else {
|
||||
_layer.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
selectedLayer.setVisible(false);
|
||||
selectedLayer = _layer;
|
||||
_layer.setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const switcherButton = document.createElement('button');
|
||||
const layerImg = document.createElement('img');
|
||||
layerImg.src = 'images/layers.svg';
|
||||
layerImg.style.width = '60%';
|
||||
switcherButton.appendChild(layerImg);
|
||||
|
||||
const switcherHandle = () => {
|
||||
const el = document.getElementById('switcher');
|
||||
if (el.style.display === 'block') {
|
||||
el.style.display = 'none';
|
||||
} else {
|
||||
el.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
switcherButton.addEventListener('click', switcherHandle, false);
|
||||
switcherButton.addEventListener('touchstart', switcherHandle, false);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.className = 'ol-switcher-button ol-unselectable ol-control';
|
||||
element.appendChild(switcherButton);
|
||||
|
||||
const switcherControl = new ol.control.Control({
|
||||
element: element
|
||||
});
|
||||
map.addControl(switcherControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up API
|
||||
*/
|
||||
function cleanup() {
|
||||
layerTrack = null;
|
||||
layerMarkers = null;
|
||||
selectedLayer = null;
|
||||
olStyles = null;
|
||||
uUI.removeElementById('popup');
|
||||
uUI.removeElementById('switcher');
|
||||
if (map && map.getTargetElement()) {
|
||||
map.getTargetElement().innerHTML = '';
|
||||
}
|
||||
map = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track
|
||||
* @param {uTrack} track Track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
*/
|
||||
function displayTrack(track, update) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
const lineString = new ol.geom.LineString([]);
|
||||
for (const position of track.positions) {
|
||||
// set marker
|
||||
setMarker(i++, track);
|
||||
if (track.continuous) {
|
||||
// update polyline
|
||||
lineString.appendCoordinate(ol.proj.fromLonLat([ position.longitude, position.latitude ]));
|
||||
}
|
||||
}
|
||||
if (lineString.getLength() > 0) {
|
||||
const lineFeature = new ol.Feature({
|
||||
geometry: lineString
|
||||
});
|
||||
layerTrack.getSource().addFeature(lineFeature);
|
||||
}
|
||||
|
||||
let extent = layerMarkers.getSource().getExtent();
|
||||
|
||||
map.getControls().forEach((el) => {
|
||||
if (el instanceof ol.control.ZoomToExtent) {
|
||||
map.removeControl(el);
|
||||
}
|
||||
});
|
||||
|
||||
if (update) {
|
||||
map.getView().fit(extent);
|
||||
const zoom = map.getView().getZoom();
|
||||
if (zoom > 20) {
|
||||
map.getView().setZoom(20);
|
||||
extent = map.getView().calculateExtent(map.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
const zoomToExtentControl = new ol.control.ZoomToExtent({
|
||||
extent,
|
||||
label: getExtentImg()
|
||||
});
|
||||
map.addControl(zoomToExtentControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear map
|
||||
*/
|
||||
function clearMap() {
|
||||
if (layerTrack) {
|
||||
layerTrack.getSource().clear();
|
||||
}
|
||||
if (layerMarkers) {
|
||||
layerMarkers.getSource().clear();
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkerStyle(position, id, posLen) {
|
||||
let iconStyle = olStyles['normal'];
|
||||
if (position.hasComment() || position.hasImage()) {
|
||||
if (id === posLen - 1) {
|
||||
iconStyle = olStyles['stopExtra'];
|
||||
} else if (id === 0) {
|
||||
iconStyle = olStyles['startExtra'];
|
||||
} else {
|
||||
iconStyle = olStyles['extra'];
|
||||
}
|
||||
} else if (id === posLen - 1) {
|
||||
iconStyle = olStyles['stop'];
|
||||
} else if (id === 0) {
|
||||
iconStyle = olStyles['start'];
|
||||
}
|
||||
return iconStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set marker
|
||||
* @param {number} id
|
||||
* @param {uTrack} track
|
||||
*/
|
||||
function setMarker(id, track) {
|
||||
// marker
|
||||
const position = track.positions[id];
|
||||
const posLen = track.positions.length;
|
||||
const marker = new ol.Feature({
|
||||
geometry: new ol.geom.Point(ol.proj.fromLonLat([ position.longitude, position.latitude ]))
|
||||
});
|
||||
|
||||
const iconStyle = getMarkerStyle(position, id, posLen);
|
||||
marker.setStyle(iconStyle);
|
||||
marker.setId(id);
|
||||
layerMarkers.getSource().addFeature(marker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate marker
|
||||
* @param id Marker sequential id
|
||||
*/
|
||||
function animateMarker(id) {
|
||||
const marker = layerMarkers.getSource().getFeatureById(id);
|
||||
const initStyle = marker.getStyle();
|
||||
const iconStyle = olStyles['hilite'];
|
||||
marker.setStyle(iconStyle);
|
||||
setTimeout(() => marker.setStyle(initStyle), 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map bounds
|
||||
* eg. (20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563)
|
||||
* @returns {number[]} Bounds
|
||||
*/
|
||||
function getBounds() {
|
||||
const extent = map.getView().calculateExtent(map.getSize());
|
||||
const bounds = ol.proj.transformExtent(extent, 'EPSG:900913', 'EPSG:4326');
|
||||
const lon_sw = bounds[0];
|
||||
const lat_sw = bounds[1];
|
||||
const lon_ne = bounds[2];
|
||||
const lat_ne = bounds[3];
|
||||
return [ lon_sw, lat_sw, lon_ne, lat_ne ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to track extent
|
||||
*/
|
||||
function zoomToExtent() {
|
||||
map.getView().fit(layerMarkers.getSource().getExtent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to bounds
|
||||
* @param {number[]} bounds
|
||||
*/
|
||||
function zoomToBounds(bounds) {
|
||||
const extent = ol.proj.transformExtent(bounds, 'EPSG:4326', 'EPSG:900913');
|
||||
map.getView().fit(extent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update size
|
||||
*/
|
||||
function updateSize() {
|
||||
map.updateSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extent image
|
||||
* @returns {HTMLImageElement}
|
||||
*/
|
||||
function getExtentImg() {
|
||||
const extentImg = document.createElement('img');
|
||||
extentImg.src = 'images/extent.svg';
|
||||
extentImg.style.width = '60%';
|
||||
return extentImg;
|
||||
}
|
||||
|
||||
export {
|
||||
name,
|
||||
init,
|
||||
cleanup,
|
||||
displayTrack,
|
||||
clearMap,
|
||||
animateMarker,
|
||||
getBounds,
|
||||
zoomToExtent,
|
||||
zoomToBounds,
|
||||
updateSize
|
||||
}
|
1
js/ol.js.map
Normal file
1
js/ol.js.map
Normal file
File diff suppressed because one or more lines are too long
42
js/src/lib/ol.js
Normal file
42
js/src/lib/ol.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import { Control, Rotate, ScaleLine, Zoom, ZoomToExtent } from 'ol/control';
|
||||
import { LineString, Point } from 'ol/geom';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import Feature from 'ol/Feature';
|
||||
import Icon from 'ol/style/Icon';
|
||||
import Map from 'ol/Map';
|
||||
import OSM from 'ol/source/OSM';
|
||||
import Overlay from 'ol/Overlay';
|
||||
import Stroke from 'ol/style/Stroke';
|
||||
import Style from 'ol/style/Style';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import Vector from 'ol/source/Vector';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import View from 'ol/View';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
|
||||
export { Feature, Map, Overlay, View };
|
||||
export const control = { Control, Rotate, ScaleLine, Zoom, ZoomToExtent };
|
||||
export const geom = { LineString, Point };
|
||||
export const layer = { TileLayer, VectorLayer };
|
||||
export const proj = { fromLonLat, toLonLat };
|
||||
export const source = { OSM, Vector, XYZ };
|
||||
export const style = { Icon, Stroke, Style };
|
340
js/src/mapapi/api_gmaps.js
Normal file
340
js/src/mapapi/api_gmaps.js
Normal file
@ -0,0 +1,340 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import { config, lang } from '../initializer.js';
|
||||
import MapViewModel from '../mapviewmodel.js';
|
||||
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() {
|
||||
const params = `?${(config.gkey != null) ? `key=${config.gkey}&` : ''}callback=gm_loaded`;
|
||||
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;
|
||||
let message = uUtils.sprintf(lang.strings['apifailure'], 'Google Maps');
|
||||
message += '<br><br>' + lang.strings['gmauthfailure'];
|
||||
message += '<br><br>' + lang.strings['gmapilink'];
|
||||
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
|
||||
* @param {uTrack} track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
*/
|
||||
displayTrack(track, update) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
// init polyline
|
||||
const polyOptions = {
|
||||
strokeColor: config.strokeColor,
|
||||
strokeOpacity: config.strokeOpacity,
|
||||
strokeWeight: config.strokeWeight
|
||||
};
|
||||
// noinspection JSCheckFunctionSignatures
|
||||
const poly = new google.maps.Polyline(polyOptions);
|
||||
poly.setMap(this.map);
|
||||
const path = poly.getPath();
|
||||
const latlngbounds = new google.maps.LatLngBounds();
|
||||
let i = 0;
|
||||
for (const position of track.positions) {
|
||||
// set marker
|
||||
this.setMarker(i++, track);
|
||||
// update polyline
|
||||
const coordinates = new google.maps.LatLng(position.latitude, position.longitude);
|
||||
if (track.continuous) {
|
||||
path.push(coordinates);
|
||||
}
|
||||
latlngbounds.extend(coordinates);
|
||||
}
|
||||
if (update) {
|
||||
this.map.fitBounds(latlngbounds);
|
||||
if (i === 1) {
|
||||
// only one point, zoom out
|
||||
const zListener =
|
||||
google.maps.event.addListenerOnce(this.map, 'bounds_changed', function () {
|
||||
if (this.getZoom()) {
|
||||
this.setZoom(15);
|
||||
}
|
||||
});
|
||||
setTimeout(function () {
|
||||
google.maps.event.removeListener(zListener)
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
this.polies.push(poly);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {uTrack} track
|
||||
* @param {number} id
|
||||
*/
|
||||
setMarker(id, track) {
|
||||
// marker
|
||||
const position = track.positions[id];
|
||||
const posLen = track.length;
|
||||
// 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;
|
||||
if (id === posLen - 1) {
|
||||
icon = GoogleMapsApi.getMarkerIcon(config.colorStop, true, isExtra);
|
||||
} else if (id === 0) {
|
||||
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
|
||||
* eg. ((52.20105108685229, 20.789387865580238), (52.292069558807135, 21.172192736185707))
|
||||
* @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;
|
591
js/src/mapapi/api_openlayers.js
Normal file
591
js/src/mapapi/api_openlayers.js
Normal file
@ -0,0 +1,591 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import MapViewModel from '../mapviewmodel.js';
|
||||
import { config } from '../initializer.js';
|
||||
import uUtils from '../utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} MarkerStyles
|
||||
* @property {Style} normal
|
||||
* @property {Style} start
|
||||
* @property {Style} stop
|
||||
* @property {Style} extra
|
||||
* @property {Style} startExtra
|
||||
* @property {Style} stopExtra
|
||||
* @property {Style} hilite
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import("../lib/ol.js")} OpenLayers
|
||||
* @property {import("ol/control")} control
|
||||
* @property {import("ol/Feature").default} Feature
|
||||
* @property {import("ol/geom")} geom
|
||||
* @property {import("ol/layer/Tile").default} layer.TileLayer
|
||||
* @property {import("ol/layer/Vector").default} layer.VectorLayer
|
||||
* @property {import("ol/Map").default} Map
|
||||
* @property {import("ol/Overlay").default} Overlay
|
||||
* @property {import("ol/proj")} proj
|
||||
* @property {import("ol/source/OSM").default} source.OSM
|
||||
* @property {import("ol/source/Vector").default} source.Vector
|
||||
* @property {import("ol/source/XYZ").default} source.XYZ
|
||||
* @property {import("ol/style/Icon").default} style.Icon
|
||||
* @property {import("ol/style/Stroke").default} style.Stroke
|
||||
* @property {import("ol/style/Style").default} style.Style
|
||||
* @property {import("ol/View").default} View
|
||||
*/
|
||||
/** @type {?OpenLayers} */
|
||||
let ol;
|
||||
|
||||
/**
|
||||
* OpenLayers API
|
||||
* @class OpenLayersApi
|
||||
* @implements {MapViewModel.api}
|
||||
*/
|
||||
export default class OpenLayersApi {
|
||||
|
||||
/**
|
||||
* @param {MapViewModel} vm
|
||||
* @param {?OpenLayers=} olModule
|
||||
*/
|
||||
constructor(vm, olModule = null) {
|
||||
/** @type {Map} */
|
||||
this.map = null;
|
||||
/** @type {MapViewModel} */
|
||||
this.viewModel = vm;
|
||||
/** @type {VectorLayer} */
|
||||
this.layerTrack = null;
|
||||
/** @type {VectorLayer} */
|
||||
this.layerMarkers = null;
|
||||
/** @type {Layer} */
|
||||
this.selectedLayer = null;
|
||||
/** @type {?MarkerStyles} */
|
||||
this.markerStyles = null;
|
||||
/** @type {?Overlay} */
|
||||
this.popup = null;
|
||||
// for tests
|
||||
if (olModule) { ol = olModule; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize map
|
||||
* @return {Promise<void, Error>}
|
||||
*/
|
||||
init() {
|
||||
uUtils.addCss('css/ol.css', 'ol_css');
|
||||
const olReady = ol ? Promise.resolve() : import(/* webpackChunkName : "ol" */'../lib/ol.js').then((m) => { ol = m });
|
||||
return olReady.then(() => {
|
||||
this.initMap();
|
||||
this.initLayers();
|
||||
this.initStyles();
|
||||
this.initPopups();
|
||||
});
|
||||
}
|
||||
|
||||
initMap() {
|
||||
const controls = [
|
||||
new ol.control.Zoom(),
|
||||
new ol.control.Rotate(),
|
||||
new ol.control.ScaleLine()
|
||||
];
|
||||
|
||||
const view = new ol.View({
|
||||
center: ol.proj.fromLonLat([ config.initLongitude, config.initLatitude ]),
|
||||
zoom: 8
|
||||
});
|
||||
|
||||
this.map = new ol.Map({
|
||||
target: this.viewModel.mapElement,
|
||||
controls: controls,
|
||||
view: view
|
||||
});
|
||||
|
||||
this.map.on('pointermove', (e) => {
|
||||
const feature = this.map.forEachFeatureAtPixel(e.pixel,
|
||||
/**
|
||||
* @param {Feature} _feature
|
||||
* @param {Layer} _layer
|
||||
* @return {Feature}
|
||||
*/
|
||||
(_feature, _layer) => {
|
||||
if (_layer.get('name') === 'Markers') {
|
||||
return _feature;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (feature) {
|
||||
this.map.getTargetElement().style.cursor = 'pointer';
|
||||
const id = feature.getId();
|
||||
if (id !== this.viewModel.model.markerOver) {
|
||||
this.viewModel.model.markerOver = id;
|
||||
}
|
||||
} else {
|
||||
this.map.getTargetElement().style.cursor = '';
|
||||
this.viewModel.model.markerOver = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize map layers
|
||||
*/
|
||||
initLayers() {
|
||||
// default layer: OpenStreetMap
|
||||
const osm = new ol.layer.TileLayer({
|
||||
name: 'OpenStreetMap',
|
||||
visible: true,
|
||||
source: new ol.source.OSM()
|
||||
});
|
||||
this.map.addLayer(osm);
|
||||
this.selectedLayer = osm;
|
||||
|
||||
// add extra tile layers
|
||||
for (const layerName in config.olLayers) {
|
||||
if (config.olLayers.hasOwnProperty(layerName)) {
|
||||
const layerUrl = config.olLayers[layerName];
|
||||
const ol_layer = new ol.layer.TileLayer({
|
||||
name: layerName,
|
||||
visible: false,
|
||||
source: new ol.source.XYZ({
|
||||
url: layerUrl
|
||||
})
|
||||
});
|
||||
this.map.addLayer(ol_layer);
|
||||
}
|
||||
}
|
||||
|
||||
// add track and markers layers
|
||||
const lineStyle = new ol.style.Style({
|
||||
stroke: new ol.style.Stroke({
|
||||
color: uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity),
|
||||
width: config.strokeWeight
|
||||
})
|
||||
});
|
||||
this.layerTrack = new ol.layer.VectorLayer({
|
||||
name: 'Track',
|
||||
type: 'data',
|
||||
source: new ol.source.Vector(),
|
||||
style: lineStyle
|
||||
});
|
||||
this.layerMarkers = new ol.layer.VectorLayer({
|
||||
name: 'Markers',
|
||||
type: 'data',
|
||||
source: new ol.source.Vector()
|
||||
});
|
||||
this.map.addLayer(this.layerTrack);
|
||||
this.map.addLayer(this.layerMarkers);
|
||||
|
||||
this.initLayerSwitcher();
|
||||
}
|
||||
|
||||
initStyles() {
|
||||
const anchor = [ 0.5, 1 ];
|
||||
this.markerStyles = {
|
||||
start: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorStart, true)
|
||||
})
|
||||
}),
|
||||
stop: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorStop, true)
|
||||
})
|
||||
}),
|
||||
normal: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
opacity: 0.7,
|
||||
src: MapViewModel.getSvgSrc(config.colorNormal, false)
|
||||
})
|
||||
}),
|
||||
extra: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorExtra, false, true)
|
||||
})
|
||||
}),
|
||||
startExtra: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorStart, true, true)
|
||||
})
|
||||
}),
|
||||
stopExtra: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorStop, true, true)
|
||||
})
|
||||
}),
|
||||
hilite: new ol.style.Style({
|
||||
image: new ol.style.Icon({
|
||||
anchor: anchor,
|
||||
src: MapViewModel.getSvgSrc(config.colorHilite, false)
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
initPopups() {
|
||||
const popupContainer = document.createElement('div');
|
||||
popupContainer.id = 'popup-container';
|
||||
popupContainer.className = 'ol-popup';
|
||||
const popupContent = document.createElement('div');
|
||||
popupContent.id = 'popup-content';
|
||||
popupContainer.appendChild(popupContent);
|
||||
const popupCloser = document.createElement('a');
|
||||
popupCloser.id = 'popup-closer';
|
||||
popupCloser.className = 'ol-popup-closer';
|
||||
popupCloser.href = '#';
|
||||
popupContainer.appendChild(popupCloser);
|
||||
|
||||
|
||||
this.popup = new ol.Overlay({
|
||||
element: popupContainer,
|
||||
autoPan: true,
|
||||
autoPanAnimation: {
|
||||
duration: 250
|
||||
}
|
||||
});
|
||||
this.map.addOverlay(this.popup);
|
||||
|
||||
popupCloser.onclick = () => {
|
||||
this.popupClose();
|
||||
popupCloser.blur();
|
||||
return false;
|
||||
};
|
||||
|
||||
// add click handler to map to show popup
|
||||
this.map.on('click', (e) => {
|
||||
const coordinate = e.coordinate;
|
||||
const feature = this.map.forEachFeatureAtPixel(e.pixel,
|
||||
/** @param {Feature} _feature
|
||||
* @param {Layer} _layer
|
||||
* @return {?Feature}
|
||||
*/
|
||||
(_feature, _layer) => {
|
||||
if (_layer.get('name') === 'Markers') {
|
||||
return _feature;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (feature) {
|
||||
this.popupOpen(feature.getId(), coordinate);
|
||||
} else {
|
||||
this.popupClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show popup at coordinate
|
||||
* @param {number} id
|
||||
* @param {Coordinate} coordinate
|
||||
*/
|
||||
popupOpen(id, coordinate) {
|
||||
this.popup.getElement().firstElementChild.innerHTML = this.viewModel.getPopupHtml(id);
|
||||
this.popup.setPosition(coordinate);
|
||||
this.viewModel.model.markerSelect = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close popup
|
||||
*/
|
||||
popupClose() {
|
||||
// eslint-disable-next-line no-undefined
|
||||
this.popup.setPosition(undefined);
|
||||
this.viewModel.model.markerSelect = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch layer to target
|
||||
* @param {string} targetName
|
||||
*/
|
||||
switchLayer(targetName) {
|
||||
this.map.getLayers().forEach(/** @param {Layer} _layer */(_layer) => {
|
||||
if (_layer.get('name') === targetName) {
|
||||
if (_layer.get('type') === 'data') {
|
||||
if (_layer.getVisible()) {
|
||||
_layer.setVisible(false);
|
||||
} else {
|
||||
_layer.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
this.selectedLayer.setVisible(false);
|
||||
this.selectedLayer = _layer;
|
||||
_layer.setVisible(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initLayerSwitcher() {
|
||||
const switcher = document.createElement('div');
|
||||
switcher.id = 'switcher';
|
||||
switcher.className = 'ol-control';
|
||||
document.body.appendChild(switcher);
|
||||
const switcherContent = document.createElement('div');
|
||||
switcherContent.id = 'switcher-content';
|
||||
switcherContent.className = 'ol-layerswitcher';
|
||||
switcher.appendChild(switcherContent);
|
||||
|
||||
this.map.getLayers().forEach(/** @param {Layer} _layer */(_layer) => {
|
||||
const layerLabel = document.createElement('label');
|
||||
layerLabel.innerHTML = _layer.get('name');
|
||||
switcherContent.appendChild(layerLabel);
|
||||
|
||||
const layerRadio = document.createElement('input');
|
||||
if (_layer.get('type') === 'data') {
|
||||
layerRadio.type = 'checkbox';
|
||||
layerLabel.className = 'ol-datalayer';
|
||||
} else {
|
||||
layerRadio.type = 'radio';
|
||||
}
|
||||
layerRadio.name = 'layer';
|
||||
layerRadio.value = _layer.get('name');
|
||||
layerRadio.onclick = (e) => this.switchLayer(e.value);
|
||||
if (_layer.getVisible()) {
|
||||
layerRadio.checked = true;
|
||||
}
|
||||
layerLabel.insertBefore(layerRadio, layerLabel.childNodes[0]);
|
||||
});
|
||||
|
||||
const switcherButton = document.createElement('button');
|
||||
const layerImg = document.createElement('img');
|
||||
layerImg.src = 'images/layers.svg';
|
||||
layerImg.style.width = '60%';
|
||||
switcherButton.appendChild(layerImg);
|
||||
|
||||
const switcherHandle = () => {
|
||||
if (switcher.style.display === 'block') {
|
||||
switcher.style.display = 'none';
|
||||
} else {
|
||||
switcher.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
switcherButton.addEventListener('click', switcherHandle, false);
|
||||
switcherButton.addEventListener('touchstart', switcherHandle, false);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.className = 'ol-switcher-button ol-unselectable ol-control';
|
||||
element.appendChild(switcherButton);
|
||||
|
||||
const switcherControl = new ol.control.Control({
|
||||
element: element
|
||||
});
|
||||
this.map.addControl(switcherControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up API
|
||||
*/
|
||||
cleanup() {
|
||||
this.layerTrack = null;
|
||||
this.layerMarkers = null;
|
||||
this.selectedLayer = null;
|
||||
this.markerStyles = null;
|
||||
uUtils.removeElementById('switcher');
|
||||
if (this.map && this.map.getTargetElement()) {
|
||||
this.map.getTargetElement().innerHTML = '';
|
||||
}
|
||||
this.map = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track
|
||||
* @param {uTrack} track Track
|
||||
* @param {boolean} update Should fit bounds if true
|
||||
*/
|
||||
displayTrack(track, update) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
const lineString = new ol.geom.LineString([]);
|
||||
for (const position of track.positions) {
|
||||
// set marker
|
||||
this.setMarker(i++, track);
|
||||
if (track.continuous) {
|
||||
// update polyline
|
||||
lineString.appendCoordinate(ol.proj.fromLonLat([ position.longitude, position.latitude ]));
|
||||
}
|
||||
}
|
||||
if (lineString.getLength() > 0) {
|
||||
const lineFeature = new ol.Feature({
|
||||
geometry: lineString
|
||||
});
|
||||
this.layerTrack.getSource().addFeature(lineFeature);
|
||||
}
|
||||
|
||||
let extent = this.layerMarkers.getSource().getExtent();
|
||||
|
||||
if (update) {
|
||||
extent = this.fitToExtent(extent);
|
||||
}
|
||||
this.setZoomToExtent(extent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or replace ZoomToExtent control
|
||||
* @param extent
|
||||
*/
|
||||
setZoomToExtent(extent) {
|
||||
this.map.getControls().forEach((el) => {
|
||||
if (el instanceof ol.control.ZoomToExtent) {
|
||||
this.map.removeControl(el);
|
||||
}
|
||||
});
|
||||
this.map.addControl(new ol.control.ZoomToExtent({
|
||||
extent,
|
||||
label: OpenLayersApi.getExtentImg()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fit to extent, zoom out if needed
|
||||
* @param {Array.<number>} extent
|
||||
* @return {Array.<number>}
|
||||
*/
|
||||
fitToExtent(extent) {
|
||||
this.map.getView().fit(extent, { padding: [ 40, 10, 10, 10 ] });
|
||||
const zoom = this.map.getView().getZoom();
|
||||
if (zoom > OpenLayersApi.ZOOM_MAX) {
|
||||
this.map.getView().setZoom(OpenLayersApi.ZOOM_MAX);
|
||||
extent = this.map.getView().calculateExtent(this.map.getSize());
|
||||
}
|
||||
return extent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear map
|
||||
*/
|
||||
clearMap() {
|
||||
if (this.layerTrack) {
|
||||
this.layerTrack.getSource().clear();
|
||||
}
|
||||
if (this.layerMarkers) {
|
||||
this.layerMarkers.getSource().clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get marker style
|
||||
* @param {number} id
|
||||
* @param {uTrack} track
|
||||
* @return {Style}
|
||||
*/
|
||||
getMarkerStyle(id, track) {
|
||||
const position = track.positions[id];
|
||||
let iconStyle = this.markerStyles.normal;
|
||||
if (position.hasComment() || position.hasImage()) {
|
||||
if (id === track.positions.length - 1) {
|
||||
iconStyle = this.markerStyles.stopExtra;
|
||||
} else if (id === 0) {
|
||||
iconStyle = this.markerStyles.startExtra;
|
||||
} else {
|
||||
iconStyle = this.markerStyles.extra;
|
||||
}
|
||||
} else if (id === track.positions.length - 1) {
|
||||
iconStyle = this.markerStyles.stop;
|
||||
} else if (id === 0) {
|
||||
iconStyle = this.markerStyles.start;
|
||||
}
|
||||
return iconStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set marker
|
||||
* @param {number} id
|
||||
* @param {uTrack} track
|
||||
*/
|
||||
setMarker(id, track) {
|
||||
// marker
|
||||
const position = track.positions[id];
|
||||
const marker = new ol.Feature({
|
||||
geometry: new ol.geom.Point(ol.proj.fromLonLat([ position.longitude, position.latitude ]))
|
||||
});
|
||||
|
||||
const iconStyle = this.getMarkerStyle(id, track);
|
||||
marker.setStyle(iconStyle);
|
||||
marker.setId(id);
|
||||
this.layerMarkers.getSource().addFeature(marker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate marker
|
||||
* @param id Marker sequential id
|
||||
*/
|
||||
animateMarker(id) {
|
||||
const marker = this.layerMarkers.getSource().getFeatureById(id);
|
||||
const initStyle = marker.getStyle();
|
||||
marker.setStyle(this.markerStyles.hilite);
|
||||
setTimeout(() => marker.setStyle(initStyle), 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get map bounds
|
||||
* eg. (20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563)
|
||||
* @returns {number[]} Bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||
*/
|
||||
getBounds() {
|
||||
const extent = this.map.getView().calculateExtent(this.map.getSize());
|
||||
const sw = ol.proj.toLonLat([ extent[0], extent[1] ]);
|
||||
const ne = ol.proj.toLonLat([ extent[2], extent[3] ]);
|
||||
return [ sw[0], sw[1], ne[0], ne[1] ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to track extent
|
||||
*/
|
||||
zoomToExtent() {
|
||||
this.map.getView().fit(this.layerMarkers.getSource().getExtent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom to bounds
|
||||
* @param {number[]} bounds [ lon_sw, lat_sw, lon_ne, lat_ne ]
|
||||
*/
|
||||
zoomToBounds(bounds) {
|
||||
const sw = ol.proj.fromLonLat([ bounds[0], bounds[1] ]);
|
||||
const ne = ol.proj.fromLonLat([ bounds[2], bounds[3] ]);
|
||||
this.map.getView().fit([ sw[0], sw[1], ne[0], ne[1] ]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update size
|
||||
*/
|
||||
updateSize() {
|
||||
this.map.updateSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extent image
|
||||
* @returns {HTMLImageElement}
|
||||
*/
|
||||
static getExtentImg() {
|
||||
const extentImg = document.createElement('img');
|
||||
extentImg.src = 'images/extent.svg';
|
||||
extentImg.style.width = '60%';
|
||||
return extentImg;
|
||||
}
|
||||
}
|
||||
OpenLayersApi.ZOOM_MAX = 20;
|
491
js/test/api_gmaps.test.js
Normal file
491
js/test/api_gmaps.test.js
Normal file
@ -0,0 +1,491 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
import * as gmStub from './googlemaps.stub.js';
|
||||
import { config, lang } from '../src/initializer.js'
|
||||
import GoogleMapsApi from '../src/mapapi/api_gmaps.js';
|
||||
import uPosition from '../src/position.js';
|
||||
import uTrack from '../src/track.js';
|
||||
import uUser from '../src/user.js';
|
||||
import uUtils from '../src/utils.js';
|
||||
|
||||
describe('Google Maps map API tests', () => {
|
||||
let container;
|
||||
const loadTimeout = 100;
|
||||
let mockViewModel;
|
||||
let api;
|
||||
|
||||
beforeEach(() => {
|
||||
gmStub.setupGmapsStub();
|
||||
GoogleMapsApi.authError = false;
|
||||
GoogleMapsApi.gmInitialized = false;
|
||||
config.initialize();
|
||||
container = document.createElement('div');
|
||||
mockViewModel = { mapElement: container, model: {} };
|
||||
api = new GoogleMapsApi(mockViewModel);
|
||||
spyOn(google.maps, 'InfoWindow').and.callThrough();
|
||||
spyOn(google.maps, 'LatLngBounds').and.callThrough();
|
||||
spyOn(google.maps, 'Map').and.callThrough();
|
||||
spyOn(google.maps, 'Marker').and.callThrough();
|
||||
spyOn(google.maps, 'Polyline').and.callThrough();
|
||||
spyOnProperty(GoogleMapsApi, 'loadTimeoutMs', 'get').and.returnValue(loadTimeout);
|
||||
spyOn(window, 'alert');
|
||||
gmStub.applyPrototypes();
|
||||
});
|
||||
|
||||
afterEach(() => gmStub.clear());
|
||||
|
||||
it('should timeout initialization of map engine', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
|
||||
// when
|
||||
api.init()
|
||||
.then(() => done.fail('resolve callback called'))
|
||||
.catch((e) => {
|
||||
expect(e.message).toContain('timeout');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail loading script', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.reject(Error('script loading error')));
|
||||
// when
|
||||
api.init()
|
||||
.then(() => done.fail('resolve callback called'))
|
||||
.catch((e) => {
|
||||
expect(e.message).toContain('loading');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should load and initialize api scripts', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
|
||||
spyOn(api, 'initMap');
|
||||
config.gkey = 'key1234567890';
|
||||
// when
|
||||
api.init()
|
||||
.then(() => {
|
||||
// then
|
||||
expect(uUtils.loadScript).toHaveBeenCalledWith(`https://maps.googleapis.com/maps/api/js?key=${config.gkey}&callback=gm_loaded`, 'mapapi_gmaps', loadTimeout);
|
||||
expect(api.initMap).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
})
|
||||
.catch((e) => done.fail(e));
|
||||
window.gm_loaded();
|
||||
});
|
||||
|
||||
it('should initialize map engine with config values', () => {
|
||||
// given
|
||||
spyOn(google.maps.InfoWindow.prototype, 'addListener');
|
||||
// when
|
||||
api.initMap();
|
||||
// then
|
||||
expect(google.maps.Map).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.Map.calls.mostRecent().args[0]).toEqual(container);
|
||||
expect(google.maps.Map.calls.mostRecent().args[1].center.latitude).toEqual(config.initLatitude);
|
||||
expect(google.maps.Map.calls.mostRecent().args[1].center.longitude).toEqual(config.initLongitude);
|
||||
expect(google.maps.InfoWindow).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.InfoWindow.prototype.addListener).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.InfoWindow.prototype.addListener).toHaveBeenCalledWith('closeclick', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should initialize map engine with null gkey', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
|
||||
// when
|
||||
api.init()
|
||||
.then(() => {
|
||||
expect(google.maps.Map).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
})
|
||||
.catch((e) => done.fail(e));
|
||||
window.gm_loaded();
|
||||
// then
|
||||
expect(uUtils.loadScript).toHaveBeenCalledWith('https://maps.googleapis.com/maps/api/js?callback=gm_loaded', 'mapapi_gmaps', loadTimeout);
|
||||
});
|
||||
|
||||
it('should fail with authorization error', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
|
||||
lang.strings['apifailure'] = 'authfailure';
|
||||
// when
|
||||
api.init()
|
||||
.then(() => done.fail('resolve callback called'))
|
||||
.catch((e) => {
|
||||
// then
|
||||
expect(e.message).toContain(lang.strings['apifailure']);
|
||||
done();
|
||||
});
|
||||
window.gm_authFailure();
|
||||
});
|
||||
|
||||
it('should show alert if authorization error occurs after initialization', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'loadScript').and.returnValue(Promise.resolve());
|
||||
lang.strings['apifailure'] = 'authfailure';
|
||||
// when
|
||||
api.init()
|
||||
.then(() => {
|
||||
expect(google.maps.Map).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
})
|
||||
.catch((e) => done.fail(e));
|
||||
window.gm_loaded();
|
||||
window.gm_authFailure();
|
||||
|
||||
expect(window.alert).toHaveBeenCalledTimes(1);
|
||||
expect(window.alert.calls.mostRecent().args[0]).toContain(lang.strings['apifailure']);
|
||||
});
|
||||
|
||||
it('should clean up class fields', () => {
|
||||
// given
|
||||
api.polies.length = 1;
|
||||
api.markers.length = 1;
|
||||
api.popup = new google.maps.InfoWindow();
|
||||
container.innerHTML = 'content';
|
||||
api.map = new google.maps.Map(container);
|
||||
spyOn(google.maps.Map.prototype, 'getDiv').and.returnValue(container);
|
||||
// when
|
||||
api.cleanup();
|
||||
// then
|
||||
expect(api.polies.length).toBe(0);
|
||||
expect(api.markers.length).toBe(0);
|
||||
expect(api.popup).toBe(null);
|
||||
expect(container.innerHTML).toBe('');
|
||||
expect(api.map).toBe(null);
|
||||
});
|
||||
|
||||
it('should clear map features', () => {
|
||||
// given
|
||||
const poly = new google.maps.Polyline();
|
||||
const marker = new google.maps.Marker();
|
||||
const popup = new google.maps.InfoWindow();
|
||||
api.polies.push(poly);
|
||||
api.markers.push(marker);
|
||||
api.popup = popup;
|
||||
spyOn(api, 'popupClose');
|
||||
poly.setMap = () => {/* ignore */};
|
||||
spyOn(poly, 'setMap');
|
||||
marker.setMap = () => {/* ignore */};
|
||||
spyOn(marker, 'setMap');
|
||||
popup.setContent = () => {/* ignore */};
|
||||
spyOn(popup, 'setContent');
|
||||
spyOn(google.maps.InfoWindow.prototype, 'getMap').and.returnValue(true);
|
||||
// when
|
||||
api.clearMap();
|
||||
// then
|
||||
expect(poly.setMap).toHaveBeenCalledWith(null);
|
||||
expect(api.polies.length).toBe(0);
|
||||
expect(marker.setMap).toHaveBeenCalledWith(null);
|
||||
expect(api.markers.length).toBe(0);
|
||||
expect(popup.setContent).toHaveBeenCalledWith('');
|
||||
expect(api.popupClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should construct track polyline and markers', () => {
|
||||
// given
|
||||
const track = getTrack();
|
||||
spyOn(api, 'setMarker');
|
||||
spyOn(google.maps, 'LatLng').and.callThrough();
|
||||
spyOn(google.maps.LatLngBounds.prototype, 'extend').and.callThrough();
|
||||
const expectedPolyOptions = {
|
||||
strokeColor: config.strokeColor,
|
||||
strokeOpacity: config.strokeOpacity,
|
||||
strokeWeight: config.strokeWeight
|
||||
};
|
||||
// when
|
||||
api.displayTrack(track, false);
|
||||
// then
|
||||
expect(api.polies.length).toBe(1);
|
||||
expect(api.polies[0].path.length).toBe(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(0, track);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(1, track);
|
||||
expect(google.maps.Polyline).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.Polyline).toHaveBeenCalledWith(expectedPolyOptions);
|
||||
expect(google.maps.LatLng.calls.mostRecent().args[0]).toEqual(track.positions[track.positions.length - 1].latitude);
|
||||
expect(google.maps.LatLng.calls.mostRecent().args[1]).toEqual(track.positions[track.positions.length - 1].longitude);
|
||||
expect(google.maps.LatLngBounds.prototype.extend).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(google.maps.LatLngBounds.prototype.extend.calls.mostRecent().args[0].latitude).toEqual(track.positions[track.positions.length - 1].latitude);
|
||||
expect(google.maps.LatLngBounds.prototype.extend.calls.mostRecent().args[0].longitude).toEqual(track.positions[track.positions.length - 1].longitude);
|
||||
});
|
||||
|
||||
it('should construct non-continuous track markers without polyline', () => {
|
||||
// given
|
||||
const track = getTrack();
|
||||
spyOn(api, 'setMarker');
|
||||
// when
|
||||
track.continuous = false;
|
||||
api.displayTrack(track, false);
|
||||
// then
|
||||
expect(api.polies.length).toBe(1);
|
||||
expect(api.polies[0].path.length).toBe(0);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
});
|
||||
|
||||
it('should fit bounds if update without zoom (should not add listener for "bounds_changed")', () => {
|
||||
// given
|
||||
const track = getTrack();
|
||||
spyOn(google.maps.event, 'addListenerOnce');
|
||||
spyOn(google.maps.Map.prototype, 'fitBounds');
|
||||
spyOn(api, 'setMarker');
|
||||
spyOn(window, 'setTimeout');
|
||||
api.map = new google.maps.Map(container);
|
||||
// when
|
||||
api.displayTrack(track, true);
|
||||
// then
|
||||
expect(api.polies.length).toBe(1);
|
||||
expect(api.polies[0].path.length).toBe(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(google.maps.Map.prototype.fitBounds).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.event.addListenerOnce).not.toHaveBeenCalled();
|
||||
expect(setTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fit bounds and zoom (add listener for "bounds_changed") if update with single position', () => {
|
||||
// given
|
||||
const track = getTrack(1);
|
||||
spyOn(google.maps.event, 'addListenerOnce');
|
||||
spyOn(google.maps.Map.prototype, 'fitBounds');
|
||||
spyOn(api, 'setMarker');
|
||||
spyOn(window, 'setTimeout');
|
||||
api.map = new google.maps.Map(container);
|
||||
// when
|
||||
api.displayTrack(track, true);
|
||||
// then
|
||||
expect(api.polies.length).toBe(1);
|
||||
expect(api.polies[0].path.length).toBe(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(google.maps.Map.prototype.fitBounds).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.event.addListenerOnce.calls.mostRecent().args[1]).toBe('bounds_changed');
|
||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should create marker from track position and add it to markers array', () => {
|
||||
// given
|
||||
const track = getTrack(1);
|
||||
track.positions[0].timestamp = 1;
|
||||
spyOn(google.maps.Marker.prototype, 'addListener');
|
||||
spyOn(google.maps.Marker.prototype, 'setIcon');
|
||||
spyOn(GoogleMapsApi, 'getMarkerIcon');
|
||||
api.map = new google.maps.Map(container);
|
||||
|
||||
expect(api.markers.length).toBe(0);
|
||||
// when
|
||||
api.setMarker(0, track);
|
||||
// then
|
||||
expect(google.maps.Marker).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.Marker.calls.mostRecent().args[0].position.latitude).toBe(track.positions[0].latitude);
|
||||
expect(google.maps.Marker.calls.mostRecent().args[0].position.longitude).toBe(track.positions[0].longitude);
|
||||
expect(google.maps.Marker.calls.mostRecent().args[0].title).toContain('1970');
|
||||
expect(google.maps.Marker.calls.mostRecent().args[0].map).toEqual(api.map);
|
||||
expect(google.maps.Marker.prototype.setIcon).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.Marker.prototype.addListener).toHaveBeenCalledTimes(3);
|
||||
expect(google.maps.Marker.prototype.addListener).toHaveBeenCalledWith('click', jasmine.any(Function));
|
||||
expect(google.maps.Marker.prototype.addListener).toHaveBeenCalledWith('mouseover', jasmine.any(Function));
|
||||
expect(google.maps.Marker.prototype.addListener).toHaveBeenCalledWith('mouseout', jasmine.any(Function));
|
||||
expect(api.markers.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should create marker different marker icon for start, end and normal position', () => {
|
||||
// given
|
||||
const track = getTrack(3);
|
||||
spyOn(google.maps.Marker.prototype, 'setIcon');
|
||||
spyOn(GoogleMapsApi, 'getMarkerIcon');
|
||||
api.map = new google.maps.Map(container);
|
||||
// when
|
||||
api.setMarker(0, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(1);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(config.colorStart, true, jasmine.any(Boolean));
|
||||
// when
|
||||
api.setMarker(1, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(2);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(config.colorNormal, false, jasmine.any(Boolean));
|
||||
// when
|
||||
api.setMarker(2, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(3);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(config.colorStop, true, jasmine.any(Boolean));
|
||||
});
|
||||
|
||||
it('should create different marker for position with comment or image', () => {
|
||||
// given
|
||||
const track = getTrack(4);
|
||||
const positionWithComment = 0;
|
||||
const positionWithImage = 1;
|
||||
const positionWithImageAndComment = 2;
|
||||
const positionWithoutCommentAndImage = 3;
|
||||
track.positions[positionWithComment].comment = 'comment 0';
|
||||
track.positions[positionWithImage].image = 'image 1';
|
||||
track.positions[positionWithImageAndComment].comment = 'comment 2';
|
||||
track.positions[positionWithImageAndComment].image = 'image 2';
|
||||
spyOn(google.maps.Marker.prototype, 'setIcon');
|
||||
spyOn(GoogleMapsApi, 'getMarkerIcon');
|
||||
api.map = new google.maps.Map(container);
|
||||
// when
|
||||
api.setMarker(positionWithComment, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(1);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Boolean), true);
|
||||
// when
|
||||
api.setMarker(positionWithImage, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(2);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Boolean), true);
|
||||
// when
|
||||
api.setMarker(positionWithImageAndComment, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(3);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Boolean), true);
|
||||
// when
|
||||
api.setMarker(positionWithoutCommentAndImage, track);
|
||||
// then
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledTimes(4);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Boolean), false);
|
||||
});
|
||||
|
||||
it('should open popup for given marker with content generated by id', () => {
|
||||
// given
|
||||
const popup = new google.maps.InfoWindow();
|
||||
const marker = new google.maps.Marker();
|
||||
const id = 1;
|
||||
spyOn(popup, 'setContent').and.callThrough();
|
||||
spyOn(popup, 'open');
|
||||
mockViewModel.getPopupHtml = (i) => `content ${i}`;
|
||||
spyOn(mockViewModel, 'getPopupHtml').and.callThrough();
|
||||
api.map = new google.maps.Map(container);
|
||||
// when
|
||||
api.popup = popup;
|
||||
api.popupOpen(id, marker);
|
||||
// then
|
||||
expect(mockViewModel.getPopupHtml).toHaveBeenCalledWith(id);
|
||||
expect(popup.setContent).toHaveBeenCalledWith(`content ${id}`);
|
||||
expect(popup.open).toHaveBeenCalledWith(api.map, marker);
|
||||
expect(api.viewModel.model.markerSelect).toBe(id);
|
||||
});
|
||||
|
||||
it('should close popup', () => {
|
||||
// given
|
||||
const popup = new google.maps.InfoWindow();
|
||||
spyOn(popup, 'close');
|
||||
api.map = new google.maps.Map(container);
|
||||
api.viewModel.model.markerSelect = 1;
|
||||
// when
|
||||
api.popup = popup;
|
||||
api.popupClose();
|
||||
// then
|
||||
expect(popup.close).toHaveBeenCalledTimes(1);
|
||||
expect(api.viewModel.model.markerSelect).toBe(null);
|
||||
});
|
||||
|
||||
it('should animate marker with given index', (done) => {
|
||||
// given
|
||||
const marker = new google.maps.Marker();
|
||||
const iconOriginal = new google.maps.Icon();
|
||||
const iconAnimated = new google.maps.Icon();
|
||||
api.markers.push(marker);
|
||||
api.popup = new google.maps.InfoWindow();
|
||||
spyOn(google.maps.Marker.prototype, 'getIcon').and.returnValue(iconOriginal);
|
||||
spyOn(google.maps.Marker.prototype, 'setIcon').and.callThrough();
|
||||
spyOn(google.maps.Marker.prototype, 'setAnimation');
|
||||
spyOn(GoogleMapsApi, 'getMarkerIcon').and.returnValue(iconAnimated);
|
||||
spyOn(google.maps.InfoWindow.prototype, 'getMap').and.returnValue(true);
|
||||
spyOn(api, 'popupClose');
|
||||
// when
|
||||
api.animateMarker(0);
|
||||
// then
|
||||
expect(api.popupClose).toHaveBeenCalledTimes(1);
|
||||
expect(google.maps.Marker.prototype.setIcon).toHaveBeenCalledWith(iconAnimated);
|
||||
expect(GoogleMapsApi.getMarkerIcon).toHaveBeenCalledWith(config.colorHilite, jasmine.any(Boolean), jasmine.any(Boolean));
|
||||
expect(google.maps.Marker.prototype.setAnimation).toHaveBeenCalledWith(google.maps.Animation.BOUNCE);
|
||||
setTimeout(() => {
|
||||
expect(google.maps.Marker.prototype.setIcon).toHaveBeenCalledWith(iconOriginal);
|
||||
expect(google.maps.Marker.prototype.setAnimation).toHaveBeenCalledWith(null);
|
||||
done();
|
||||
}, 2100);
|
||||
});
|
||||
|
||||
it('should return map bounds array', () => {
|
||||
// given
|
||||
const ne = new google.maps.LatLng(1, 2);
|
||||
const sw = new google.maps.LatLng(3, 4);
|
||||
const bounds = new google.maps.LatLngBounds(sw, ne);
|
||||
api.map = new google.maps.Map(container);
|
||||
spyOn(google.maps.Map.prototype, 'getBounds').and.returnValue(bounds);
|
||||
// when
|
||||
const result = api.getBounds();
|
||||
// then
|
||||
expect(result).toEqual([ sw.longitude, sw.latitude, ne.longitude, ne.latitude ]);
|
||||
});
|
||||
|
||||
it('should zoom to markers extent', () => {
|
||||
// given
|
||||
api.markers.push(new google.maps.Marker());
|
||||
api.markers.push(new google.maps.Marker());
|
||||
api.map = new google.maps.Map(container);
|
||||
spyOn(google.maps.LatLngBounds.prototype, 'extend');
|
||||
spyOn(google.maps.Map.prototype, 'fitBounds');
|
||||
// when
|
||||
api.zoomToExtent();
|
||||
// then
|
||||
expect(google.maps.LatLngBounds.prototype.extend).toHaveBeenCalledTimes(api.markers.length);
|
||||
expect(google.maps.Map.prototype.fitBounds).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should zoom to given bounds array', () => {
|
||||
// given
|
||||
const ne = new google.maps.LatLng(1, 2);
|
||||
const sw = new google.maps.LatLng(3, 4);
|
||||
const fitBounds = new google.maps.LatLngBounds(sw, ne);
|
||||
const bounds = [ sw.longitude, sw.latitude, ne.longitude, ne.latitude ];
|
||||
api.map = new google.maps.Map(container);
|
||||
spyOn(google.maps.Map.prototype, 'fitBounds');
|
||||
// when
|
||||
api.zoomToBounds(bounds);
|
||||
// then
|
||||
expect(google.maps.Map.prototype.fitBounds).toHaveBeenCalledWith(fitBounds);
|
||||
});
|
||||
|
||||
it('should return timeout in ms', () => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOnProperty(GoogleMapsApi, 'loadTimeoutMs', 'get').and.callThrough();
|
||||
|
||||
expect(GoogleMapsApi.loadTimeoutMs).toEqual(jasmine.any(Number));
|
||||
});
|
||||
|
||||
function getTrack(length = 2) {
|
||||
const track = new uTrack(1, 'test track', new uUser(1, 'testUser'));
|
||||
track.positions = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
track.positions.push(getPosition());
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
function getPosition(latitude = 52.23, longitude = 21.01) {
|
||||
const position = new uPosition();
|
||||
position.latitude = latitude;
|
||||
position.longitude = longitude;
|
||||
return position;
|
||||
}
|
||||
});
|
570
js/test/api_openlayers.test.js
Normal file
570
js/test/api_openlayers.test.js
Normal file
@ -0,0 +1,570 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
import OpenlayersApi from '../src/mapapi/api_openlayers.js';
|
||||
import { config } from '../src/initializer.js'
|
||||
import uPosition from '../src/position.js';
|
||||
import uTrack from '../src/track.js';
|
||||
import uUser from '../src/user.js';
|
||||
import uUtils from '../src/utils.js';
|
||||
|
||||
describe('Openlayers map API tests', () => {
|
||||
let container;
|
||||
let mockViewModel;
|
||||
let api;
|
||||
let mockMap;
|
||||
|
||||
beforeEach(() => {
|
||||
config.initialize();
|
||||
document.body.innerHTML = '';
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
mockViewModel = { mapElement: container, model: {} };
|
||||
api = new OpenlayersApi(mockViewModel, ol);
|
||||
mockMap = new ol.Map({ target: container });
|
||||
});
|
||||
|
||||
it('should load and initialize api scripts', (done) => {
|
||||
// given
|
||||
spyOn(uUtils, 'addCss');
|
||||
spyOn(api, 'initMap');
|
||||
spyOn(api, 'initLayers');
|
||||
spyOn(api, 'initStyles');
|
||||
spyOn(api, 'initPopups');
|
||||
// when
|
||||
api.init()
|
||||
.then(() => {
|
||||
// then
|
||||
expect(uUtils.addCss).toHaveBeenCalledTimes(1);
|
||||
expect(api.initMap).toHaveBeenCalledTimes(1);
|
||||
expect(api.initLayers).toHaveBeenCalledTimes(1);
|
||||
expect(api.initStyles).toHaveBeenCalledTimes(1);
|
||||
expect(api.initPopups).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
})
|
||||
.catch((e) => done.fail(e));
|
||||
});
|
||||
|
||||
it('should initialize map engine with config values', () => {
|
||||
// given
|
||||
spyOn(ol.Map.prototype, 'on');
|
||||
// when
|
||||
api.initMap();
|
||||
// then
|
||||
expect(api.map).toEqual(jasmine.any(ol.Map));
|
||||
expect(api.map.getTarget()).toBe(container);
|
||||
expect(api.map.getControls().getLength()).toBe(3);
|
||||
expect(api.map.getView().getCenter()).toEqual(ol.proj.fromLonLat([ config.initLongitude, config.initLatitude ]));
|
||||
expect(ol.Map.prototype.on).toHaveBeenCalledWith('pointermove', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should initialize map layers with config values', () => {
|
||||
// given
|
||||
spyOn(api, 'initLayerSwitcher');
|
||||
config.olLayers = {
|
||||
'layer1': 'http://layer1',
|
||||
'layer2': 'http://layer2'
|
||||
};
|
||||
api.map = mockMap;
|
||||
// when
|
||||
api.initLayers();
|
||||
// then
|
||||
expect(api.map.getLayers().getLength()).toEqual(5);
|
||||
api.map.getLayers().forEach((_layer) => {
|
||||
const name = _layer.get('name');
|
||||
switch (name) {
|
||||
case 'OpenStreetMap':
|
||||
|
||||
expect(_layer).toEqual(jasmine.any(ol.layer.TileLayer));
|
||||
expect(_layer.getVisible()).toBe(true);
|
||||
expect(_layer.getSource()).toEqual(jasmine.any(ol.source.OSM));
|
||||
expect(_layer.get('type')).not.toBe('data');
|
||||
expect(api.selectedLayer).toEqual(_layer);
|
||||
break;
|
||||
case 'layer1':
|
||||
case 'layer2':
|
||||
|
||||
expect(_layer.getVisible()).toBe(false);
|
||||
expect(_layer).toEqual(jasmine.any(ol.layer.TileLayer));
|
||||
expect(_layer.getSource()).toEqual(jasmine.any(ol.source.XYZ));
|
||||
expect(_layer.get('type')).not.toBe('data');
|
||||
break;
|
||||
case 'Track':
|
||||
|
||||
expect(_layer.getVisible()).toBe(true);
|
||||
expect(_layer).toEqual(jasmine.any(ol.layer.VectorLayer));
|
||||
expect(_layer.getSource()).toEqual(jasmine.any(ol.source.Vector));
|
||||
expect(_layer.getStyle().getStroke().getColor()).toBe(uUtils.hexToRGBA(config.strokeColor, config.strokeOpacity));
|
||||
expect(_layer.getStyle().getStroke().getWidth()).toBe(config.strokeWeight);
|
||||
expect(_layer.get('type')).toBe('data');
|
||||
expect(api.layerTrack).toEqual(_layer);
|
||||
break;
|
||||
case 'Markers':
|
||||
|
||||
expect(_layer.getVisible()).toBe(true);
|
||||
expect(_layer).toEqual(jasmine.any(ol.layer.VectorLayer));
|
||||
expect(_layer.getSource()).toEqual(jasmine.any(ol.source.Vector));
|
||||
expect(_layer.get('type')).toBe('data');
|
||||
expect(api.layerMarkers).toEqual(_layer);
|
||||
break;
|
||||
default:
|
||||
fail(`Unexpected layer: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
expect(api.initLayerSwitcher).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should initialize layer switcher element and control', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
api.map.addLayer(new ol.layer.TileLayer());
|
||||
const controlsCount = api.map.getControls().getLength();
|
||||
// when
|
||||
api.initLayerSwitcher();
|
||||
const switcher = document.getElementById('switcher');
|
||||
// then
|
||||
expect(switcher).toEqual(jasmine.any(HTMLDivElement));
|
||||
expect(switcher.firstChild.childNodes.length).toEqual(1);
|
||||
expect(api.map.getControls().getLength()).toBe(controlsCount + 1)
|
||||
});
|
||||
|
||||
it('should clean up and initialize styles', () => {
|
||||
// given
|
||||
api.markerStyles = {
|
||||
'style1': 'to be cleaned up'
|
||||
};
|
||||
// when
|
||||
api.initStyles();
|
||||
// then
|
||||
expect(Object.keys(api.markerStyles).length).toBe(7);
|
||||
});
|
||||
|
||||
it('should initialize popups', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
// when
|
||||
api.initPopups();
|
||||
const popup = document.getElementById('popup-container');
|
||||
// then
|
||||
expect(popup).toEqual(jasmine.any(HTMLDivElement));
|
||||
expect(api.popup).toEqual(jasmine.any(ol.Overlay));
|
||||
expect(api.popup.getElement()).toBe(popup);
|
||||
});
|
||||
|
||||
it('should switch layers', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
const layer1 = new ol.layer.TileLayer({ name: 'layer1', visible: true });
|
||||
const layer2 = new ol.layer.TileLayer({ name: 'layer2', visible: false });
|
||||
api.selectedLayer = layer1;
|
||||
api.map.addLayer(layer1);
|
||||
api.map.addLayer(layer2);
|
||||
// when
|
||||
api.switchLayer('layer2');
|
||||
// then
|
||||
expect(api.selectedLayer).toEqual(layer2);
|
||||
expect(layer1.getVisible()).toBe(false);
|
||||
expect(layer2.getVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle data layer visibility', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
const layer1 = new ol.layer.TileLayer({ name: 'layer1', visible: true, type: 'data' });
|
||||
const layer2 = new ol.layer.TileLayer({ name: 'layer2', visible: false, type: 'data' });
|
||||
api.selectedLayer = layer1;
|
||||
api.map.addLayer(layer1);
|
||||
api.map.addLayer(layer2);
|
||||
// when
|
||||
api.switchLayer('layer1');
|
||||
// then
|
||||
expect(api.selectedLayer).toEqual(layer1);
|
||||
expect(layer1.getVisible()).toBe(false);
|
||||
expect(layer2.getVisible()).toBe(false);
|
||||
// when
|
||||
api.switchLayer('layer2');
|
||||
// then
|
||||
expect(api.selectedLayer).toEqual(layer1);
|
||||
expect(layer1.getVisible()).toBe(false);
|
||||
expect(layer2.getVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clean up class fields', () => {
|
||||
// given
|
||||
api.layerTrack = new ol.layer.VectorLayer();
|
||||
api.layerMarkers = new ol.layer.VectorLayer();
|
||||
api.selectedLayer = api.layerTrack;
|
||||
api.markerStyles = { style: 'style' };
|
||||
document.body.appendChild(document.createElement('div'));
|
||||
api.map = mockMap;
|
||||
// when
|
||||
api.cleanup();
|
||||
// then
|
||||
expect(api.layerTrack).toBe(null);
|
||||
expect(api.layerMarkers).toBe(null);
|
||||
expect(api.selectedLayer).toBe(null);
|
||||
expect(api.markerStyles).toBe(null);
|
||||
expect(container.innerHTML).toBe('');
|
||||
expect(api.map).toBe(null);
|
||||
});
|
||||
|
||||
it('should clear marker and track layers features', () => {
|
||||
// given
|
||||
api.layerTrack = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerTrack.getSource().addFeature(new ol.Feature());
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerMarkers.getSource().addFeature(new ol.Feature());
|
||||
|
||||
expect(api.layerTrack.getSource().getFeatures().length).toBe(1);
|
||||
expect(api.layerMarkers.getSource().getFeatures().length).toBe(1);
|
||||
// when
|
||||
api.clearMap();
|
||||
// then
|
||||
expect(api.layerTrack.getSource().getFeatures().length).toBe(0);
|
||||
expect(api.layerMarkers.getSource().getFeatures().length).toBe(0);
|
||||
});
|
||||
|
||||
it('should construct track markers with track layer', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
api.layerTrack = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
const track = getTrack();
|
||||
spyOn(api, 'setMarker');
|
||||
spyOn(api, 'fitToExtent');
|
||||
// when
|
||||
api.displayTrack(track, false);
|
||||
let zoomControl;
|
||||
api.map.getControls().forEach((el) => {
|
||||
if (el instanceof ol.control.ZoomToExtent) {
|
||||
zoomControl = el;
|
||||
}
|
||||
});
|
||||
// then
|
||||
expect(api.layerTrack.getSource().getFeatures().length).toBe(1);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(0, track);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(1, track);
|
||||
expect(api.fitToExtent).not.toHaveBeenCalled();
|
||||
// noinspection JSUnusedAssignment
|
||||
expect(zoomControl.extent).toEqual(api.layerMarkers.getSource().getExtent());
|
||||
expect(api.layerTrack.getSource().getFeatures()[0].getGeometry().getCoordinates().length).toEqual(track.positions.length);
|
||||
});
|
||||
|
||||
it('should construct non-continuous track markers without track layer', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
api.map.addControl(new ol.control.ZoomToExtent());
|
||||
api.layerTrack = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
const track = getTrack();
|
||||
track.continuous = false;
|
||||
spyOn(api, 'setMarker');
|
||||
spyOn(api, 'fitToExtent');
|
||||
// when
|
||||
api.displayTrack(track, false);
|
||||
let zoomControl;
|
||||
api.map.getControls().forEach((el) => {
|
||||
if (el instanceof ol.control.ZoomToExtent) {
|
||||
zoomControl = el;
|
||||
}
|
||||
});
|
||||
// then
|
||||
expect(api.layerTrack.getSource().getFeatures().length).toBe(0);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(0, track);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(1, track);
|
||||
expect(api.fitToExtent).not.toHaveBeenCalled();
|
||||
// noinspection JSUnusedAssignment
|
||||
expect(zoomControl.extent).toEqual(api.layerMarkers.getSource().getExtent());
|
||||
});
|
||||
|
||||
it('should fit to extent if update without zoom', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
api.layerTrack = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
const track = getTrack();
|
||||
spyOn(api, 'setMarker');
|
||||
const markersExtent = [ 3, 2, 1, 0 ];
|
||||
spyOn(api, 'fitToExtent').and.callFake((_extent) => _extent);
|
||||
spyOn(ol.source.Vector.prototype, 'getExtent').and.returnValue(markersExtent);
|
||||
// when
|
||||
api.displayTrack(track, true);
|
||||
let zoomControl;
|
||||
api.map.getControls().forEach((el) => {
|
||||
if (el instanceof ol.control.ZoomToExtent) {
|
||||
zoomControl = el;
|
||||
}
|
||||
});
|
||||
// then
|
||||
expect(api.layerTrack.getSource().getFeatures().length).toBe(1);
|
||||
expect(api.setMarker).toHaveBeenCalledTimes(track.positions.length);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(0, track);
|
||||
expect(api.setMarker).toHaveBeenCalledWith(1, track);
|
||||
expect(api.fitToExtent).toHaveBeenCalledWith(markersExtent);
|
||||
// noinspection JSUnusedAssignment
|
||||
expect(zoomControl.extent).toEqual(markersExtent);
|
||||
});
|
||||
|
||||
it('should fit to extent', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
spyOn(ol.View.prototype, 'fit');
|
||||
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenlayersApi.ZOOM_MAX - 1);
|
||||
const extent = [ 0, 1, 2, 3 ];
|
||||
// when
|
||||
const result = api.fitToExtent(extent);
|
||||
// then
|
||||
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent, jasmine.any(Object));
|
||||
expect(result).toEqual(extent);
|
||||
});
|
||||
|
||||
it('should fit to extent and zoom to max value', () => {
|
||||
// given
|
||||
const extent = [ 0, 1, 2, 3 ];
|
||||
const zoomedExtent = [ 3, 2, 1, 0 ];
|
||||
api.map = mockMap;
|
||||
spyOn(ol.View.prototype, 'fit');
|
||||
spyOn(ol.View.prototype, 'getZoom').and.returnValue(OpenlayersApi.ZOOM_MAX + 1);
|
||||
spyOn(ol.View.prototype, 'setZoom');
|
||||
spyOn(ol.View.prototype, 'calculateExtent').and.returnValue(zoomedExtent);
|
||||
// when
|
||||
const result = api.fitToExtent(extent);
|
||||
// then
|
||||
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent, jasmine.any(Object));
|
||||
expect(ol.View.prototype.setZoom).toHaveBeenCalledWith(OpenlayersApi.ZOOM_MAX);
|
||||
expect(result).toEqual(zoomedExtent);
|
||||
});
|
||||
|
||||
it('should create marker from track position and add it to markers layer', () => {
|
||||
// given
|
||||
const track = getTrack(1);
|
||||
track.positions[0].timestamp = 1;
|
||||
const id = 0;
|
||||
api.map = mockMap;
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
spyOn(api, 'getMarkerStyle');
|
||||
// when
|
||||
api.setMarker(id, track);
|
||||
const marker = api.layerMarkers.getSource().getFeatures()[0];
|
||||
// then
|
||||
expect(marker.getId()).toBe(id);
|
||||
expect(marker.getGeometry().getFirstCoordinate()).toEqual(ol.proj.fromLonLat([ track.positions[0].longitude, track.positions[0].latitude ]));
|
||||
});
|
||||
|
||||
it('should get different marker style for start, end and normal position', () => {
|
||||
// given
|
||||
const track = getTrack(3);
|
||||
api.markerStyles = {
|
||||
normal: 'normal',
|
||||
stop: 'stop',
|
||||
start: 'start'
|
||||
};
|
||||
// when
|
||||
let style = api.getMarkerStyle(0, track);
|
||||
// then
|
||||
expect(style).toBe('start');
|
||||
// when
|
||||
style = api.getMarkerStyle(1, track);
|
||||
// then
|
||||
expect(style).toBe('normal');
|
||||
// when
|
||||
style = api.getMarkerStyle(2, track);
|
||||
// then
|
||||
expect(style).toBe('stop');
|
||||
});
|
||||
|
||||
it('should create different marker for position with comment', () => {
|
||||
// given
|
||||
const track = getTrack(3);
|
||||
track.positions[0].comment = 'comment';
|
||||
track.positions[1].comment = 'comment';
|
||||
track.positions[2].comment = 'comment';
|
||||
api.markerStyles = {
|
||||
extra: 'extra',
|
||||
stopExtra: 'stopExtra',
|
||||
startExtra: 'startExtra'
|
||||
};
|
||||
// when
|
||||
let style = api.getMarkerStyle(0, track);
|
||||
// then
|
||||
expect(style).toBe('startExtra');
|
||||
// when
|
||||
style = api.getMarkerStyle(1, track);
|
||||
// then
|
||||
expect(style).toBe('extra');
|
||||
// when
|
||||
style = api.getMarkerStyle(2, track);
|
||||
// then
|
||||
expect(style).toBe('stopExtra');
|
||||
});
|
||||
|
||||
it('should create different marker for position with image', () => {
|
||||
// given
|
||||
const track = getTrack(3);
|
||||
track.positions[0].image = 'image';
|
||||
track.positions[1].image = 'image';
|
||||
track.positions[2].image = 'image';
|
||||
api.markerStyles = {
|
||||
extra: 'extra',
|
||||
stopExtra: 'stopExtra',
|
||||
startExtra: 'startExtra'
|
||||
};
|
||||
// when
|
||||
let style = api.getMarkerStyle(0, track);
|
||||
// then
|
||||
expect(style).toBe('startExtra');
|
||||
// when
|
||||
style = api.getMarkerStyle(1, track);
|
||||
// then
|
||||
expect(style).toBe('extra');
|
||||
// when
|
||||
style = api.getMarkerStyle(2, track);
|
||||
// then
|
||||
expect(style).toBe('stopExtra');
|
||||
});
|
||||
|
||||
it('should animate marker with given index', (done) => {
|
||||
// given
|
||||
const styleOriginal = new ol.style.Style();
|
||||
const styleAnimated = new ol.style.Style();
|
||||
api.markerStyles = {
|
||||
hilite: styleAnimated
|
||||
};
|
||||
const id = 1;
|
||||
const marker = new ol.Feature();
|
||||
marker.setStyle(styleOriginal);
|
||||
marker.setId(id);
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
api.layerMarkers.getSource().addFeature(marker);
|
||||
// when
|
||||
api.animateMarker(1);
|
||||
// then
|
||||
expect(marker.getStyle()).toBe(styleAnimated);
|
||||
setTimeout(() => {
|
||||
expect(marker.getStyle()).toBe(styleOriginal);
|
||||
done();
|
||||
}, 2100);
|
||||
});
|
||||
|
||||
it('should call View.fit with markers layer extent', () => {
|
||||
// given
|
||||
const extent = [ 0, 1, 2, 3 ];
|
||||
api.map = mockMap;
|
||||
api.layerMarkers = new ol.layer.VectorLayer({ source: new ol.source.Vector() });
|
||||
spyOn(ol.View.prototype, 'fit');
|
||||
spyOn(ol.source.Vector.prototype, 'getExtent').and.returnValue(extent);
|
||||
// when
|
||||
api.zoomToExtent();
|
||||
// then
|
||||
expect(ol.View.prototype.fit).toHaveBeenCalledWith(extent);
|
||||
});
|
||||
|
||||
it('should get map bounds and convert to WGS84 (EPSG:4326)', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
const extent = [ 2292957.24947, 6828285.71702, 2378184.536, 6861382.95027 ];
|
||||
spyOn(ol.View.prototype, 'calculateExtent').and.returnValue(extent);
|
||||
// when
|
||||
const bounds = api.getBounds();
|
||||
// then
|
||||
expect(ol.View.prototype.calculateExtent).toHaveBeenCalledWith(jasmine.any(Array));
|
||||
expect(bounds[0]).toBeCloseTo(20.597985430276808);
|
||||
expect(bounds[1]).toBeCloseTo(52.15547181298076);
|
||||
expect(bounds[2]).toBeCloseTo(21.363595171488573);
|
||||
expect(bounds[3]).toBeCloseTo(52.33750879522563);
|
||||
});
|
||||
|
||||
it('should convert bounds to EPSG:3857 and fit view', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
const bounds = [ 20.597985430276808, 52.15547181298076, 21.363595171488573, 52.33750879522563 ];
|
||||
spyOn(ol.View.prototype, 'fit');
|
||||
// when
|
||||
api.zoomToBounds(bounds);
|
||||
// then
|
||||
const extent = ol.View.prototype.fit.calls.mostRecent().args[0];
|
||||
|
||||
expect(extent[0]).toBeCloseTo(2292957.24947);
|
||||
expect(extent[1]).toBeCloseTo(6828285.71702);
|
||||
expect(extent[2]).toBeCloseTo(2378184.536);
|
||||
expect(extent[3]).toBeCloseTo(6861382.95027);
|
||||
});
|
||||
|
||||
it('should update map size', () => {
|
||||
// given
|
||||
api.map = mockMap;
|
||||
spyOn(ol.Map.prototype, 'updateSize');
|
||||
// when
|
||||
api.updateSize();
|
||||
// then
|
||||
expect(ol.Map.prototype.updateSize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should open popup at coordinate and close it', () => {
|
||||
// given
|
||||
const id = 1;
|
||||
const coordinate = [ 1, 2 ];
|
||||
mockViewModel.getPopupHtml = (i) => `content ${i}`;
|
||||
mockViewModel.model.markerSelect = null;
|
||||
spyOn(mockViewModel, 'getPopupHtml').and.callThrough();
|
||||
api.map = mockMap;
|
||||
const popupContainer = document.createElement('div');
|
||||
const popupContent = document.createElement('div');
|
||||
popupContainer.appendChild(popupContent);
|
||||
api.popup = new ol.Overlay({ element: popupContainer });
|
||||
api.map.addOverlay(api.popup);
|
||||
// when
|
||||
api.popupOpen(id, coordinate);
|
||||
// then
|
||||
expect(api.popup.getPosition()).toEqual(coordinate);
|
||||
expect(api.popup.getElement().firstElementChild.innerHTML).toBe(`content ${id}`);
|
||||
expect(mockViewModel.model.markerSelect).toBe(id);
|
||||
// when
|
||||
api.popupClose();
|
||||
// then
|
||||
// eslint-disable-next-line no-undefined
|
||||
expect(api.popup.getPosition()).toBe(undefined);
|
||||
expect(api.popup.getElement().firstElementChild.innerHTML).toBe('content 1');
|
||||
expect(mockViewModel.model.markerSelect).toBe(null);
|
||||
});
|
||||
|
||||
function getTrack(length = 2) {
|
||||
const track = new uTrack(1, 'test track', new uUser(1, 'testUser'));
|
||||
track.positions = [];
|
||||
let lat = 21.01;
|
||||
let lon = 52.23;
|
||||
for (let i = 0; i < length; i++) {
|
||||
track.positions.push(getPosition(lat, lon));
|
||||
lat += 0.5;
|
||||
lon += 0.5;
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
function getPosition(latitude = 52.23, longitude = 21.01) {
|
||||
const position = new uPosition();
|
||||
position.latitude = latitude;
|
||||
position.longitude = longitude;
|
||||
return position;
|
||||
}
|
||||
});
|
99
js/test/googlemaps.stub.js
Normal file
99
js/test/googlemaps.stub.js
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* μ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/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable func-style */
|
||||
const stubObj = {};
|
||||
const stubFn = function() {/* ignore */};
|
||||
const stubFnObj = function() { return stubObj; };
|
||||
|
||||
export const setupGmapsStub = () => {
|
||||
// noinspection JSUnresolvedVariable
|
||||
window.google = {
|
||||
maps: {
|
||||
Animation: {
|
||||
BOUNCE: 1,
|
||||
DROP: 2
|
||||
},
|
||||
event: {
|
||||
addListener: stubFn,
|
||||
addListenerOnce: stubFn,
|
||||
removeListener: stubFn
|
||||
},
|
||||
Icon: stubFn,
|
||||
InfoWindow: stubFn,
|
||||
LatLng: function(lat, lng) {
|
||||
this.latitude = parseFloat(lat);
|
||||
this.longitude = parseFloat(lng);
|
||||
},
|
||||
LatLngBounds: function(sw, ne) {
|
||||
this.sw = sw;
|
||||
this.ne = ne;
|
||||
},
|
||||
Map: stubFn,
|
||||
MapTypeId: {
|
||||
HYBRID: 1,
|
||||
ROADMAP: 2,
|
||||
SATELLITE: 3,
|
||||
TERRAIN: 4
|
||||
},
|
||||
Marker: stubFn,
|
||||
Point: stubFnObj,
|
||||
Polyline: function(opts) {
|
||||
this.options = opts;
|
||||
this.path = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
applyPrototypes(stubFn, stubObj);
|
||||
};
|
||||
|
||||
export const applyPrototypes = () => {
|
||||
window.google.maps.InfoWindow.prototype.addListener = stubFn;
|
||||
window.google.maps.InfoWindow.prototype.close = stubFn;
|
||||
window.google.maps.InfoWindow.prototype.getMap = stubFn;
|
||||
window.google.maps.InfoWindow.prototype.open = stubFn;
|
||||
window.google.maps.InfoWindow.prototype.setContent = stubFn;
|
||||
window.google.maps.LatLng.prototype.lat = function () { return this.latitude; };
|
||||
window.google.maps.LatLng.prototype.lng = function () { return this.longitude; };
|
||||
window.google.maps.LatLngBounds.prototype.extend = stubFn;
|
||||
window.google.maps.LatLngBounds.prototype.getNorthEast = function () { return this.ne; };
|
||||
window.google.maps.LatLngBounds.prototype.getSouthWest = function () { return this.sw; };
|
||||
window.google.maps.Map.prototype.fitBounds = stubFn;
|
||||
window.google.maps.Map.prototype.getBounds = stubFn;
|
||||
window.google.maps.Map.prototype.getCenter = stubFn;
|
||||
window.google.maps.Map.prototype.getDiv = stubFn;
|
||||
window.google.maps.Map.prototype.getZoom = stubFn;
|
||||
window.google.maps.Map.prototype.setCenter = stubFn;
|
||||
window.google.maps.Map.prototype.setMapTypeId = stubFn;
|
||||
window.google.maps.Map.prototype.setOptions = stubFn;
|
||||
window.google.maps.Map.prototype.setZoom = stubFn;
|
||||
window.google.maps.Marker.prototype.addListener = stubFn;
|
||||
window.google.maps.Marker.prototype.getIcon = stubFn;
|
||||
window.google.maps.Marker.prototype.getPosition = stubFn;
|
||||
window.google.maps.Marker.prototype.setAnimation = stubFn;
|
||||
window.google.maps.Marker.prototype.setIcon = stubFn;
|
||||
window.google.maps.Marker.prototype.setMap = stubFn;
|
||||
window.google.maps.Polyline.prototype.getPath = function () { return this.path; };
|
||||
window.google.maps.Polyline.prototype.setMap = stubFn;
|
||||
};
|
||||
|
||||
export const clear = () => {
|
||||
// noinspection JSAnnotator,JSUnresolvedVariable
|
||||
delete window.google;
|
||||
};
|
7
js/test/openlayers.bundle.js
Normal file
7
js/test/openlayers.bundle.js
Normal file
File diff suppressed because one or more lines are too long
@ -7,7 +7,7 @@ const preprocessors = {};
|
||||
const reporters = [ 'progress' ];
|
||||
// don't preprocess files on debug run
|
||||
if (!process.env._INTELLIJ_KARMA_INTERNAL_PARAMETER_debug && !process.argv.includes('--debug')) {
|
||||
preprocessors['src/*.js'] = 'karma-coverage-istanbul-instrumenter';
|
||||
preprocessors['src/**/*.js'] = 'karma-coverage-istanbul-instrumenter';
|
||||
reporters.push('coverage-istanbul');
|
||||
}
|
||||
|
||||
@ -19,7 +19,9 @@ module.exports = function(config) {
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
{ pattern: 'test/*.test.js', type: 'module' },
|
||||
{ pattern: 'src/*.js', type: 'module', included: false }
|
||||
{ pattern: 'test/*.stub.js', type: 'module', included: false },
|
||||
{ pattern: 'src/**/*.js', type: 'module', included: false },
|
||||
{ pattern: 'test/openlayers.bundle.js', type: 'js', included: true }
|
||||
],
|
||||
// list of files / patterns to exclude
|
||||
exclude: [],
|
||||
|
6026
package-lock.json
generated
6026
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@ -2,11 +2,11 @@
|
||||
"name": "ulogger-server",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a web application for real-time collection of geolocation data, tracks viewing and management. Together with a dedicated [μlogger mobile client](https://github.com/bfabiszewski/ulogger-android) it may be used as a complete self hosted server–client solution for logging and monitoring users' geolocation.",
|
||||
"main": "js/ulogger.js",
|
||||
"main": "js/src/ulogger.js",
|
||||
"scripts": {
|
||||
"test": "karma start karma.conf.js",
|
||||
"start": "parcel index.html",
|
||||
"build": "parcel build --public-url . js/ulogger.js"
|
||||
"start": "webpack-dev-server --open --config webpack.dev.js",
|
||||
"build": "webpack --config webpack.prod.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -15,30 +15,34 @@
|
||||
"author": "Bartek Fabiszewski",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.0",
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/cli": "^7.7.4",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-minify-webpack-plugin": "^0.3.1",
|
||||
"browserlist": "^1.0.1",
|
||||
"core-js": "^3.4.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-jasmine": "^4.0.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"core-js": "^3.4.2",
|
||||
"eslint": "^6.7.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jasmine": "^4.1.0",
|
||||
"jasmine": "^3.5.0",
|
||||
"karma": "^4.4.1",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-coverage-istanbul-instrumenter": "^1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^2.1.0",
|
||||
"karma-coverage-istanbul-reporter": "^2.1.1",
|
||||
"karma-jasmine": "^2.0.1",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"puppeteer": "^2.0.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.10"
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ol": "^5.3.3"
|
||||
"@babel/runtime": "^7.7.4",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"ol": "^6.1.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
|
22
webpack.common.js
Normal file
22
webpack.common.js
Normal file
@ -0,0 +1,22 @@
|
||||
const path = require('path');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './js/src/ulogger.js',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'js/dist'),
|
||||
publicPath: 'js/dist/'
|
||||
},
|
||||
plugins: [ new CleanWebpackPlugin() ],
|
||||
optimization: {
|
||||
splitChunks: {
|
||||
cacheGroups: {
|
||||
vendors: {
|
||||
test: /[\\/]node_modules[\\/]|[\\/]ol.js/,
|
||||
name: 'ol'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
7
webpack.dev.js
Normal file
7
webpack.dev.js
Normal file
@ -0,0 +1,7 @@
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-source-map',
|
||||
});
|
19
webpack.prod.js
Normal file
19
webpack.prod.js
Normal file
@ -0,0 +1,19 @@
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [ '@babel/preset-env' ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user