UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

280 lines (251 loc) 9.9 kB
import Map from "./Map"; import { COLORS, QUALITYSCORE_VALUES } from "../../utils/utils"; import { MAP_EXPR_QUALITYSCORE, switchCoefValue, MAP_THEMES, mapFiltersToLayersFilters } from "../../utils/map"; export const MAP_FILTERS = [ "minDate", "maxDate", "pic_type", "camera", "theme", "qualityscore"]; /** * MapMore is a more complete version of [Map UI component](#Panoramax.components.ui.Map). * * It offers advanced features like themes, filters and more. * * Note that all functions of [MapLibre GL JS class Map](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/) are also available. * * ⚠️ This class doesn't inherit from [EventTarget](https://developer.mozilla.org/fr/docs/Web/API/EventTarget), so it doesn't have `addEventListener` and `dispatchEvent` functions. * It uses instead [`on`](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#on) and `fire` functions from MapLibre Map class. * `fire` function doesn't take directly [`Event`](https://developer.mozilla.org/fr/docs/Web/API/Event) objects, but a string and object data. * @class Panoramax.components.ui.MapMore * @extends Panoramax.components.ui.Map * @param {Panoramax.components.core.Basic} parent The parent view * @param {Element} container The DOM element to create into * @param {object} [options] The map options (any of [MapLibre GL settings](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapOptions/) or any supplementary option defined here) * @param {object} [options.raster] The MapLibre raster source for aerial background. This must be a JSON object following [MapLibre raster source definition](https://maplibre.org/maplibre-style-spec/sources/#raster). * @param {string} [options.background=streets] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to streets. * @param {string} [options.theme=default] The map theme (default, age, score, type) * @fires Panoramax.components.ui.Map#background-changed * @fires Panoramax.components.ui.Map#users-changed * @fires Panoramax.components.ui.Map#sequence-hover * @fires Panoramax.components.ui.Map#sequence-click * @fires Panoramax.components.ui.Map#picture-click * @fires Panoramax.components.ui.MapMore#filters-changed * @example * const map = new Panoramax.components.ui.MapMore(viewer, mapNode, {center: {lat: 48.7, lng: -1.7}}); */ export default class MapMore extends Map { constructor(parent, container, options = {}) { super(parent, container, options); // Map theme this._mapFilters = {}; if(this._options.theme) { this._mapFilters = { theme: this._options.theme }; } } reloadLayersStyles() { super.reloadLayersStyles(); // Also handle the grid stats if(this._hasGridStats()) { let newType = "coef"; if(this._mapFilters.pic_type) { newType = this._mapFilters.pic_type == "flat" ? "coef_flat_pictures" : "coef_360_pictures"; } this.getStyle().layers .filter(l => l.id.endsWith("_grid")) .forEach(l => { const newl = switchCoefValue(l, newType); for(let p in newl.layout) { this.setLayoutProperty(l.id, p, newl.layout[p]); } for(let p in newl.paint) { this.setPaintProperty(l.id, p, newl.paint[p]); } }); } } /** * Computes dates to use for map theme by picture/sequence age * @private */ _getDatesForLayerColors() { const oneDay = 24 * 60 * 60 * 1000; const d0 = Date.now(); const d1 = d0 - 30 * oneDay; const d2 = d0 - 365 * oneDay; const d3 = d0 - 2 * 365 * oneDay; return [d1, d2, d3].map(d => new Date(d).toISOString().split("T")[0]); } /** * Retrieve map layer color scheme according to selected theme. * @private */ _getLayerColorStyle(layer) { // Hidden style const s = ["case", ["==", ["get", "hidden"], true], COLORS.HIDDEN ]; // Selected sequence style const picId = this._parent.psv?._myVTour?.state?.loadingNode || this._parent.psv?._myVTour?.state?.currentNode?.id; const seqId = picId ? this._parent.psv?._picturesSequences[picId] : null; if(layer == "sequences" && seqId) { s.push(["==", ["get", "id"], seqId], COLORS.SELECTED); } else if(layer == "pictures" && seqId) { s.push(["in", seqId, ["get", "sequences"]], COLORS.SELECTED); } // Themes styles if(this._mapFilters.theme == MAP_THEMES.AGE) { const prop = layer == "sequences" ? "date" : "ts"; const dt = this._getDatesForLayerColors(); s.push( ["!", ["has", prop]], COLORS.BASE, [">=", ["get", prop], dt[0]], COLORS.PALETTE_4, [">=", ["get", prop], dt[1]], COLORS.PALETTE_3, [">=", ["get", prop], dt[2]], COLORS.PALETTE_2, COLORS.PALETTE_1 ); } else if(this._mapFilters.theme == MAP_THEMES.TYPE) { s.push( ["!", ["has", "type"]], COLORS.BASE, ["==", ["get", "type"], "equirectangular"], COLORS.QUALI_1, COLORS.QUALI_2 ); } else if(this._mapFilters.theme == MAP_THEMES.SCORE) { s.push( ["==", MAP_EXPR_QUALITYSCORE, 5], QUALITYSCORE_VALUES[0].color, ["==", MAP_EXPR_QUALITYSCORE, 4], QUALITYSCORE_VALUES[1].color, ["==", MAP_EXPR_QUALITYSCORE, 3], QUALITYSCORE_VALUES[2].color, ["==", MAP_EXPR_QUALITYSCORE, 2], QUALITYSCORE_VALUES[3].color, QUALITYSCORE_VALUES[4].color, ); } else { s.push(COLORS.BASE); } return s; } /** * Retrieve map sort key according to selected theme. * @private */ _getLayerSortStyle(layer) { // Values // - 100 : on top / selected feature // - 90 : hidden feature // - 20-80 : custom ranges // - 10 : basic feature // - 0 : on bottom / feature with undefined property // Hidden style const s = ["case", ["==", ["get", "hidden"], true], 90 ]; // Selected sequence style const picId = this._parent.psv?._myVTour?.state?.loadingNode || this._parent.psv?._myVTour?.state?.currentNode?.id; const seqId = picId ? this._parent.psv?._picturesSequences[picId] : null; if(layer == "sequences" && seqId) { s.push(["==", ["get", "id"], seqId], 100); } else if(layer == "pictures" && seqId) { s.push(["in", seqId, ["get", "sequences"]], 100); } // Themes styles if(this._mapFilters.theme == MAP_THEMES.AGE) { const prop = layer == "sequences" ? "date" : "ts"; const dt = this._getDatesForLayerColors(); s.push( ["!", ["has", prop]], 0, [">=", ["get", prop], dt[0]], 50, [">=", ["get", prop], dt[1]], 49, [">=", ["get", prop], dt[2]], 48, ); } else if(this._mapFilters.theme == MAP_THEMES.TYPE) { s.push( ["!", ["has", "type"]], 0, ["==", ["get", "type"], "equirectangular"], 50, ); } else if(this._mapFilters.theme == MAP_THEMES.SCORE) { s.push( ["==", MAP_EXPR_QUALITYSCORE, 5], 80, ["==", MAP_EXPR_QUALITYSCORE, 4], 65, ["==", MAP_EXPR_QUALITYSCORE, 3], 50, ["==", MAP_EXPR_QUALITYSCORE, 2], 35, ["==", MAP_EXPR_QUALITYSCORE, 1], 20, ); } s.push(10); return s; } /** * Change the map filters * @param {object} filters Filtering values * @param {string} [filters.minDate] Start date for pictures (format YYYY-MM-DD) * @param {string} [filters.maxDate] End date for pictures (format YYYY-MM-DD) * @param {string} [filters.pic_type] Type of picture to keep (flat, equirectangular) * @param {string} [filters.camera] Camera make and model to keep * @param {string} [filters.theme] Map theme to use * @param {number[]} [filters.qualityscore] QualityScore values, as a list of 1 to 5 grades * @param {boolean} [skipZoomIn=false] If true, doesn't force zoom in to map level >= 7 * @memberof Panoramax.components.core.MapMore# */ setFilters(filters, skipZoomIn = false) { let { mapFilters, mapSeqFilters, mapPicFilters, reloadMapStyle } = mapFiltersToLayersFilters(filters, this._hasGridStats()); this._mapFilters = mapFilters; if(reloadMapStyle) { this.reloadLayersStyles(); } const allUsers = this.getVisibleUsers().includes("geovisio"); if(mapSeqFilters && allUsers) { mapSeqFilters = ["step", ["zoom"], true, 7, mapSeqFilters ]; } this.filterUserLayersContent("sequences", mapSeqFilters); this.filterUserLayersContent("pictures", mapPicFilters); if( !skipZoomIn && ( mapSeqFilters !== null || mapPicFilters !== null || (this._mapFilters.theme !== null && this._mapFilters.theme !== MAP_THEMES.DEFAULT) ) && allUsers && this.getZoom() < 7 && !this._hasGridStats() ) { this.easeTo({ zoom: 7 }); } /** * Event for filters changes * @event Panoramax.components.ui.MapMore#filters-changed * @type {maplibregl.util.evented.Event} * @property {string} [minDate] The minimum date in time range (ISO format) * @property {string} [maxDate] The maximum date in time range (ISO format) * @property {string} [pic_type] Camera type (equirectangular, flat, null/empty string for both) * @property {string} [camera] Camera make and model * @property {string} [theme] Map theme * @property {number[]} [qualityscore] QualityScore values, as a list of 1 to 5 grades */ this.fire("filters-changed", Object.assign({}, this._mapFilters)); } /** * Make given user layers visible on map, and hide all others (if any) * @memberof Panoramax.components.ui.Map# * @param {string|string[]} visibleIds The user layers IDs to display */ async setVisibleUsers(visibleIds = []) { await super.setVisibleUsers(visibleIds); // Force reload of styles & filters let { mapSeqFilters, mapPicFilters, reloadMapStyle } = mapFiltersToLayersFilters(this._mapFilters, this._hasGridStats()); if(reloadMapStyle) { this.reloadLayersStyles(); } const allUsers = visibleIds.includes("geovisio"); if(mapSeqFilters && allUsers) { mapSeqFilters = ["step", ["zoom"], true, 7, mapSeqFilters ]; } this.filterUserLayersContent("sequences", mapSeqFilters); this.filterUserLayersContent("pictures", mapPicFilters); } }