Source: component/osmap.js

/*! meta-admin/modules/osmap */
/*jslint
    browser, long
*/
/*global
    L
*/
import {} from "leaflet";
import {} from "leaflet.markercluster";
import Logger from "js-logger";
import ko from "knockout";
import model from "meta-core/modules/model";
import _ from "underscore";
import ext from "util-web/modules/ext";
import i18next from "util-web/modules/i18next";
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";

/**
 * osmap component.
 *
 * @module meta-admin/modules/component/osmap
 */
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);
        }
    }
});