UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

431 lines (384 loc) 13.6 kB
export const MAP_FILTERS_JS2URL = { "minDate": "date_from", "maxDate": "date_to", "pic_type": "pic_type", "camera": "camera", "theme": "theme", "qualityscore": "pic_score", }; const MAP_FILTERS_URL2JS = Object.fromEntries(Object.entries(MAP_FILTERS_JS2URL).map(v => [v[1], v[0]])); const MAP_NONE = ["none", "null", "false", false]; const MAPLIBRE_OPTIONS = [ "antialias", "bearing", "bearingSnap", "bounds", "boxZoom", "clickTolerance", "collectResourceTiming", "cooperativeGestures", "crossSourceCollisions", "doubleClickZoom", "dragPan", "dragRotate", "fadeDuration", "failIfMajorPerformanceCaveat", "fitBoundsOptions", "hash", "interactive", "localIdeographFontFamily", "locale", "logoPosition", "maplibreLogo", "maxBounds", "maxCanvasSize", "maxPitch", "maxTileCacheSize", "maxTileCacheZoomLevels", "maxZoom", "minPitch", "minZoom", "pitch", "pitchWithRotate", "pixelRatio", "preserveDrawingBuffer", "refreshExpiredTiles", "renderWorldCopies", "scrollZoom", "touchPitch", "touchZoomRotate", "trackResize", "transformCameraUpdate", "transformRequest", "validateStyle" ]; function filterMapLibreOptions(opts) { return Object.fromEntries(Object.entries(opts).filter(([key]) => MAPLIBRE_OPTIONS.includes(key))); } /** * Merges all URL parameters and component attributes into a single set of coherent settings. * * @class Panoramax.utils.InitParameters * @param {object} [componentAttrs] HTML attributes from parent component * @param {object} [urlParams] Parameters extracted from URL * @param {object} [browserStorage] Parameters read from local/session storage */ export default class InitParameters { // eslint-disable-line import/no-unused-modules constructor(componentAttrs = {}, urlParams = {}, browserStorage = {}) { // Skip URL parameters if disabled by component if(componentAttrs["url-parameters"] === "false") { urlParams = {}; } // Sanitize PSV parameters let componentPsv = {}; if(typeof componentAttrs?.psv === "object") { componentPsv = componentAttrs.psv; } // Sanitize Map parameters let componentMap = {}; if(typeof componentAttrs?.map === "object") { componentMap = componentAttrs.map; } let browserMap = {}; if(typeof browserStorage?.map === "object") { browserMap = browserStorage.map; } // Extract map position from URL let urlMap = urlParams.map && urlParams.map !== "none" ? getMapPositionFromString(urlParams.map) : null; // Parse and prioritize all parameters // - Overlapping between URL & component let map = MAP_NONE.includes(urlParams.map) || MAP_NONE.includes(componentAttrs.map) ? false : true; let focus = urlParams.focus || componentAttrs.focus; let picture = urlParams.pic || componentAttrs.picture; let users = urlParams.users || componentAttrs.users; let psv_speed = urlParams.speed || componentPsv?.transitionDuration; let psv_nav = urlParams.nav || componentPsv?.picturesNavigation; let map_theme = urlParams.theme || browserMap?.theme || componentMap.theme; let map_background = urlParams.background || browserMap?.background || componentMap.background; let map_center = urlMap?.center || (!picture && (browserMap?.center || componentMap.center)) || [0,0]; let map_zoom = urlMap?.zoom || browserMap?.zoom || componentMap.zoom; let map_pitch = urlMap?.pitch || componentMap.pitch; let map_bearing = urlMap?.bearing || componentMap.bearing; // - Component only let geocoder = componentAttrs.geocoder; let widgets = componentAttrs.widgets; let sequence = componentAttrs.sequence; let fetchOptions = componentAttrs.fetchOptions; let style = componentAttrs.style; let lang = componentAttrs.lang; let endpoint = componentAttrs.endpoint; let map_raster = componentMap.raster; let map_attribution = componentMap.attributionControl; let map_others = filterMapLibreOptions(componentMap); let keyboardShortcuts = componentAttrs["keyboard-shortcuts"]; // - URL only let psv_xyz = urlParams.xyz; let map_date_from = urlParams.date_from; let map_date_to = urlParams.date_to; let map_pic_type = urlParams.pic_type; let map_camera = urlParams.camera; let map_pic_score = urlParams.pic_score; let psv_xywh = urlParams.xywh; // Check coherence if(!["map", "pic"].includes(focus)) { console.warn("Invalid value for parameter focus:", focus); focus = map && !picture ? "map" : "pic"; } else if(focus === "map" && !map) { console.warn("Parameter focus can't be 'map' as map is disabled"); focus = "pic"; } if(map_background == "aerial" && !map_raster) { console.warn("Parameter background can't be 'aerial' as no aerial imagery is available"); map_background = "streets"; } if(map && !picture) { focus = "map"; } // Put all attributes in appropriate container this._parentInit = { map, users, fetchOptions, style, lang, endpoint }; this._parentPostInit = { focus, picture, sequence, geocoder, widgets, forceFocus: true, keyboardShortcuts: true }; this._psvInit = Object.fromEntries( Object.entries(componentPsv).filter( ([k,]) => !["transitionDuration", "picturesNavigation"].includes(k) ) ); this._psvAny = { transitionDuration: psv_speed, picturesNavigation: psv_nav, }; this._psvPostInit = { xyz: psv_xyz, xywh: psv_xywh }; this._mapInit = { raster: map_raster, attributionControl: map_attribution, ...map_others }; this._mapAny = { theme: map_theme, background: map_background, center: map_center, zoom: map_zoom, pitch: map_pitch, bearing: map_bearing, users, }; this._mapPostInit = { date_from: map_date_from, date_to: map_date_to, pic_type: map_pic_type, camera: map_camera, pic_score: map_pic_score, }; if(keyboardShortcuts === "false") { this._psvInit.keyboard = false; this._psvInit.keyboardActions = {}; this._mapInit.keyboard = false; this._parentPostInit.keyboardShortcuts = false; } } /** * Cleans out undefined values from object. * @param {object} obj The input object * @returns {object} Clean object without undefined values * @private */ _sanitize(obj) { return Object.fromEntries(Object.entries(obj).filter(([,v]) => v !== undefined)); } /** * Get core component initialization parameters. * They must be passed as soon as possible. * @memberof Panoramax.utils.InitParameters# */ getParentInit() { return this._sanitize(this._parentInit); } /** * Get core component post-initialization parameters. * They must be passed after first rendering or init. * @memberof Panoramax.utils.InitParameters# */ getParentPostInit() { return this._sanitize(this._parentPostInit); } /** * Get Photo Sphere Viewer initialization parameters. * They must be passed as soon as possible. * @memberof Panoramax.utils.InitParameters# */ getPSVInit() { return this._sanitize(Object.assign({}, this._psvInit, this._psvAny)); } /** * Get Photo Sphere Viewer post-initialization parameters. * They must be passed after first rendering or init. * @memberof Panoramax.utils.InitParameters# */ getPSVPostInit() { return this._sanitize(Object.assign({}, this._psvPostInit, this._psvAny)); } /** * Get MapLibre GL initialization parameters. * They must be passed as soon as possible. * @memberof Panoramax.utils.InitParameters# */ getMapInit() { return this._sanitize(Object.assign({}, this._mapInit, this._mapAny)); } /** * Get MapLibre GL post-initialization parameters. * They must be passed after first rendering or init. * @memberof Panoramax.utils.InitParameters# */ getMapPostInit() { return this._sanitize(Object.assign({}, this._mapPostInit, this._mapAny)); } /** * Reads public properties from LitElement and returns a classic key-value object. * @param {class} compClass The component class (with static `properties` definition) * @param {LitElement} comp The component itself */ static GetComponentProperties(compClass, comp) { const props = {}; for(let classK in compClass.properties) { let classV = compClass.properties[classK]; if(!classV.state && comp[classK] != null) { props[classK] = comp[classK]; } } return props; } } /** * Extracts map center, zoom & more from formatted string. * @param {string} str The map position as hash string * @param {Panoramax.components.ui.Map} [map] The current map * @returns {object} { center, zoom, pitch, bearing } * @private */ export function getMapPositionFromString(str, map) { const loc = (str || "").split("/"); if (loc.length >= 3 && !loc.some(v => isNaN(v))) { const res = { center: [+loc[2], +loc[1]], zoom: +loc[0], pitch: +(loc[4] || 0) }; if(map) { res.bearing = map.dragRotate.isEnabled() && map.touchZoomRotate.isEnabled() ? +(loc[3] || 0) : map.getBearing(); } return res; } else { return null; } } /** * Extracts from string xyz position * @param {string} str The xyz position as hash string * @returns {object} { x, y, z } * @private */ export function xyzParamToPSVPosition(str) { const loc = (str || "").split("/"); if (loc.length === 3 && !loc.some(v => isNaN(v))) { const res = { x: +loc[0], y: +loc[1], z: +loc[2] }; return res; } else { return null; } } /** * Extracts from string xywh position * @param {string} str The xywh position as hash string * @param {object} [picmeta] The current picture metadata, used to compute zoom based on image size * @returns {object} { textureX, textureY, z } * @private */ function xywhParamToPSVPosition(str, picmeta) { const loc = (str || "").split(","); if (loc.length === 4 && !loc.some(v => isNaN(v))) { const size = picmeta?.properties?.["pers:interior_orientation"]?.sensor_array_dimensions; const res = { textureX: +loc[0] + loc[2] / 2, textureY: +loc[1] + loc[3] / 2, z: size && size.length == 2 ? (1 - (((loc[2] / size[0]) + (loc[3] / size[1])) / 2)) * 75 : null, }; return res; } else { return null; } } /** * Extracts from hash parsed keys all map filters values * @param {*} vals Hash keys * @returns {object} Map filters * @private */ export function paramsToMapFilters(vals) { const newMapFilters = {}; for(let k in MAP_FILTERS_URL2JS) { if(vals[k]) { newMapFilters[MAP_FILTERS_URL2JS[k]] = vals[k]; } } if(newMapFilters.qualityscore) { let values = newMapFilters.qualityscore.split(""); const mapping = {"A": 5, "B": 4, "C": 3, "D": 2, "E": 1}; newMapFilters.qualityscore = values.map(v => mapping[v]); } return newMapFilters; } /** * Change PSV current state based on standardized parameters. * @param {Photo} psv The Photo Sphere Viewer component to change. * @param {object} params The parameters to apply. */ export function alterPSVState(psv, params) { // Change xyz position if(params.xyz) { psv.addEventListener("picture-loaded", () => { const coords = xyzParamToPSVPosition(params.xyz); psv.setXYZ(coords.x, coords.y, coords.z); }, {once: true}); } // Change xywh position if(params.xywh) { psv.addEventListener("picture-loaded", () => { const coords = xywhParamToPSVPosition(params.xywh, psv.getPictureMetadata()); psv.rotate(coords); psv.zoom(coords.z); }, {once: true}); } // Change transitionDuration let td = params.transitionDuration || params.speed; if(td !== undefined) { psv.setTransitionDuration(td); } // Change pictures navigation mode let nav = params.picturesNavigation || params.nav; if(["none", "pic", "any", "seq"].includes(nav)) { psv.setPicturesNavigation(nav); } } /** * Change MapLibre GL current state based on standardized parameters. * @param {Map} map The MapLibre component to change. * @param {object} params The parameters to apply. */ export async function alterMapState(map, params) { // Map position const mapOpts = getMapPositionFromString(params.map, map); if(mapOpts) { map.jumpTo(mapOpts); } // Visible users let vu = Array.isArray(params.users) ? params.users : (params.users || "").split(","); if(vu.length === 0 || (vu.length === 1 && vu[0].trim() === "")) { vu = ["geovisio"]; } await map.setVisibleUsers(vu); // Change map filters map.setFilters?.(paramsToMapFilters(params)); // Change map background if(["aerial", "streets"].includes(params.background)) { map.setBackground(params.background); } } /** * Change PhotoViewer current state based on standardized parameters. * @param {PhotoViewer} viewer The PhotoViewer component to change. * @param {object} params The parameters to apply. */ export function alterPhotoViewerState(viewer, params) { // Restore selected picture let pic = params.picture || params.pic; if(pic) { const picIds = pic.split(";"); // Handle multiple IDs coming from OSM if(picIds.length > 1) { console.warn("Multiple picture IDs passed in URL, only first one kept"); } viewer.select(null, picIds[0], true); } else { viewer.select(); } } /** * Change Viewer current state based on standardized parameters. * @param {Viewer} viewer The Viewer component to change. * @param {object} params The parameters to apply. */ export function alterViewerState(viewer, params) { alterPhotoViewerState(viewer, params); // Change focus if(params.focus === "map" && viewer?.map) { viewer.setPopup(false); viewer._setFocus("map", null, params.forceFocus); } else if(params.focus === "pic" && viewer?.mini) { viewer.setPopup(false); viewer._setFocus("pic", null, params.forceFocus); } else if(params.focus && params.focus === "meta" && viewer?.mini) { viewer._setFocus("pic", null, params.forceFocus); } }