@panoramax/web-viewer
Version:
Panoramax web viewer for geolocated pictures
431 lines (384 loc) • 13.6 kB
JavaScript
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);
}
}