UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

442 lines (396 loc) 13.6 kB
/* eslint-disable no-unused-vars */ import "./Editor.css"; import Basic from "./Basic"; import Map from "../ui/Map"; import Photo from "../ui/Photo"; import { apiFeatureToPSVNode } from "../../utils/picture"; import { linkMapAndPhoto } from "../../utils/map"; import { VECTOR_STYLES } from "../../utils/map"; import { SYSTEM as PSSystem } from "@photo-sphere-viewer/core"; import { createWebComp } from "../../utils/widgets"; const LAYER_HEADING_ID = "sequence-headings"; /** * Editor allows to focus on a single sequence, and preview what you edits would look like. * It shows both picture and map. * * Make sure to set width/height through CSS for proper display. * * This component has a [CorneredGrid](#Panoramax.components.layout.CorneredGrid) layout, you can use directly any slot element to pass custom widgets. * @class Panoramax.components.core.Editor * @element pnx-editor * @extends Panoramax.components.core.Basic * @fires Panoramax.components.core.Basic#select * @fires Panoramax.components.core.Basic#ready * @fires Panoramax.components.core.Basic#broken * @property {Panoramax.components.ui.Loader} loader The loader screen * @property {Panoramax.utils.API} api The API manager * @property {Panoramax.components.ui.Map} map The MapLibre GL map itself * @property {Panoramax.components.ui.Photo} psv The Photo Sphere Viewer component itself * @property {Panoramax.components.layout.CorneredGrid} grid The grid layout manager * @slot `top-left` The top-left corner * @slot `top` The top middle corner * @slot `top-right` The top-right corner * @slot `bottom-left` The bottom-left corner * @slot `bottom` The bottom middle corner * @slot `bottom-right` The bottom-right corner * @example * ```html * <!-- Basic example --> * <pnx-editor * endpoint="https://panoramax.openstreetmap.fr/" * style="width: 300px; height: 250px" * /> * * <!-- With slotted widgets --> * <pnx-editor * endpoint="https://panoramax.openstreetmap.fr/" * style="width: 300px; height: 250px" * > * <p slot="top-right">My custom text</p> * </pnx-editor> * ``` */ export default class Editor extends Basic { /** * Component properties. All of [Basic properties](#Panoramax.components.core.Basic+properties) are available as well. * @memberof Panoramax.components.core.Editor# * @mixes Panoramax.components.core.Basic#properties * @type {Object} * @property {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md)) * @property {string} [picture] The picture ID to display * @property {string} [sequence] The sequence ID of the picture displayed * @property {object} [fetchOptions] Set custom options for fetch calls made against API ([same syntax as fetch options parameter](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters)) * @property {string|object} [mapstyle] The map's MapLibre style. This can be an a JSON object conforming to the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/), or a URL string pointing to one. Defaults to OSM vector tiles. * @property {string} [lang] To override language used for labels. Defaults to using user's preferred languages. * @property {object} [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). * @property {string} [background=streets] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street. */ static properties = { raster: {converter: Basic.GetJSONConverter()}, background: {type: String}, ...Basic.properties }; constructor() { super(); this.raster = null; this.background = "streets"; this.users = []; // Avoid default map data showing up // Create sub-containers this._psvContainer = document.createElement("div"); this._psvContainer.setAttribute("slot", "bg"); this._mapContainer = document.createElement("div"); this._mapContainer.setAttribute("slot", "bg"); this.grid = createWebComp("pnx-cornered-grid"); this.grid.appendChild(this._psvContainer); this.grid.appendChild(this._mapContainer); this.onceAPIReady().then(() => { this.loader.setAttribute("value", 30); // Check sequence ID is set if(!this.sequence) { this.loader.dismiss({}, "No sequence is selected"); } // Events this.addEventListener("select", this._onSelect.bind(this)); this._initPSV(); this._initMap(); }); } /** @private */ disconnectedCallback() { super.disconnectedCallback(); this.map?.destroy(); this.psv?.destroy(); } getClassName() { return "Editor"; } onceReady() { if(this.map && this.psv && this.map.loaded?.()) { return Promise.resolve(); } else { return new Promise(resolve => setTimeout(resolve, 100)).then(this.onceReady.bind(this)); } } connectedCallback() { if(Array.isArray(this.users) && this.users.length > 0) { console.warn("Parameters users can't be changed in Editor, only selected sequence can be visible"); this.users = []; } super.connectedCallback(); } /** @private */ firstUpdated() { super.firstUpdated(); this._moveChildToGrid(); } attributeChangedCallback(name, old, value) { if(name === "users" && Array.isArray(value) && value.length > 0) { console.warn("Parameters users can't be changed in Editor, only selected sequence can be visible"); } else { super.attributeChangedCallback(name, old, value); } } /** @private */ render() { return [this.loader, this.grid]; } getSubComponentsNames() { return super.getSubComponentsNames().concat(["map", "psv", "grid"]); } /** @private */ _initPSV() { try { this.psv = new Photo(this, this._psvContainer); this.psv._myVTour.datasource.nodeResolver = this._getNode.bind(this); } catch(e) { let err = !PSSystem.isWebGLSupported ? this._t.pnx.error_webgl : this._t.pnx.error_psv; this.loader.dismiss(e, err); } } /** @private */ _initMap() { try { this.map = new Map(this, this._mapContainer, { raster: this.raster, background: this.background, supplementaryStyle: this._createMapStyle(), zoom: 15, // Hack to avoid _initMapPosition call picMarkerDraggable: true, }); linkMapAndPhoto(this); this.loader.setAttribute("value", 50); this._loadSequence(); this.map.once("load", () => { if(this.map.hasTwoBackgrounds()) { this._addMapBackgroundWidget(); } this._bindPicturesEvents(); }); // Override picMarker setRotation for heading preview const oldRot = this.map._picMarker.setRotation.bind(this.map._picMarker); this.map._picMarker.setRotation = h => { h = this._lastRelHeading === undefined ? h : h + this._lastRelHeading - this.psv.getPictureRelativeHeading(); return oldRot(h); }; } catch(e) { this.loader.dismiss(e, this._t.pnx.error_psv); } } /** * Create style for GeoJSON sequence data. * @private */ _createMapStyle() { return { sources: { geovisio_editor_sequences: { type: "geojson", data: {"type": "FeatureCollection", "features": [] } } }, layers: [ { "id": "geovisio_editor_sequences", "type": "line", "source": "geovisio_editor_sequences", "layout": { ...VECTOR_STYLES.SEQUENCES.layout }, "paint": { ...VECTOR_STYLES.SEQUENCES.paint }, }, { "id": "geovisio_editor_pictures", "type": "circle", "source": "geovisio_editor_sequences", "layout": { ...VECTOR_STYLES.PICTURES.layout }, "paint": { ...VECTOR_STYLES.PICTURES.paint }, } ] }; } /** * Creates events handlers on pictures layer * @private */ _bindPicturesEvents() { // Pictures events this.map.on("mousemove", "geovisio_editor_pictures", () => { this.map.getCanvas().style.cursor = "pointer"; }); this.map.on("mouseleave", "geovisio_editor_pictures", () => { this.map.getCanvas().style.cursor = ""; }); this.map.on("click", "geovisio_editor_pictures", this.map._onPictureClick.bind(this.map)); } /** * Displays currently selected sequence on map * @private */ _loadSequence() { this.loader.setAttribute("value", 60); return this.api.getSequenceItems(this.sequence).then(seq => { this.loader.setAttribute("value", 80); // Hide loader after source load this.map.once("sourcedata", () => { this.map.setPaintProperty("geovisio_editor_sequences", "line-color", this.map._getLayerColorStyle("sequences")); this.map.setPaintProperty("geovisio_editor_pictures", "circle-color", this.map._getLayerColorStyle("pictures")); this.map.setLayoutProperty("geovisio_editor_sequences", "visibility", "visible"); this.map.setLayoutProperty("geovisio_editor_pictures", "visibility", "visible"); this.map.once("styledata", () => this.loader.dismiss()); }); // Create data source this._sequenceData = seq.features; this.map.getSource("geovisio_editor_sequences").setData({ "type": "FeatureCollection", "features": [ { "id": this.sequence, "type": "Feature", "properties": { "id": this.sequence, }, "geometry": { "type": "LineString", "coordinates": seq.features.map(p => p.geometry.coordinates) } }, ...seq.features.map(f => { f.properties.id = f.id; f.properties.sequences = [this.sequence]; return f; }) ] }); // Select picture if any if(this.picture) { const pic = seq.features.find(p => p.id === this.picture); if(pic) { this.select(this.sequence, this.picture, true); this.map.jumpTo({ center: pic.geometry.coordinates, zoom: 18 }); } else { console.log("Picture with ID", pic, "was not found"); } } // Show area of sequence otherwise else { const bbox = [ ...seq.features[0].geometry.coordinates, ...seq.features[0].geometry.coordinates ]; for(let i=1; i < seq.features.length; i++) { const c = seq.features[i].geometry.coordinates; if(c[0] < bbox[0]) { bbox[0] = c[0]; } if(c[1] < bbox[1]) { bbox[1] = c[1]; } if(c[0] > bbox[2]) { bbox[2] = c[0]; } if(c[1] > bbox[3]) { bbox[3] = c[1]; } } this.map.fitBounds(bbox, {animate: false}); } }).catch(e => this.loader.dismiss(e, this._t.pnx.error_api)); } /** * Get the PSV node for wanted picture. * * @param {string} picId The picture ID * @returns The PSV node * @private */ _getNode(picId) { const f = this._sequenceData.find(f => f.properties.id === picId); const n = f ? apiFeatureToPSVNode(f, this._t, this._isInternetFast) : null; if(n) { delete n.links; } return n; } /** * Creates the widget to switch between aerial and streets imagery * @private */ _addMapBackgroundWidget() { // Container const pnlLayers = createWebComp("pnx-map-background", {_parent: this, size: "sm", slot: "bottom-left"}); this.grid.appendChild(pnlLayers); } /** @private */ _moveChildToGrid() { const slotContent = Array.from(this.querySelectorAll("[slot]")); slotContent.forEach(n => { // Add parent + translation for our components if(n.tagName?.toLowerCase().startsWith("pnx-")) { n._parent = this; n._t = this._t; } this.grid.appendChild(n); }); } /** * Preview on map how the new relative heading would reflect on all pictures. * This doesn't change anything on API-side, it's just a preview. * @memberof Panoramax.components.core.Editor# * @param {number} [relHeading] The new relative heading compared to sequence path. In degrees, between -180 and 180 (0 = front, -90 = left, 90 = right). Set to null to remove preview. */ previewSequenceHeadingChange(relHeading) { const layerExists = this.map.getLayer(LAYER_HEADING_ID) !== undefined; this.map._picMarkerPreview.remove(); // If no value set, remove layer if(relHeading === undefined) { delete this._lastRelHeading; if(layerExists) { this.map.setLayoutProperty(LAYER_HEADING_ID, "visibility", "none"); } // Update selected picture marker if(this.picture) { this.map._picMarker.setRotation(this.psv.getXY().x); } return; } this._lastRelHeading = relHeading; // Create preview layer if(!layerExists) { this.map.addLayer({ "id": LAYER_HEADING_ID, "type": "symbol", "source": "geovisio_editor_sequences", "layout": { "icon-image": "pnx-marker", "icon-overlap": "always", "icon-size": 0.8, }, }); } // Change heading const currentRelHeading = - this.psv.getPictureRelativeHeading(); this.map.setLayoutProperty(LAYER_HEADING_ID, "visibility", "visible"); this.map.setLayoutProperty( LAYER_HEADING_ID, "icon-rotate", ["+", ["get", "view:azimuth"], currentRelHeading, relHeading ] ); // Skip selected picture and linestring geom const filters = [["==", ["geometry-type"], "Point"]]; if(this.picture) { filters.push(["!=", ["get", "id"], this.picture]); } this.map.setFilter(LAYER_HEADING_ID, ["all", ...filters]); // Update selected picture marker if(this.picture) { this.map._picMarker.setRotation(this.psv.getXY().x); } } /** * Event handler for picture loading * @private */ _onSelect() { // Update preview of heading change if(this._lastRelHeading !== undefined) { this.previewSequenceHeadingChange(this._lastRelHeading); } } } customElements.define("pnx-editor", Editor);