/*! meta-admin/modules/osmap */
/*jslint
browser, long
*/
/*global
L
*/
/**
* osmap component.
*
* @module meta-admin/modules/component/osmap
*/
import _ from "underscore";
import {} from "leaflet";
import {} from "leaflet.markercluster";
import ext from "util-web/modules/ext";
import i18next from "util-web/modules/i18next";
import ko from "knockout";
import Logger from "js-logger";
import model from "meta-core/modules/model";
import template from "./osmap.html";
const LOG = Logger.get("meta-admin/osmap");
const TILE_LAYER = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "<a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a>"
});
const POSITION_ICON = L.icon({
iconSize: [64, 64],
iconUrl: "static/position.png",
popupAnchor: [0, -32]
});
const ALERT_ICON = L.icon({
iconSize: [64, 64],
iconUrl: "static/alert.png",
popupAnchor: [0, -32]
});
const WARN_ICON = L.icon({
iconSize: [64, 64],
iconUrl: "static/warn.png",
popupAnchor: [0, -32]
});
const MARKER_BLUE_ICON = L.icon({
iconAnchor: [32, 64],
iconSize: [64, 64],
iconUrl: "static/marker_blue.png",
popupAnchor: [0, -64]
});
const MARKER_PINK_ICON = L.icon({
iconAnchor: [32, 64],
iconSize: [64, 64],
iconUrl: "static/marker_pink.png",
popupAnchor: [0, -64]
});
const ICONS = Object.freeze({
ALERT: ALERT_ICON,
BLUE: MARKER_BLUE_ICON,
PINK: MARKER_PINK_ICON,
WARN: WARN_ICON
});
const DEFAULT_OBJECT_ICON = "BLUE";
export default Object.freeze({
template,
viewModel: {
createViewModel: function ({viewmodel}) {
const vm = {};
const objectMarkersById = new Map();
const objectMarkers = L.markerClusterGroup();
const currentPositionMarker = L.marker([0, 0], {
icon: POSITION_ICON
}).bindPopup(i18next.t("map_position"));
const onPositionChange = function (position) {
const currentPostion = currentPositionMarker.getLatLng();
const newPosition = L.latLng(position.coords.latitude, position.coords.longitude);
if (currentPostion.equals(newPosition)) {
return;
}
LOG.debug("onPositionChange", newPosition);
currentPositionMarker.setLatLng(newPosition);
};
const bindPopup = function (feature, name) {
feature.bindPopup(name);
feature.on("mouseover", function () {
feature.openPopup();
});
feature.on("mouseout", function () {
feature.closePopup();
});
};
const createMarker = function (id, name, position, optionalIconId, optionalIcon) {
const options = {id, name};
if (optionalIconId) {
options.iconId = optionalIconId;
options.icon = ICONS[optionalIconId];
}
if (optionalIcon) {
options.icon = optionalIcon;
}
const marker = L.marker(position, options);
bindPopup(marker, name);
return marker;
};
const createPolygon = function (id, name, positions) {
const polygon = L.polygon(positions, {id, name});
bindPopup(polygon, name);
return polygon;
};
const createPolyline = function (id, name, positions) {
const polyline = L.polyline(positions, {id, name});
bindPopup(polyline, name);
return polyline;
};
const createRectangle = function (id, name, positions) {
const rectangle = L.rectangle(positions, {id, name});
bindPopup(rectangle, name);
return rectangle;
};
const newObjectMarker = function (id, name, position, iconId) {
const objectMarker = createMarker(id, name, position, iconId);
objectMarker.on("click", function (event) {
const objectId = _.get(event, ["target", "options", "id"]);
LOG.debug("objectMarker click", event.target.options);
if (_.isEmpty(objectId)) {
return;
}
vm.editedObjectMarker(objectId);
});
return objectMarker;
};
const pickPositionPromise = ko.observable();
let geolocationWatchId;
let map;
L.Icon.Default.imagePath = viewmodel.config.map.markerPath;
vm.isActive = ko.observable(false);
vm.activateGeolocation = function () {
if (geolocationWatchId) {
return;
}
return ext.requestPermission({
name: "geolocation",
onGranted: function () {
LOG.debug("activateGeolocation granted");
geolocationWatchId = navigator.geolocation.watchPosition(onPositionChange);
},
onRequested: function () {
LOG.debug("activateGeolocation requested");
return navigator.geolocation.getCurrentPosition(onPositionChange);
}
});
};
vm.layersControl = L.control.layers({}, {}, {position: "bottomleft"});
vm.objectMarkers = ko.observableArray();
vm.objectMarkersComputed = ko.computed(function () {
const objects = vm.objectMarkers();
const objectIds = [];
const expiredMarkers = [];
const newMarkers = [];
const removedMarkers = [];
LOG.debug(`objectMarkersComputed, objectsSize=${objects.length}`);
objects.forEach(function (metaObject) {
const metaId = metaObject.id;
LOG.debug(`objectMarkersComputed, metaId=${metaId}`);
const existingMarker = objectMarkersById.get(metaId);
const position = ko.unwrap(metaObject.position);
const isValidPosition = (
position
? model.isValidPosition(position)
: false
);
const iconId = ko.unwrap(metaObject.icon) || DEFAULT_OBJECT_ICON;
if (existingMarker) {
if (isValidPosition) {
existingMarker.setLatLng(position);
LOG.debug(`objectMarkersComputed, ${existingMarker.options.iconId} transition to ${iconId}`);
if (existingMarker.options.iconId !== iconId) {
existingMarker.options.iconId = iconId;
existingMarker.setIcon(ICONS[iconId]);
}
} else {
objectMarkersById.delete(metaId);
removedMarkers.push(existingMarker);
}
} else if (isValidPosition) {
const objectMarker = newObjectMarker(
metaId,
ko.unwrap(metaObject.name) || metaId,
position,
iconId
);
objectMarkersById.set(metaId, objectMarker);
newMarkers.push(objectMarker);
}
objectIds.push(metaId);
});
objectMarkersById.forEach(function (ignore, metaId) {
if (objectIds.includes(metaId) === false) {
expiredMarkers.push(metaId);
}
});
LOG.debug(`objectMarkersComputed, expiredMarkersSize=${expiredMarkers.length}`);
expiredMarkers.forEach(function (metaId) {
removedMarkers.push(objectMarkersById.get(metaId));
objectMarkersById.delete(metaId);
});
LOG.debug(`objectMarkersComputed, newMarkersSize=${newMarkers.length}`);
objectMarkers.addLayers(newMarkers);
LOG.debug(`objectMarkersComputed, removedMarkersSize=${removedMarkers.length}`);
objectMarkers.removeLayers(removedMarkers);
}).extend({throttle: 500});
vm.activeObjectMarker = ko.observable();
vm.editedObjectMarker = ko.observable();
vm.pickPosition = function () {
return new Promise(function (resolve, reject) {
pickPositionPromise({reject, resolve});
});
};
vm.initializeMap = function () {
return new Promise(function (resolve) {
LOG.debug("initializeMap");
map = L.map("map", {
center: viewmodel.config.map.center,
layers: [TILE_LAYER, objectMarkers, currentPositionMarker],
maxZoom: 18,
zoom: 8,
zoomControl: false
});
L.control.scale().addTo(map);
map.on("click", function (event) {
const promise = pickPositionPromise();
if (Boolean(promise) === false) {
return;
}
pickPositionPromise(undefined);
promise.resolve(event.latlng);
});
vm.layersControl.addBaseLayer(TILE_LAYER, i18next.t("map"));
vm.layersControl.addOverlay(objectMarkers, i18next.t("map_markers"));
vm.layersControl.addOverlay(currentPositionMarker, i18next.t("map_position"));
vm.layersControl.addTo(map);
viewmodel.registerMapApiProvider({
ICONS,
activateGeolocation: vm.activateGeolocation,
addOverlay: function (layer, name) {
vm.layersControl.addOverlay(layer, name);
layer.addTo(map);
},
createMarker,
createPolygon,
createPolyline,
createRectangle,
editedObjectMarker: vm.editedObjectMarker,
fitBounds: function (positions) {
map.fitBounds(positions);
},
objectMarkers: vm.objectMarkers,
panTo: function (position) {
map.panTo(position);
},
pickPosition: vm.pickPosition
});
vm.isActive(true);
setTimeout(function () {
map.invalidateSize(true);
}, 500);
resolve();
});
};
vm.initializeMap().then(function () {
LOG.debug("initializeMap done");
});
vm.dispose = function () {
LOG.debug("dispose");
if (geolocationWatchId) {
navigator.geolocation.clearWatch(geolocationWatchId);
}
vm.objectMarkersComputed.dispose();
objectMarkersById.clear();
map.remove();
};
return Object.freeze(vm);
}
}
});