From 2a6d903009ee1a4748b00dc76fd3913fee300ad7 Mon Sep 17 00:00:00 2001 From: arno974 Date: Wed, 8 Apr 2026 14:46:56 +0400 Subject: [PATCH 1/4] =?UTF-8?q?Am=C3=A9lioration=20de=20la=20v=C3=A9rifica?= =?UTF-8?q?tion=20des=20=C3=A9v=C3=A9nements=20et=20suppression=20d'erreur?= =?UTF-8?q?s=20d'initialisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/api/panoramax/panoramax_3.8.js | 236 ++++++++++++++++++------- 1 file changed, 169 insertions(+), 67 deletions(-) diff --git a/library/api/panoramax/panoramax_3.8.js b/library/api/panoramax/panoramax_3.8.js index a8661d7..ce5098a 100644 --- a/library/api/panoramax/panoramax_3.8.js +++ b/library/api/panoramax/panoramax_3.8.js @@ -15,6 +15,9 @@ const lizmapPanoramax = function() { // Dock position: can be dock, minidock const DOCK_POSITION = 'dock'; + + // BUFFER RADIUS FOR PANORAMAX SEARCH (in map units) + const BUFFER_RADIUS = 3; // Title of the dock const DOCK_TITLE = 'Panoramax'; @@ -54,7 +57,10 @@ const lizmapPanoramax = function() { ################################### ******************************** */ - // HTML Content + const PANORAMAX_JS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@3.2.3/build/index.min.js'; + const PANORAMAX_CSS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@3.2.3/build/index.min.css' + + // HTML Content const DOM_ID_PANORAMAX = "LizPanoramax-viewer"; const HTML_TEMPLATE = `

${POPUP_TEXT}

@@ -67,7 +73,14 @@ const lizmapPanoramax = function() { constructor(){ //Check if Panoramax Layer exists. If not, display an error message and exit directly this.panoramaxLayer = this.#getPanoramaxLayer(); - + this.panoramaxDockOpen = false; + + this.mapClickHandler = null; + this.panoViewerListeners = { + 'psv:view-rotated': null, + 'psv:picture-loaded': null + }; + if(!this.panoramaxLayer) { // check if there is a panoramax layer in the project const error = `No Panoramax layer available. @@ -88,10 +101,7 @@ const lizmapPanoramax = function() { } // everything is good we can display the "normal" dock content - this.#addMapEvent(); - this.#addLizmapDock(HTML_TEMPLATE); - //this.#setPanoramaxLayerVisibility(false); - this.#addPanoramaxHeadingLayer(); + this.#addLizmapDock(HTML_TEMPLATE); }); } @@ -101,37 +111,50 @@ const lizmapPanoramax = function() { */ async #loadScripts() { return new Promise(resolve => { - - const panoramax_js = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@3.2.3/build/index.min.js'; - const panoramax_css = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@3.2.3/build/index.min.css' - - if (!document.querySelector(`script[src="${panoramax_js}"]`)) { - // Chargement du script JS - const script = document.createElement('script'); - script.src = panoramax_js; - script.setAttribute('type', 'text/javascript'); - - script.onload = function () { - resolve(true); - }; - - script.onerror = function () { - if (DEBUG_MODE) console.error("Échec du chargement du script."); - resolve(false); - }; - - document.head.appendChild(script); - + + let scriptLoaded = !!document.querySelector(`script[src="${PANORAMAX_JS_URL}"]`); + let cssLoaded = !!document.querySelector(`link[href="${PANORAMAX_CSS_URL}"]`); + + + let jsPromise, cssPromise; + + if (!scriptLoaded) { + jsPromise = new Promise(jsResolve => { + const script = document.createElement('script'); + script.src = PANORAMAX_JS_URL; + script.type = 'text/javascript'; + script.onload = () => jsResolve(true); + script.onerror = () => { + if (DEBUG_MODE) console.error("Échec du chargement du script."); + jsResolve(false); + }; + document.head.appendChild(script); + }); + } else { + jsPromise = Promise.resolve(true); } - - if (!document.querySelector(`link[href="${panoramax_css}"]`)) { - // Chargement du CSS - const style = document.createElement("link"); - style.href = panoramax_css; - style.rel = 'stylesheet'; - style.type = 'text/css'; - document.head.appendChild(style); - } + + if (!cssLoaded) { + cssPromise = new Promise(cssResolve => { + const style = document.createElement("link"); + style.href = PANORAMAX_CSS_URL; + style.rel = 'stylesheet'; + style.type = 'text/css'; + style.onload = () => cssResolve(true); + style.onerror = () => { + if (DEBUG_MODE) console.error("Échec du chargement du CSS."); + cssResolve(false); + }; + document.head.appendChild(style); + }); + } else { + cssPromise = Promise.resolve(true); + } + + Promise.all([jsPromise, cssPromise]).then(results => { + // Si l'un des deux a échoué, on retourne false + resolve(results.every(Boolean)); + }); }); } @@ -189,6 +212,18 @@ const lizmapPanoramax = function() { * @param {Panoramax Picture} picture */ #setPanoramaxHeadingLayerHeading(picture){ + // Vérifie l'existence de la couche et de sa source avant de tenter de les utiliser + if(!this.layerArrowHeadingSource || !this.layerArrowHeading) { + if (DEBUG_MODE) console.warn("Heading layer not initialized"); + return; + } + + // Valider les données + if(!picture.geometry?.coordinates || !picture.properties) { + if (DEBUG_MODE) console.warn("Invalid picture data", picture); + return; + } + const oldFeature = this.layerArrowHeadingSource.getFeatures()?.[0]; if (oldFeature) { this.layerArrowHeadingSource.removeFeature(oldFeature); @@ -200,7 +235,8 @@ const lizmapPanoramax = function() { picture.geometry.coordinates[1] ]).transform('EPSG:4326', lizMap.map.projection.projCode) })); - const r = picture.properties["view:azimuth"] * (Math.PI/180); + const azimuth = picture.properties["view:azimuth"] ?? 0; + const r = azimuth * (Math.PI/180); this.layerArrowHeading.getStyle().getImage().setRotation(r); this.layerArrowHeading.changed(); } @@ -219,11 +255,16 @@ const lizmapPanoramax = function() { * Init all the method */ initPanoramaxDock(){ - if(this.panoramaxLayer){ + if(this.panoramaxLayer && !this.panoramaxDockOpen){ lizMap.mainLizmap.popup.active = false; - this.panoramaxDockOpen = true; + this.panoramaxDockOpen = true; + this.#addPanoramaxHeadingLayer(); this.#setPanoramaxLayerVisibility(true); - this.#addPanoramaxViewer(); + //Attendre que le DOM soit prêt + setTimeout(() => { + this.#addPanoramaxViewer(); + this.#addMapEvent(); + }, 100); } } @@ -231,51 +272,103 @@ const lizmapPanoramax = function() { * Add Panoramax Viewer */ #addPanoramaxViewer(){ - this.panoViewer = new Panoramax.Viewer( - DOM_ID_PANORAMAX, - PANORAMAX_INSTANCE, - { - hash:false, // !!! do not change => change Lizmap URL - map: false - } - ); - this.#addPanoramaxViewerEvent(); + if (!window.Panoramax) { + if (DEBUG_MODE) console.error("Panoramax global not available"); + return; + } + const viewerContainer = document.getElementById(DOM_ID_PANORAMAX); + if (!viewerContainer) { + if (DEBUG_MODE) console.error("Viewer container not found in DOM"); + return; + } + if(this.panoViewer) { + if (DEBUG_MODE) console.warn("Panoramax Viewer already initialized"); + return; + } + try { + this.panoViewer = new Panoramax.Viewer( + DOM_ID_PANORAMAX, + PANORAMAX_INSTANCE, + { + hash:false, // !!! do not change => change Lizmap URL + map: false + } + ); + this.#addPanoramaxViewerEvent(); + } catch (error) { + if (DEBUG_MODE) console.error("Error initializing Panoramax Viewer", error); + throw new Error("Error initializing Panoramax Viewer"); + } } /** * Add all Panoramax Viewer Events */ #addPanoramaxViewerEvent(){ - this.panoViewer.addEventListener('psv:view-rotated', (e) => { + // Stocker les listeners AVEC les bonnes fonctions + this.panoViewerListeners['psv:view-rotated'] = (e) => { if(e.explicitOriginalTarget._selectedPicId){ - let r = e.detail.x * (Math.PI/180); + const azimuth = e.detail.x ?? 0; // Valeur par défaut + let r = azimuth * (Math.PI/180); this.layerArrowHeading.getStyle().getImage().setRotation(r); this.layerArrowHeading.changed(); } - }); + }; - this.panoViewer.addEventListener('psv:picture-loaded', (e) => { - let r = e.detail.x * (Math.PI/180); - if(this.layerArrowHeadingSource.getFeatures()[0] && e.detail.lon && e.detail.lat){ + this.panoViewerListeners['psv:picture-loaded'] = (e) => { + const azimuth = e.detail.x ?? 0; // Valeur par défaut + let r = azimuth * (Math.PI/180); + if(this.layerArrowHeadingSource.getFeatures()[0] + && typeof e.detail?.lon === 'number' + && typeof e.detail?.lat === 'number' ){ const coords = lizMap.ol.proj.transform([e.detail.lon, e.detail.lat], 'EPSG:4326', lizMap.mainLizmap.projection); lizMap.mainLizmap.map.getView().setCenter(coords); this.layerArrowHeadingSource.getFeatures()[0].getGeometry().setCoordinates(coords); } this.layerArrowHeading.getStyle().getImage().setRotation(r); this.layerArrowHeading.changed(); - }); + }; + + this.panoViewer.addEventListener('psv:view-rotated', this.panoViewerListeners['psv:view-rotated']); + this.panoViewer.addEventListener('psv:picture-loaded', this.panoViewerListeners['psv:picture-loaded']); } /** * Fetch picture on single map cick */ #addMapEvent(){ - lizMap.mainLizmap.map.on('singleclick', e => { + // Créer une méthode nommée pour pouvoir la désabonner + this.mapClickHandler = (e) => { //Fire event only if panoramax dock is opened if(this.panoramaxDockOpen){ - const extent =this.#getBufferedExtent(e.coordinate); + const extent = this.#getBufferedExtent(e.coordinate); this.#getPanoramaxPicture(extent); } + }; + lizMap.mainLizmap.map.on('singleclick', this.mapClickHandler); + } + + /** + * Remove map click event listener + */ + #removeMapEvent(){ + if(this.mapClickHandler){ + lizMap.mainLizmap.map.un('singleclick', this.mapClickHandler); + this.mapClickHandler = null; + } + } + + /** + * Remove all Panoramax Viewer Events + */ + #removePanoramaxViewerEvent(){ + if(!this.panoViewer) return; + + Object.keys(this.panoViewerListeners).forEach(eventName => { + if(this.panoViewerListeners[eventName]){ + this.panoViewer.removeEventListener(eventName, this.panoViewerListeners[eventName]); + this.panoViewerListeners[eventName] = null; + } }); } @@ -287,13 +380,12 @@ const lizmapPanoramax = function() { #getBufferedExtent(p){ const point = new lizMap.ol.geom.Point(p); const extent = point.getExtent(); - const radius = 3; - const bufferedExtent = new lizMap.ol.extent.buffer(extent,radius); + const bufferedExtent = new lizMap.ol.extent.buffer(extent,BUFFER_RADIUS); const pbl = new lizMap.ol.geom.Point([bufferedExtent[0], bufferedExtent[1]]); //bottom left const pur = new lizMap.ol.geom.Point([bufferedExtent[2], bufferedExtent[3]]); //upper right - if(lizMap.map.projection.projCode != "EPSG:4326"){ + if(lizMap.map.projection.projCode !== "EPSG:4326"){ // reproject extent to 4326 pbl.transform(lizMap.map.projection.projCode, 'EPSG:4326'); pur.transform(lizMap.map.projection.projCode, 'EPSG:4326'); @@ -323,7 +415,7 @@ const lizmapPanoramax = function() { throw new Error(error); } const picture = await response.json(); - if(picture.features.length){ + if(picture?.features?.length > 0 && this.panoViewer){ this.panoViewer.select(null, picture.features[0].id, true); this.#setPanoramaxHeadingLayerHeading(picture.features[0]); } @@ -340,10 +432,18 @@ const lizmapPanoramax = function() { if(this.panoramaxLayer) { lizMap.mainLizmap.popup.active = true; this.panoramaxDockOpen = false; + + // Remove map click listener + this.#removeMapEvent(); + + // Remove Panoramax viewer listeners + this.#removePanoramaxViewerEvent(); + + // Clear layer used for heading arrow this.layerArrowHeadingSource.clear(); //Hide layer - lizPanoramax.#setPanoramaxLayerVisibility(false); + this.#setPanoramaxLayerVisibility(false); // Remove all viewer references if (this.panoViewer) { @@ -363,6 +463,8 @@ const lizmapPanoramax = function() { /** * Lizmap event */ + let lizPanoramax; + lizMap.events.on({ 'uicreated': function(e) { lizPanoramax = new LizPanoramax(); @@ -370,24 +472,24 @@ const lizmapPanoramax = function() { //MINI DOCK 'minidockopened': e => { - if (e.id === DOCK_ID) { + if (e.id === DOCK_ID && lizPanoramax) { lizPanoramax.initPanoramaxDock(); } }, 'minidockclosed': e => { - if (e.id === DOCK_ID) { + if (e.id === DOCK_ID && lizPanoramax) { lizPanoramax.removePanoramaxDock(); } }, //DOCK 'dockopened': e => { - if (e.id === DOCK_ID) { + if (e.id === DOCK_ID && lizPanoramax) { lizPanoramax.initPanoramaxDock(); } }, 'dockclosed': e => { - if (e.id === DOCK_ID) { + if (e.id === DOCK_ID && lizPanoramax) { lizPanoramax.removePanoramaxDock(); } }, From e140ffbe4e0964b5f2673f172ebd5c121adc5a65 Mon Sep 17 00:00:00 2001 From: arno974 Date: Wed, 8 Apr 2026 16:59:13 +0400 Subject: [PATCH 2/4] =?UTF-8?q?feat(panoramax):=20Migration=20V3->V4=20et?= =?UTF-8?q?=20mise=20=C3=A0=20jour=20README=20-=20Renommage=20des=20deux?= =?UTF-8?q?=20fichiers=20JS=20pour=20une=20meilleur=20coh=C3=A9rence=20ave?= =?UTF-8?q?c=20la=20version=20du=20viewer=20de=20Panoramax=20-=20Portage?= =?UTF-8?q?=20vers=20Panoramax=20V4=20avec=20Web=20Components=20(pnx-photo?= =?UTF-8?q?-viewer)=20-=20Utilisation=20de=20oncePSVReady()=20pour=20garan?= =?UTF-8?q?tir=20l'initialisation=20compl=C3=A8te=20-=20Correction=20de=20?= =?UTF-8?q?la=20signature=20select()=20:=20select(seqId,=20picId,=20force)?= =?UTF-8?q?=20-=20Nettoyage=20optimis=C3=A9=20du=20viewer=20pour=20=C3=A9v?= =?UTF-8?q?iter=20le=20cache=20de=20photos=20anciennes=20-=20Documentation?= =?UTF-8?q?=20compl=C3=A8te=20:=20diff=C3=A9rences=20V3/V4,=20recommandati?= =?UTF-8?q?ons,=20instructions=20claires=20-=20Correction=20typos=20et=20s?= =?UTF-8?q?uppression=20doublons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/api/panoramax/README.md | 31 +- .../{panoramax_3.8.js => panoramax3_3.9.js} | 0 library/api/panoramax/panoramax4_3.9.js | 495 ++++++++++++++++++ 3 files changed, 518 insertions(+), 8 deletions(-) rename library/api/panoramax/{panoramax_3.8.js => panoramax3_3.9.js} (100%) create mode 100644 library/api/panoramax/panoramax4_3.9.js diff --git a/library/api/panoramax/README.md b/library/api/panoramax/README.md index 6083b80..6363905 100644 --- a/library/api/panoramax/README.md +++ b/library/api/panoramax/README.md @@ -24,13 +24,20 @@ chargera de la reprojection. ![alt text](image-1.png) ## Utilisation +Deux fichiers JavaScript sont disponibles : +- **panoramax3_3.9.js** : utilise l'API V3 de Panoramax (historique, stable) +- **panoramax4_3.9.js** : utilise l'API V4 de Panoramax (Web Components, recommandé) + +**Nous recommandons d'utiliser la V4** qui est plus moderne et mieux maintenue. Pour utiliser le script Panoramax dans votre projet : 1. assurez-vous d'avoir une couche Panoramax présente dans votre projet QGIS -2. copier `panoramax_3.8.js` dans le dossier `media/js` de votre projet +2. copier `panoramax3_3.9.js` ou `panoramax4_3.9.js` dans le dossier `media/js/default` de votre répertoire (ou `media/js/nom du projet`) 3. le bouton Panoramax s'affichera dans Lizmap Web Client -4. En cliquant sur ce bouton, les photos associées aux points de la couche Panoramax seront affichées. - Un clic sur un point permet d'afficher la photo correspondante. +4. en cliquant sur ce bouton, un dock s'ouvrira affichant le visualiseur de photos +5. cliquez sur un point de la couche Panoramax sur la carte pour afficher la photo correspondante + - une flèche directionnelle indique l'azimut de la photo + - la carte se centre automatiquement sur le point ## Personnalisation @@ -63,13 +70,21 @@ projection for this script to work. QGIS will handle the reprojection. ## Usage +Two JavaScript files are available: +- **panoramax3_3.9.js**: uses the Panoramax V3 API (legacy, stable) +- **panoramax4_3.9.js**: uses the Panoramax V4 API (Web Components, recommended) + +**We recommend using V4**, which is more modern and better maintained. -1. copy the panoramax_3.8.js file to the media/js folder of your QGIS project. -2. verify that the Panoramax layer (or group) exists in your QGIS project. -3. open Lizmap Web Client and ensure the version installed is 3.8 or higher. -4. the Panoramax button should now appear in the Lizmap interface, allowing you to view the photos related to the points - in the Panoramax layer. +To use the Panoramax script in your project: +1. ensure you have a Panoramax layer present in your QGIS project +2. copy `panoramax3_3.9.js` or `panoramax4_3.9.js` to the `media/js/default` folder of your project directory (or `media/js/project name`) +3. the Panoramax button will appear in Lizmap Web Client +4. by clicking on this button, a dock will open displaying the photo viewer +5. click on a point in the Panoramax layer on the map to display the corresponding photo + - a directional arrow indicates the photo's azimuth + - the map automatically centers on the point ## Customization diff --git a/library/api/panoramax/panoramax_3.8.js b/library/api/panoramax/panoramax3_3.9.js similarity index 100% rename from library/api/panoramax/panoramax_3.8.js rename to library/api/panoramax/panoramax3_3.9.js diff --git a/library/api/panoramax/panoramax4_3.9.js b/library/api/panoramax/panoramax4_3.9.js new file mode 100644 index 0000000..2e666bd --- /dev/null +++ b/library/api/panoramax/panoramax4_3.9.js @@ -0,0 +1,495 @@ +/** + * @license Mozilla Public License Version 2.0 + * This script has been developed by the "community" + * There isn't any guarantee that this script will work on another version of Lizmap Web Client. + */ + +const lizmapPanoramax = function() { + + // ID of the dock (do not change) + const DOCK_ID = 'panoramax'; + + // Icon of the dock menu and used before each link + // See https://getbootstrap.com/2.3.2/base-css.html#icons + const DOCK_ICON = 'icon-camera'; + + // Dock position: can be dock, minidock + const DOCK_POSITION = 'dock'; + + // BUFFER RADIUS FOR PANORAMAX SEARCH (in map units) + const BUFFER_RADIUS = 3; + + // Title of the dock + const DOCK_TITLE = 'Panoramax'; + + // Panoramax Vector Tile Layer in QGIS + // VERY IMPORTANT => The layer name must be the same as the one in QGIS + const PANORAMAX_QGIS_LAYER_NAME = "Panoramax" + + // ARROW ICON PROPERTIES + const ARROW_ICON_SIZE = 0.3; + const ARROW_ICON_COLOR = "#e4e8e6"; + + const PANORAMAX_INSTANCE = 'https://api.panoramax.xyz/api'; + + const CONTENT_TEXT = { + "fr" : "Veuillez cliquer sur un point de la couche Panoramax pour afficher les photos." + ,"en" : "Please click on a point in the Panoramax layer to display the photos." + ,"it" : "Per favore, fai clic su un punto del livello Panoramax per visualizzare le foto." + ,"es" : "Por favor, haz clic en un punto de la capa Panoramax para mostrar las fotos." + ,"de" : "Bitte klicken Sie auf einen Punkt in der Panoramax-Schicht, um die Fotos anzuzeigen." + ,"pt" : "Por favor, clique em um ponto da camada Panoramax para exibir as fotos." + ,"nl" : "Klik alstublieft op een punt in de Panoramax-laag om de foto's te bekijken." + ,"pl" : "Kliknij punkt na warstwie Panoramax, aby wyświetlić zdjęcia." + }; + + //Change text depending on navigator language + const DEFAULT_LANGUAGE = "en" + const NAVIGATOR_LANGUAGE = navigator.language ? navigator.language.slice(0, 2) : DEFAULT_LANGUAGE; + //IF the navigator.language is not listed in CONTENT_TEXT => switch to the DEFAULT_LANGUAGE + const POPUP_TEXT = CONTENT_TEXT[NAVIGATOR_LANGUAGE] ? CONTENT_TEXT[NAVIGATOR_LANGUAGE] : CONTENT_TEXT[DEFAULT_LANGUAGE]; + + const DEBUG_MODE = false; + + /** ******************************** + ################################### + DO NOT MODIFY BELOW THIS LINE + ################################### + ******************************** */ + + const PANORAMAX_JS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@latest/build/index.min.js'; + const PANORAMAX_CSS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@latest/build/index.min.css' + + // HTML Content + const PHOTO_VIEWER = ` + `; + + const HTML_TEMPLATE = `
+

${POPUP_TEXT}

+ ${PHOTO_VIEWER} +
`; + + const SVG_ARROW = ``; + + class LizPanoramax{ + constructor(){ + //Check if Panoramax Layer exists. If not, display an error message and exit directly + this.panoramaxLayer = this.#getPanoramaxLayer(); + this.panoramaxDockOpen = false; + + this.mapClickHandler = null; + this.panoViewerListeners = { + 'psv:view-rotated': null, + 'psv:picture-loaded': null + }; + + if(!this.panoramaxLayer) { + // check if there is a panoramax layer in the project + const error = `No Panoramax layer available. + You must add a Panoramax Layer into your QGIS project + and be sure that the PANORAMAX_QGIS_LAYER_NAME is the same as your QGIS Panoramax Layer name` + if (DEBUG_MODE) console.error(error); + this.#addLizmapDock(`

${error}

`); + return + } + + //Panoramax Layer has been found. The JS/CSS Script can be loaded + this.#loadScripts().then(success => { + if(!success){ + const error = "Panoramax external script not fully loaded" + if (DEBUG_MODE) console.error(error); + this.#addLizmapDock(`

${error}

`); + return + } + + // everything is good we can display the "normal" dock content + this.#addLizmapDock(HTML_TEMPLATE); + }); + } + + /** + * LOAD PANORAMAX EXTERNAL JS AND CSS + * @returns {Promise} + */ + async #loadScripts() { + return new Promise(resolve => { + + let scriptLoaded = !!document.querySelector(`script[src="${PANORAMAX_JS_URL}"]`); + let cssLoaded = !!document.querySelector(`link[href="${PANORAMAX_CSS_URL}"]`); + + + let jsPromise, cssPromise; + + if (!scriptLoaded) { + jsPromise = new Promise(jsResolve => { + const script = document.createElement('script'); + script.src = PANORAMAX_JS_URL; + script.type = 'text/javascript'; + script.onload = () => jsResolve(true); + script.onerror = () => { + if (DEBUG_MODE) console.error("Échec du chargement du script."); + jsResolve(false); + }; + document.head.appendChild(script); + }); + } else { + jsPromise = Promise.resolve(true); + } + + if (!cssLoaded) { + cssPromise = new Promise(cssResolve => { + const style = document.createElement("link"); + style.href = PANORAMAX_CSS_URL; + style.rel = 'stylesheet'; + style.type = 'text/css'; + style.onload = () => cssResolve(true); + style.onerror = () => { + if (DEBUG_MODE) console.error("Échec du chargement du CSS."); + cssResolve(false); + }; + document.head.appendChild(style); + }); + } else { + cssPromise = Promise.resolve(true); + } + + Promise.all([jsPromise, cssPromise]).then(results => { + // Si l'un des deux a échoué, on retourne false + resolve(results.every(Boolean)); + }); + }); + } + + /** + * Get Panoramax Layer + * The PANORAMAX_QGIS_LAYER_NAME must be the same as the one QGIS + * @returns OpenLayers Layer + */ + #getPanoramaxLayer(){ + try { + let PanoramaxLayer = lizMap.mainLizmap.state.rootMapGroup.getMapLayerByName(PANORAMAX_QGIS_LAYER_NAME); + return PanoramaxLayer; + } catch (error) { + return false + } + } + + /** + * Add Lizmap Dock + * @param {*} htmlContent + */ + #addLizmapDock(htmlContent){ + lizMap.addDock( + DOCK_ID, + DOCK_TITLE, + DOCK_POSITION, + htmlContent, + DOCK_ICON + ); + } + + /** + * add layer to draw that will be used to draw arrow direction + * @returns Openlayers Layer + */ + #addPanoramaxHeadingLayer(){ + this.layerArrowHeadingSource = new lizMap.ol.source.Vector({ wrapX: false }); + this.layerArrowHeading = new lizMap.ol.layer.Vector({ + title: 'panoramax-pov', + source: this.layerArrowHeadingSource, + style: new lizMap.ol.style.Style({ + image: new lizMap.ol.style.Icon({ + src: 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(SVG_ARROW), + scale: ARROW_ICON_SIZE + }) + }), + }); + this.layerArrowHeading.setZIndex(1001); + lizMap.mainLizmap.map.addLayer(this.layerArrowHeading); + return + } + + /** + * Set Arrow Heading depending on picture parameter + * @param {Panoramax Picture} picture + */ + #setPanoramaxHeadingLayerHeading(picture){ + // Vérifie l'existence de la couche et de sa source avant de tenter de les utiliser + if(!this.layerArrowHeadingSource || !this.layerArrowHeading) { + if (DEBUG_MODE) console.warn("Heading layer not initialized"); + return; + } + + // Valider les données + if(!picture.geometry?.coordinates || !picture.properties) { + if (DEBUG_MODE) console.warn("Invalid picture data", picture); + return; + } + + const oldFeature = this.layerArrowHeadingSource.getFeatures()?.[0]; + if (oldFeature) { + this.layerArrowHeadingSource.removeFeature(oldFeature); + } + + this.layerArrowHeadingSource.addFeature(new lizMap.ol.Feature({ + geometry: new lizMap.ol.geom.Point([ + picture.geometry.coordinates[0], + picture.geometry.coordinates[1] + ]).transform('EPSG:4326', lizMap.map.projection.projCode) + })); + const azimuth = picture.properties["view:azimuth"] ?? 0; + const r = azimuth * (Math.PI/180); + this.layerArrowHeading.getStyle().getImage().setRotation(r); + this.layerArrowHeading.changed(); + } + + /** + * Hide/Show Panoramax layer + * @param {boolean} visibility + */ + #setPanoramaxLayerVisibility(visibility){ + if(this.panoramaxLayer){ + this.panoramaxLayer.checked = visibility; + } + } + + /** + * Init all the method + */ + initPanoramaxDock(){ + if(this.panoramaxLayer && !this.panoramaxDockOpen){ + lizMap.mainLizmap.popup.active = false; //Empêche l'affichage du popup Lizmap au clic sur la carte pour éviter les conflits avec le clic pour récupérer les photos panoramax + this.panoramaxDockOpen = true; + this.#addPanoramaxHeadingLayer(); + this.#setPanoramaxLayerVisibility(true); + //Attendre que le DOM soit prêt + setTimeout(() => { + this.#addPanoramaxViewer(); + this.#addMapEvent(); + }, 100); + } + } + + /** + * Add Panoramax Viewer + */ + async #addPanoramaxViewer(){ + const viewerElement = document.querySelector('#panoramax_dock_content pnx-photo-viewer'); + if (!viewerElement) { + if (DEBUG_MODE) console.error("Viewer element not found"); + return; + } + // Attendre que le Photo Sphere Viewer soit vraiment prêt + await viewerElement.oncePSVReady(); + + // Réinitialiser le composant + this.panoViewer = viewerElement; + this.panoViewer.select(null, null, false); + this.#addPanoramaxViewerEvent(); + } + + /** + * Add all Panoramax Viewer Events + */ + #addPanoramaxViewerEvent(){ + // Stocker les listeners AVEC les bonnes fonctions + this.panoViewerListeners['psv:view-rotated'] = (e) => { + + if(e.detail?.x){ + const azimuth = e.detail.x ?? 0; // Valeur par défaut + let r = azimuth * (Math.PI/180); + this.layerArrowHeading.getStyle().getImage().setRotation(r); + this.layerArrowHeading.changed(); + } + }; + + this.panoViewerListeners['psv:picture-loaded'] = (e) => { + const azimuth = e.detail?.x ?? 0; // Valeur par défaut + let r = azimuth * (Math.PI/180); + if(this.layerArrowHeadingSource.getFeatures()[0] + && typeof e.detail?.lon === 'number' + && typeof e.detail?.lat === 'number' ){ + const coords = lizMap.ol.proj.transform([e.detail.lon, e.detail.lat], 'EPSG:4326', lizMap.mainLizmap.projection); + lizMap.mainLizmap.map.getView().setCenter(coords); + this.layerArrowHeadingSource.getFeatures()[0].getGeometry().setCoordinates(coords); + } + this.layerArrowHeading.getStyle().getImage().setRotation(r); + this.layerArrowHeading.changed(); + }; + + this.panoViewer.addEventListener('psv:view-rotated', this.panoViewerListeners['psv:view-rotated']); + this.panoViewer.addEventListener('psv:picture-loaded', this.panoViewerListeners['psv:picture-loaded']); + } + + /** + * Fetch picture on single map cick + */ + #addMapEvent(){ + // Créer une méthode nommée pour pouvoir la désabonner + this.mapClickHandler = (e) => { + //Fire event only if panoramax dock is opened + if(this.panoramaxDockOpen){ + const extent = this.#getBufferedExtent(e.coordinate); + this.#getPanoramaxPicture(extent); + } + }; + lizMap.mainLizmap.map.on('singleclick', this.mapClickHandler); + } + + /** + * Remove map click event listener + */ + #removeMapEvent(){ + if(this.mapClickHandler){ + lizMap.mainLizmap.map.un('singleclick', this.mapClickHandler); + this.mapClickHandler = null; + } + } + + /** + * Remove all Panoramax Viewer Events + */ + #removePanoramaxViewerEvent(){ + if(!this.panoViewer) return; + + Object.keys(this.panoViewerListeners).forEach(eventName => { + if(this.panoViewerListeners[eventName]){ + this.panoViewer.removeEventListener(eventName, this.panoViewerListeners[eventName]); + this.panoViewerListeners[eventName] = null; + } + }); + } + + /** + * + * @param {*} point + * @returns Array coordinates + */ + #getBufferedExtent(p){ + const point = new lizMap.ol.geom.Point(p); + const extent = point.getExtent(); + const bufferedExtent = new lizMap.ol.extent.buffer(extent,BUFFER_RADIUS); + + const pbl = new lizMap.ol.geom.Point([bufferedExtent[0], bufferedExtent[1]]); //bottom left + const pur = new lizMap.ol.geom.Point([bufferedExtent[2], bufferedExtent[3]]); //upper right + + if(lizMap.map.projection.projCode !== "EPSG:4326"){ + // reproject extent to 4326 + pbl.transform(lizMap.map.projection.projCode, 'EPSG:4326'); + pur.transform(lizMap.map.projection.projCode, 'EPSG:4326'); + } + return [pbl.flatCoordinates, + pur.flatCoordinates]; + } + + /** + * Query Panoramax API + * @param {[bl_x,bl_y,upr_x,up_y]} extent + */ + async #getPanoramaxPicture(extent){ + //URL example : https://api.panoramax.xyz/api/search?limit=1&bbox=55.500236%2C-20.892392%2C55.500238%2C-20.892390 + //Coord -20.89238774,55.50023601 + // -> Should return 9df3252f-dcad-42db-b46e-6d3e52571acb photo id + + try { + const PanoramaxSearchParams = new URLSearchParams({ + 'limit': 1 + ,'bbox':`${extent[0][0].toFixed(6)},${extent[0][1].toFixed(6)},${extent[1][0].toFixed(6)},${extent[1][1].toFixed(6)}` + }); + const response = await fetch(`${PANORAMAX_INSTANCE}/search?${PanoramaxSearchParams}`); + if (!response.ok) { + const error = `Erreur HTTP: ${response.status}`; + if (DEBUG_MODE) console.error(error); + throw new Error(error); + } + const picture = await response.json(); + if(picture?.features?.length > 0 && this.panoViewer){ + this.panoViewer?.select?.(null, picture.features[0].id, true); + this.#setPanoramaxHeadingLayerHeading(picture.features[0]); + } + } catch (error) { + if (DEBUG_MODE) console.error(error); + throw new Error(error); + } + } + + /** + * Remove all Panoramax object and instance + */ + removePanoramaxDock(){ + if(this.panoramaxLayer) { + lizMap.mainLizmap.popup.active = true; + this.panoramaxDockOpen = false; + + // Remove map click listener + this.#removeMapEvent(); + + // Remove Panoramax viewer listeners + this.#removePanoramaxViewerEvent(); + + // Clear layer used for heading arrow + this.layerArrowHeadingSource.clear(); + + //Hide layer + this.#setPanoramaxLayerVisibility(false); + + // Remove all viewer references + if (this.panoViewer) { + this.panoViewer.psv?.stopSequence?.(); + //this.panoViewer.destroy(); // --> Should be used for removing object but currently throwing an error + const panoViewer = document.querySelector('#panoramax_dock_content pnx-photo-viewer'); + if(panoViewer){ + panoViewer.remove(); + document.querySelector("#panoramax_dock_content").insertAdjacentHTML('beforeend', PHOTO_VIEWER); + } + this.panoViewer = null; + } + } + } + } + + /** + * Lizmap event + */ + let lizPanoramax; + + lizMap.events.on({ + 'uicreated': function(e) { + lizPanoramax = new LizPanoramax(); + }, + + //MINI DOCK + 'minidockopened': e => { + if (e.id === DOCK_ID && lizPanoramax) { + lizPanoramax.initPanoramaxDock(); + } + }, + 'minidockclosed': e => { + if (e.id === DOCK_ID && lizPanoramax) { + lizPanoramax.removePanoramaxDock(); + } + }, + + //DOCK + 'dockopened': e => { + if (e.id === DOCK_ID && lizPanoramax) { + lizPanoramax.initPanoramaxDock(); + } + }, + 'dockclosed': e => { + if (e.id === DOCK_ID && lizPanoramax) { + lizPanoramax.removePanoramaxDock(); + } + }, + }); + + return { + 'id': DOCK_ID, + 'title': DOCK_TITLE, + } + +}(); From b5b4acdccef480e49c6de6ead7210760becd4bd2 Mon Sep 17 00:00:00 2001 From: arno974 Date: Thu, 23 Apr 2026 09:47:14 +0400 Subject: [PATCH 3/4] feat: Add Panoramax Vector Tile integration with heading visualization - Replace QGIS dependency with IGN/OSM MVT sources - Add direction arrow layer with smooth animations - Robust CDN timeout + proper cleanup --- library/api/panoramax/panoramax4_3.9.js | 442 +++++++++++++----------- 1 file changed, 237 insertions(+), 205 deletions(-) diff --git a/library/api/panoramax/panoramax4_3.9.js b/library/api/panoramax/panoramax4_3.9.js index 2e666bd..0b5b155 100644 --- a/library/api/panoramax/panoramax4_3.9.js +++ b/library/api/panoramax/panoramax4_3.9.js @@ -15,17 +15,10 @@ const lizmapPanoramax = function() { // Dock position: can be dock, minidock const DOCK_POSITION = 'dock'; - - // BUFFER RADIUS FOR PANORAMAX SEARCH (in map units) - const BUFFER_RADIUS = 3; - + // Title of the dock const DOCK_TITLE = 'Panoramax'; - // Panoramax Vector Tile Layer in QGIS - // VERY IMPORTANT => The layer name must be the same as the one in QGIS - const PANORAMAX_QGIS_LAYER_NAME = "Panoramax" - // ARROW ICON PROPERTIES const ARROW_ICON_SIZE = 0.3; const ARROW_ICON_COLOR = "#e4e8e6"; @@ -47,8 +40,8 @@ const lizmapPanoramax = function() { const DEFAULT_LANGUAGE = "en" const NAVIGATOR_LANGUAGE = navigator.language ? navigator.language.slice(0, 2) : DEFAULT_LANGUAGE; //IF the navigator.language is not listed in CONTENT_TEXT => switch to the DEFAULT_LANGUAGE - const POPUP_TEXT = CONTENT_TEXT[NAVIGATOR_LANGUAGE] ? CONTENT_TEXT[NAVIGATOR_LANGUAGE] : CONTENT_TEXT[DEFAULT_LANGUAGE]; - + const POPUP_TEXT = CONTENT_TEXT[NAVIGATOR_LANGUAGE] || CONTENT_TEXT[DEFAULT_LANGUAGE]; + const DEBUG_MODE = false; /** ******************************** @@ -57,8 +50,8 @@ const lizmapPanoramax = function() { ################################### ******************************** */ - const PANORAMAX_JS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@latest/build/index.min.js'; - const PANORAMAX_CSS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@latest/build/index.min.css' + const PANORAMAX_JS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@4.4.0/build/index.min.js'; + const PANORAMAX_CSS_URL = 'https://cdn.jsdelivr.net/npm/@panoramax/web-viewer@4.4.0/build/index.min.css' // HTML Content const PHOTO_VIEWER = ``; + const PANORAMAX_SOURCES = { + IGN: { + url: 'https://panoramax.ign.fr/api/map/{z}/{x}/{y}.mvt', + maxZoom: 15, + }, + OSM: { + url: 'https://panoramax.openstreetmap.fr/api/map/{z}/{x}/{y}.mvt', + maxZoom: 15, + } + }; + + const PANORAMAX_LAYER_CONFIG = { + name: 'Panoramax Images', + visibleOnStartUp: false, + zIndex: 100 + }; + class LizPanoramax{ constructor(){ - //Check if Panoramax Layer exists. If not, display an error message and exit directly - this.panoramaxLayer = this.#getPanoramaxLayer(); + // Initialize state this.panoramaxDockOpen = false; - + this.panoramaxVectorLayers = null; + this.panoramaxLayersGroup = null; + + // Initialize map handlers this.mapClickHandler = null; + this.panoramaxLayerClickHandler = null; + + // Initialize viewer listeners this.panoViewerListeners = { - 'psv:view-rotated': null, - 'psv:picture-loaded': null + 'psv:view-rotated': null }; - if(!this.panoramaxLayer) { - // check if there is a panoramax layer in the project - const error = `No Panoramax layer available. - You must add a Panoramax Layer into your QGIS project - and be sure that the PANORAMAX_QGIS_LAYER_NAME is the same as your QGIS Panoramax Layer name` - if (DEBUG_MODE) console.error(error); - this.#addLizmapDock(`

${error}

`); - return - } - - //Panoramax Layer has been found. The JS/CSS Script can be loaded + // Load external scripts (Panoramax viewer) this.#loadScripts().then(success => { if(!success){ - const error = "Panoramax external script not fully loaded" + const error = "Panoramax external script not fully loaded"; if (DEBUG_MODE) console.error(error); this.#addLizmapDock(`

${error}

`); - return + return; } - // everything is good we can display the "normal" dock content - this.#addLizmapDock(HTML_TEMPLATE); - }); + // Scripts loaded successfully, display dock content + this.#addLizmapDock(HTML_TEMPLATE); + + // Create Vector Tile layers (no longer dependent on QGIS) + if (!this.panoramaxVectorLayers) { + this.panoramaxVectorLayers = this.#addPanoramaxVectorLayers(); + + if (DEBUG_MODE) { + console.log('Panoramax Vector Tile layers created and registered'); + } + } + }); } /** @@ -121,7 +134,6 @@ const lizmapPanoramax = function() { let scriptLoaded = !!document.querySelector(`script[src="${PANORAMAX_JS_URL}"]`); let cssLoaded = !!document.querySelector(`link[href="${PANORAMAX_CSS_URL}"]`); - let jsPromise, cssPromise; if (!scriptLoaded) { @@ -157,27 +169,20 @@ const lizmapPanoramax = function() { cssPromise = Promise.resolve(true); } - Promise.all([jsPromise, cssPromise]).then(results => { - // Si l'un des deux a échoué, on retourne false + Promise.race([ + Promise.all([jsPromise, cssPromise]), + new Promise((_, reject) => + setTimeout(() => reject(new Error('CDN timeout')), 10000) + ) + ]).then(results => { resolve(results.every(Boolean)); - }); + }).catch(error => { + if (DEBUG_MODE) console.error('Script loading failed:', error); + resolve(false); + }); }); } - - /** - * Get Panoramax Layer - * The PANORAMAX_QGIS_LAYER_NAME must be the same as the one QGIS - * @returns OpenLayers Layer - */ - #getPanoramaxLayer(){ - try { - let PanoramaxLayer = lizMap.mainLizmap.state.rootMapGroup.getMapLayerByName(PANORAMAX_QGIS_LAYER_NAME); - return PanoramaxLayer; - } catch (error) { - return false - } - } - + /** * Add Lizmap Dock * @param {*} htmlContent @@ -192,6 +197,116 @@ const lizmapPanoramax = function() { ); } + /** + * Create and add Panoramax Vector Tile layers to the map + * @returns {Array} Array of panoramax layers + */ + #addPanoramaxVectorLayers() { + const layers = []; + + Object.entries(PANORAMAX_SOURCES).forEach(([key, config]) => { + + // Create VectorTile source + const source = new lizMap.ol.source.VectorTile({ + url: config.url, + format: new lizMap.ol.format.MVT(), + maxZoom: config.maxZoom, + tileGridStrategy: 'all' + }); + + // Create VectorTile layer + const layer = new lizMap.ol.layer.VectorTile({ + title: `${PANORAMAX_LAYER_CONFIG}.name ${key}`, + source: source, + zIndex: PANORAMAX_LAYER_CONFIG.zIndex, + style: this.#setPanoramaxLayerStyle(), + projection: 'EPSG:3857' + }); + + lizMap.mainLizmap.map.addLayer(layer); + layer.setVisible(PANORAMAX_LAYER_CONFIG.visibleOnStartUp); + layers.push(layer); + }); + return layers; + } + + /** + * Define styling for Panoramax layers + * NOTE : COULD BE PASSED AS PARAMETERS FOR THE NEXT VERSION + * @returns {Function} Style function + */ + #setPanoramaxLayerStyle() { + // Default style + const fill = new lizMap.ol.style.Fill({ + color: 'rgba(255,255,255,0.4)', + }); + const stroke = new lizMap.ol.style.Stroke({ + color: '#3399CC', + width: 1.25, + }); + const style = new lizMap.ol.style.Style({ + image: new lizMap.ol.style.Circle({ + fill: fill, + stroke: stroke, + radius: 5, + }), + fill: fill, + stroke: stroke, + }); + return style; + } + + /** + * Toggle Panoramax layers visibility + * @param {boolean} visible + */ + #setPanoramaxLayersVisibility(visible) { + if (this.panoramaxVectorLayers) { + this.panoramaxVectorLayers.forEach((layer) => { + layer.setVisible(visible); + }); + } + } + + /** + * Add click handler to Panoramax vector tile layers + */ + #addPanoramaxLayerClickEvent() { + this.panoramaxLayerClickHandler = (e) => { + if (!this.panoramaxDockOpen) return; + + const features = lizMap.mainLizmap.map.getFeaturesAtPixel(e.pixel, + function (feature) { + return feature; + },{ + layerFilter: (layer) => { + return this.panoramaxVectorLayers && + this.panoramaxVectorLayers.some(({ layer: l }) => l === layer); + } + }); + + if (features.length > 0) { + const feature = features[0]; + if(feature.getProperties()?.id){ + const pictureId = feature.getProperties().id; + this.panoViewer?.select?.(null, pictureId, true); + this.#setPanoramaxHeadingLayer(feature); + } + } + }; + lizMap.mainLizmap.map.on('singleclick', this.panoramaxLayerClickHandler); + } + + /** + * Remove Vector Tile layer click handler + */ + #removePanoramaxLayerClickEvent() { + if (this.panoramaxLayerClickHandler) { + lizMap.mainLizmap.map.un('singleclick', this.panoramaxLayerClickHandler); + this.panoramaxLayerClickHandler = null; + } + } + /** * add layer to draw that will be used to draw arrow direction * @returns Openlayers Layer @@ -210,66 +325,77 @@ const lizmapPanoramax = function() { }); this.layerArrowHeading.setZIndex(1001); lizMap.mainLizmap.map.addLayer(this.layerArrowHeading); - return + return this.layerArrowHeading } /** * Set Arrow Heading depending on picture parameter * @param {Panoramax Picture} picture */ - #setPanoramaxHeadingLayerHeading(picture){ + #setPanoramaxHeadingLayer(picFeature){ // Vérifie l'existence de la couche et de sa source avant de tenter de les utiliser if(!this.layerArrowHeadingSource || !this.layerArrowHeading) { if (DEBUG_MODE) console.warn("Heading layer not initialized"); return; - } - - // Valider les données - if(!picture.geometry?.coordinates || !picture.properties) { - if (DEBUG_MODE) console.warn("Invalid picture data", picture); + } + + if (!picFeature?.flatCoordinates_ || !Array.isArray(picFeature.flatCoordinates_) || picFeature.flatCoordinates_.length != 2){ + if (DEBUG_MODE) console.warn("Wrong picture coordinates", picFeature.flatCoordinates_); return; } + + // Accès à propriété privée OpenLayers + // Il doit y avoir un moyen plus simple d'accéder aux coordonnées + // ex. getCoordinates() -> mais retourne une erreur + const [picLon, picLat] = picFeature.flatCoordinates_; const oldFeature = this.layerArrowHeadingSource.getFeatures()?.[0]; if (oldFeature) { this.layerArrowHeadingSource.removeFeature(oldFeature); } - + this.layerArrowHeadingSource.addFeature(new lizMap.ol.Feature({ - geometry: new lizMap.ol.geom.Point([ - picture.geometry.coordinates[0], - picture.geometry.coordinates[1] - ]).transform('EPSG:4326', lizMap.map.projection.projCode) + geometry: new lizMap.ol.geom.Point([picLon, picLat]) })); - const azimuth = picture.properties["view:azimuth"] ?? 0; + + lizMap.mainLizmap.map.getView().animate({ + center: [picLon, picLat], + duration: 750, + }); + + const azimuth = picFeature.getProperties?.()?.heading ?? 0; const r = azimuth * (Math.PI/180); this.layerArrowHeading.getStyle().getImage().setRotation(r); this.layerArrowHeading.changed(); } - /** - * Hide/Show Panoramax layer - * @param {boolean} visibility - */ - #setPanoramaxLayerVisibility(visibility){ - if(this.panoramaxLayer){ - this.panoramaxLayer.checked = visibility; - } - } /** * Init all the method */ initPanoramaxDock(){ - if(this.panoramaxLayer && !this.panoramaxDockOpen){ - lizMap.mainLizmap.popup.active = false; //Empêche l'affichage du popup Lizmap au clic sur la carte pour éviter les conflits avec le clic pour récupérer les photos panoramax - this.panoramaxDockOpen = true; - this.#addPanoramaxHeadingLayer(); - this.#setPanoramaxLayerVisibility(true); - //Attendre que le DOM soit prêt + if(!this.panoramaxDockOpen){ + // Vérifier que popup existe avant d'y accéder + if (lizMap?.mainLizmap?.popup) { + lizMap.mainLizmap.popup.active = false; + } + this.panoramaxDockOpen = true; + + // Charger et enregistrer les couches VectorTile + if (!this.panoramaxVectorLayers) { + this.panoramaxVectorLayers = this.#addPanoramaxVectorLayers(); + } + + // Afficher les couches + this.#setPanoramaxLayersVisibility(true); + + // Ajouter les couches flèche de direction + this.#addPanoramaxHeadingLayer(); + + // Attendre que le DOM soit prêt setTimeout(() => { this.#addPanoramaxViewer(); - this.#addMapEvent(); + this.#addPanoramaxLayerClickEvent(); }, 100); } } @@ -284,8 +410,13 @@ const lizmapPanoramax = function() { return; } // Attendre que le Photo Sphere Viewer soit vraiment prêt - await viewerElement.oncePSVReady(); - + try { + await viewerElement.oncePSVReady(); + } catch (error) { + console.error('Viewer initialization failed:', error); + return; // ou afficher message utilisateur + } + // Réinitialiser le composant this.panoViewer = viewerElement; this.panoViewer.select(null, null, false); @@ -296,9 +427,7 @@ const lizmapPanoramax = function() { * Add all Panoramax Viewer Events */ #addPanoramaxViewerEvent(){ - // Stocker les listeners AVEC les bonnes fonctions - this.panoViewerListeners['psv:view-rotated'] = (e) => { - + this.panoViewerListeners['psv:view-rotated'] = (e) => { if(e.detail?.x){ const azimuth = e.detail.x ?? 0; // Valeur par défaut let r = azimuth * (Math.PI/180); @@ -306,49 +435,9 @@ const lizmapPanoramax = function() { this.layerArrowHeading.changed(); } }; - - this.panoViewerListeners['psv:picture-loaded'] = (e) => { - const azimuth = e.detail?.x ?? 0; // Valeur par défaut - let r = azimuth * (Math.PI/180); - if(this.layerArrowHeadingSource.getFeatures()[0] - && typeof e.detail?.lon === 'number' - && typeof e.detail?.lat === 'number' ){ - const coords = lizMap.ol.proj.transform([e.detail.lon, e.detail.lat], 'EPSG:4326', lizMap.mainLizmap.projection); - lizMap.mainLizmap.map.getView().setCenter(coords); - this.layerArrowHeadingSource.getFeatures()[0].getGeometry().setCoordinates(coords); - } - this.layerArrowHeading.getStyle().getImage().setRotation(r); - this.layerArrowHeading.changed(); - }; - this.panoViewer.addEventListener('psv:view-rotated', this.panoViewerListeners['psv:view-rotated']); - this.panoViewer.addEventListener('psv:picture-loaded', this.panoViewerListeners['psv:picture-loaded']); } - /** - * Fetch picture on single map cick - */ - #addMapEvent(){ - // Créer une méthode nommée pour pouvoir la désabonner - this.mapClickHandler = (e) => { - //Fire event only if panoramax dock is opened - if(this.panoramaxDockOpen){ - const extent = this.#getBufferedExtent(e.coordinate); - this.#getPanoramaxPicture(extent); - } - }; - lizMap.mainLizmap.map.on('singleclick', this.mapClickHandler); - } - - /** - * Remove map click event listener - */ - #removeMapEvent(){ - if(this.mapClickHandler){ - lizMap.mainLizmap.map.un('singleclick', this.mapClickHandler); - this.mapClickHandler = null; - } - } /** * Remove all Panoramax Viewer Events @@ -362,92 +451,35 @@ const lizmapPanoramax = function() { this.panoViewerListeners[eventName] = null; } }); - } - - /** - * - * @param {*} point - * @returns Array coordinates - */ - #getBufferedExtent(p){ - const point = new lizMap.ol.geom.Point(p); - const extent = point.getExtent(); - const bufferedExtent = new lizMap.ol.extent.buffer(extent,BUFFER_RADIUS); - - const pbl = new lizMap.ol.geom.Point([bufferedExtent[0], bufferedExtent[1]]); //bottom left - const pur = new lizMap.ol.geom.Point([bufferedExtent[2], bufferedExtent[3]]); //upper right - - if(lizMap.map.projection.projCode !== "EPSG:4326"){ - // reproject extent to 4326 - pbl.transform(lizMap.map.projection.projCode, 'EPSG:4326'); - pur.transform(lizMap.map.projection.projCode, 'EPSG:4326'); - } - return [pbl.flatCoordinates, - pur.flatCoordinates]; - } - - /** - * Query Panoramax API - * @param {[bl_x,bl_y,upr_x,up_y]} extent - */ - async #getPanoramaxPicture(extent){ - //URL example : https://api.panoramax.xyz/api/search?limit=1&bbox=55.500236%2C-20.892392%2C55.500238%2C-20.892390 - //Coord -20.89238774,55.50023601 - // -> Should return 9df3252f-dcad-42db-b46e-6d3e52571acb photo id - - try { - const PanoramaxSearchParams = new URLSearchParams({ - 'limit': 1 - ,'bbox':`${extent[0][0].toFixed(6)},${extent[0][1].toFixed(6)},${extent[1][0].toFixed(6)},${extent[1][1].toFixed(6)}` - }); - const response = await fetch(`${PANORAMAX_INSTANCE}/search?${PanoramaxSearchParams}`); - if (!response.ok) { - const error = `Erreur HTTP: ${response.status}`; - if (DEBUG_MODE) console.error(error); - throw new Error(error); - } - const picture = await response.json(); - if(picture?.features?.length > 0 && this.panoViewer){ - this.panoViewer?.select?.(null, picture.features[0].id, true); - this.#setPanoramaxHeadingLayerHeading(picture.features[0]); - } - } catch (error) { - if (DEBUG_MODE) console.error(error); - throw new Error(error); - } - } + } + /** * Remove all Panoramax object and instance */ removePanoramaxDock(){ - if(this.panoramaxLayer) { - lizMap.mainLizmap.popup.active = true; - this.panoramaxDockOpen = false; + this.panoramaxDockOpen = false; + lizMap.mainLizmap.popup.active = true; - // Remove map click listener - this.#removeMapEvent(); - - // Remove Panoramax viewer listeners - this.#removePanoramaxViewerEvent(); - - // Clear layer used for heading arrow - this.layerArrowHeadingSource.clear(); + // Remove all event listeners + this.#removePanoramaxLayerClickEvent(); + this.#removePanoramaxViewerEvent(); - //Hide layer - this.#setPanoramaxLayerVisibility(false); - - // Remove all viewer references - if (this.panoViewer) { - this.panoViewer.psv?.stopSequence?.(); - //this.panoViewer.destroy(); // --> Should be used for removing object but currently throwing an error - const panoViewer = document.querySelector('#panoramax_dock_content pnx-photo-viewer'); - if(panoViewer){ - panoViewer.remove(); - document.querySelector("#panoramax_dock_content").insertAdjacentHTML('beforeend', PHOTO_VIEWER); - } - this.panoViewer = null; + // Clear heading layer + this.layerArrowHeadingSource.clear(); + + // Hide Vector Tile layers + this.#setPanoramaxLayersVisibility(false); + + // Cleanup Panoramax viewer + if (this.panoViewer) { + this.panoViewer.psv?.stopSequence?.(); + const panoViewer = document.querySelector('#panoramax_dock_content pnx-photo-viewer'); + if(panoViewer){ + panoViewer.remove(); + document.querySelector("#panoramax_dock_content").insertAdjacentHTML('beforeend', PHOTO_VIEWER); } + this.panoViewer = null; } } } @@ -492,4 +524,4 @@ const lizmapPanoramax = function() { 'title': DOCK_TITLE, } -}(); +}(); \ No newline at end of file From 684a9cbf22ad7a1b4676286b2122c2efcdf47687 Mon Sep 17 00:00:00 2001 From: arno974 Date: Fri, 15 May 2026 15:32:31 +0400 Subject: [PATCH 4/4] feat: ajout pnx-widget-player et suivi de position sur la carte --- library/api/panoramax/panoramax4_3.9.js | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/library/api/panoramax/panoramax4_3.9.js b/library/api/panoramax/panoramax4_3.9.js index 0b5b155..2c5a681 100644 --- a/library/api/panoramax/panoramax4_3.9.js +++ b/library/api/panoramax/panoramax4_3.9.js @@ -57,6 +57,7 @@ const lizmapPanoramax = function() { const PHOTO_VIEWER = ` `; @@ -98,7 +99,8 @@ const lizmapPanoramax = function() { // Initialize viewer listeners this.panoViewerListeners = { - 'psv:view-rotated': null + 'psv:view-rotated': null, + 'psv:picture-loaded': null }; // Load external scripts (Panoramax viewer) @@ -419,7 +421,13 @@ const lizmapPanoramax = function() { // Réinitialiser le composant this.panoViewer = viewerElement; - this.panoViewer.select(null, null, false); + this.panoViewer.select(null, null, false); + + const player = document.createElement('pnx-widget-player'); + player._parent = viewerElement; + player.setAttribute('size', 'md'); + viewerElement.insertAdjacentElement('afterend', player); + this.#addPanoramaxViewerEvent(); } @@ -435,6 +443,25 @@ const lizmapPanoramax = function() { this.layerArrowHeading.changed(); } }; + + this.panoViewerListeners['psv:picture-loaded'] = (e) => { + if (!this.layerArrowHeadingSource || !this.layerArrowHeading) return; + + const azimuth = e.detail.x ?? 0; // Valeur par défaut + let r = azimuth * (Math.PI/180); + const feature = this.layerArrowHeadingSource.getFeatures()[0]; + if (feature + && typeof e.detail?.lon === 'number' + && typeof e.detail?.lat === 'number') { + const coords = lizMap.ol.proj.transform([e.detail.lon, e.detail.lat], 'EPSG:4326', lizMap.mainLizmap.projection); + lizMap.mainLizmap.map.getView().animate({ center: coords, duration: 750 }); + feature.getGeometry().setCoordinates(coords); + } + this.layerArrowHeading.getStyle().getImage().setRotation(r); + this.layerArrowHeading.changed(); + }; + + this.panoViewer.addEventListener('psv:picture-loaded', this.panoViewerListeners['psv:picture-loaded']); this.panoViewer.addEventListener('psv:view-rotated', this.panoViewerListeners['psv:view-rotated']); }