diff --git a/package.json b/package.json
index 5ad434495a..637b2042c5 100644
--- a/package.json
+++ b/package.json
@@ -114,6 +114,7 @@
"@mapbox/geojsonhint": "3.3.0",
"@mapbox/togeojson": "0.16.2",
"@mapstore/patcher": "https://github.com/geosolutions-it/Patcher/tarball/master",
+ "@panoramax/web-viewer": "4.2.0",
"@turf/along": "6.5.0",
"@turf/area": "6.5.0",
"@turf/bbox": "4.1.0",
diff --git a/web/client/components/map/openlayers/Map.jsx b/web/client/components/map/openlayers/Map.jsx
index 069c8f51b2..0feabef51b 100644
--- a/web/client/components/map/openlayers/Map.jsx
+++ b/web/client/components/map/openlayers/Map.jsx
@@ -13,6 +13,7 @@ import View from 'ol/View';
import { get as getProjection, toLonLat } from 'ol/proj';
import Zoom from 'ol/control/Zoom';
import GeoJSON from 'ol/format/GeoJSON';
+import {LineString, MultiLineString, MultiPoint, MultiPolygon, Polygon, Point} from "ol/geom";
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4.js';
@@ -32,6 +33,8 @@ import 'ol/ol.css';
// add overrides for css
import './mapstore-ol-overrides.css';
+import Feature from "ol/Feature";
+import RenderFeature from "ol/render/Feature";
const geoJSONFormat = new GeoJSON();
@@ -399,11 +402,49 @@ class OpenlayersMap extends React.Component {
return view.getProjection().getExtent() || msGetProjection(props.projection).extent;
};
+ /**
+ * Compute the OLGeometry from a RenderedGeometry
+ * @param geomLike
+ * @returns {Point|MultiPoint|null|MultiLineString|LineString|*|Polygon|MultiPolygon}
+ */
+ renderGeometryToOLGeometry = (geomLike) => {
+ if (!geomLike) return null;
+ // If it is a OLGeom, we return it as it is
+ if (typeof geomLike.clone === 'function') {
+ return geomLike;
+ }
+ const type = geomLike.getType?.();
+ const coords = geomLike.getFlatCoordinates?.();
+ if (!type || !coords) return null;
+
+ switch (type) {
+ case 'Point': return new Point(coords);
+ case 'MultiPoint': return new MultiPoint([coords]);
+ case 'LineString': return new LineString(coords);
+ case 'MultiLineString': return new MultiLineString([coords]);
+ case 'Polygon': return new Polygon(coords);
+ case 'MultiPolygon': return new MultiPolygon([coords]);
+ default: return null; // types not supported
+ }
+ };
+
getIntersectedFeatures = (map, pixel) => {
let groupIntersectedFeatures = {};
map.forEachFeatureAtPixel(pixel, (feature, layer) => {
if (layer?.get('msId')) {
- const geoJSONFeature = geoJSONFormat.writeFeatureObject(feature, {
+ let olFeature = feature;
+ // Transform RenderFeature to an olFeature
+ // It is necessary to compute intersected features
+ // The MVT features are of type RenderFeature
+ if (feature instanceof RenderFeature) {
+ const geometry = this.renderGeometryToOLGeometry(feature.getGeometry());
+ // If null, not supported cause we can't compute intersects
+ if (!geometry) return null;
+ olFeature = new Feature(geometry);
+ olFeature.setProperties(feature.getProperties());
+ }
+
+ const geoJSONFeature = geoJSONFormat.writeFeatureObject(olFeature, {
featureProjection: this.props.projection,
dataProjection: 'EPSG:4326'
});
@@ -411,6 +452,7 @@ class OpenlayersMap extends React.Component {
? [ ...groupIntersectedFeatures[layer.get('msId')], geoJSONFeature ]
: [ geoJSONFeature ];
}
+ return null;
});
const intersectedFeatures = Object.keys(groupIntersectedFeatures).map(id => ({ id, features: groupIntersectedFeatures[id] }));
return intersectedFeatures;
diff --git a/web/client/components/map/openlayers/plugins/TileProviderLayer.js b/web/client/components/map/openlayers/plugins/TileProviderLayer.js
index 34d6c4bda6..9e770f040e 100644
--- a/web/client/components/map/openlayers/plugins/TileProviderLayer.js
+++ b/web/client/components/map/openlayers/plugins/TileProviderLayer.js
@@ -9,11 +9,16 @@ import Layers from '../../../../utils/openlayers/Layers';
import TileProvider from '../../../../utils/TileConfigProvider';
import CoordinatesUtils from '../../../../utils/CoordinatesUtils';
import { getUrls, template } from '../../../../utils/TileProviderUtils';
+import VectorTileLayer from 'ol/layer/VectorTile';
+import VectorTileSource from 'ol/source/VectorTile';
+import MVT from 'ol/format/MVT';
import XYZ from 'ol/source/XYZ';
import TileLayer from 'ol/layer/Tile';
import axios from 'axios';
import { getCredentials } from '../../../../utils/SecurityUtils';
import { isEqual } from 'lodash';
+import {applyDefaultStyleToVectorLayer} from '../../../../utils/StyleUtils';
+import {getStyle} from '../VectorStyle';
function lBoundsToOlExtent(bounds, destPrj) {
var [ [ miny, minx], [ maxy, maxx ] ] = bounds;
return CoordinatesUtils.reprojectBbox([minx, miny, maxx, maxy], 'EPSG:4326', CoordinatesUtils.normalizeSRS(destPrj));
@@ -58,9 +63,51 @@ function tileXYZToOpenlayersOptions(options) {
}
Layers.registerType('tileprovider', {
- create: (options) => {
+ create: (options, map) => {
let [url, opt] = TileProvider.getLayerConfig(options.provider, options);
opt.url = url;
+ const isMVT = options.format === 'application/vnd.mapbox-vector-tile';
+ // specific case of mvt layers
+ if (isMVT) {
+ const source = new VectorTileSource({
+ format: new MVT({}),
+ url: options.url,
+ maxZoom: options.maximumLevel ?? 22,
+ minZoom: options.minimumLevel ?? 0
+ });
+
+ const layer = new VectorTileLayer({
+ msId: options.id,
+ source,
+ visible: options.visibility !== false,
+ zIndex: options.zIndex,
+ opacity: options.opacity,
+ declutter: options.declutter ?? true,
+ preload: options.preload ?? 0,
+ cacheSize: options.cacheSize ?? 256,
+ tilePixelRatio: options.tilePixelRatio ?? 1,
+ renderBuffer: options.renderBuffer ?? 100,
+ renderMode: options.renderMode ?? 'hybrid' // or vector
+ });
+ // MapStore Style (GeoStyler) if supported, otherwise Openlayers style
+ if (options.style) {
+ getStyle(applyDefaultStyleToVectorLayer({ ...options, asPromise: true }))
+ .then((style) => {
+ if (style) {
+ if (style.__geoStylerStyle) {
+ style({ map }).then((olStyle) => layer.setStyle(olStyle));
+ } else {
+ layer.setStyle(style); // OL style (function/Style)
+ }
+ }
+ });
+ } else if (options.olStyle) {
+ layer.setStyle(options.olStyle); // OL style directly set
+ }
+
+ return layer;
+ }
+ // other cases keep working the same way
return new TileLayer(tileXYZToOpenlayersOptions(opt));
},
update: (layer, newOptions, oldOptions) => {
diff --git a/web/client/plugins/StreetView/StreetView.jsx b/web/client/plugins/StreetView/StreetView.jsx
index 0cda4d1b43..52d2d9de48 100644
--- a/web/client/plugins/StreetView/StreetView.jsx
+++ b/web/client/plugins/StreetView/StreetView.jsx
@@ -51,7 +51,7 @@ const StreetViewPluginContainer = connect(() => ({}), {
* StreetView Plugin. Uses Google Street View services to provide the navigation of Google panoramic photos of street view service through the map.
* @name StreetView
* @memberof plugins
- * @property {string} provider the Street View provider. Can be `google`, `cyclomedia` or `mapillary`. It is set to `google` by default.
+ * @property {string} provider the Street View provider. Can be `google`, `cyclomedia`, `mapillary` or `panoramax`. It is set to `google` by default.
* @property {string} cfg.apiKey The API key to use. This is generically valid for all the providers.
* It is Mandatory in production. Depending on the provider, this can be also configured globally:
* - `google` provider: In order to allow fine billing strategies (with different API keys), the API key can be defined and customized here in this configuration option or in `localConfig.json` with the following order of priority:
@@ -115,6 +115,23 @@ const StreetViewPluginContainer = connect(() => ({}), {
* - `initOptions.doOAuthLogoutOnDestroy` (optional). If true, the plugin will logout from the StreetSmart API when the plugin is destroyed. Default: `false`.
* - `mapillary` provider:
* - `providerSettings.ApiURL` The URL of the the custom Geojson endpoint API. Currently is only supported a custom GeoJSON format. Example of endpoint is `https://hostname/directory-with-images/`, ensure the directory contains all the images and the index.json (GeoJSON) file
+ * - `panoramax` provider:
+ * Here an example, and below the details for every property:
+ * ```json
+ * {
+ * "provider": "panoramax",
+ * "providerSettings": {
+ * "srs": "EPSG:4326",
+ * "PanoramaxApiURL": "https://api.panoramax.xyz/api"
+ * "minimumLevel": 0,
+ * "maximumLevel": 15
+ * }
+ * ```
+ * - `providerSettings` (optional). The settings specific for the provider. It is an object with the following properties:
+ * - `providerSettings.PanoramaxApiURL` (optional). The URL of the Panoramax API. Default: `https://api.panoramax.xyz/api`.
+ * - `providerSettings.srs` (optional). Coordinate reference system code to use for the API. Default: `EPSG:4326`. Note that the SRS used here must be supported by the Panoramax API **and** defined in `localConfig.json` file, in `projectionDefs`. This param is not used yet, panoramax api is implemented to receive coordinates only in SRS EPSG:4326
+ * - `providerSettings.minimumLevel` The minimum zoom level at which the provider can provide tiles. Default value is 0
+ * - `providerSettings.maximumLevel` The maximul zoom level at which the provider can provide tiles Default value is 15 according to the openstreetmap and IGN Panoramax instances
* Generally speaking, you should prefer general settings in `localConfig.json` over the plugin configuration, in order to reuse the same configuration for default viewer and all the contexts, automatically. This way you will not need to configure the `apiKey` in every context.
*
**Important**: You can use only **one** API-key for a MapStore instance. The api-key can be configured replicated in every plugin configuration or using one of the unique global settings (suggested) in `localConfig.json`). @see {@link https://github.com/googlemaps/js-api-loader/issues/5|here} and @see {@link https://github.com/googlemaps/js-api-loader/issues/100|here}
* @property {boolean} [cfg.useDataLayer=true] If true, adds to the map a layer for street view data availability when the plugin is turned on.
diff --git a/web/client/plugins/StreetView/api/index.js b/web/client/plugins/StreetView/api/index.js
index 298dfac0cc..4ea853b0e4 100644
--- a/web/client/plugins/StreetView/api/index.js
+++ b/web/client/plugins/StreetView/api/index.js
@@ -1,12 +1,14 @@
import * as google from '../api/google';
import * as cyclomedia from '../api/cyclomedia';
import * as mapillary from '../api/mapillary';
+import * as panoramax from '../api/panoramax';
/**
* Street view APIs
* @prop google google street view API
* @prop cyclomedia cyclomedia street view API
* @prop mapillary mapillary street view API
+ * @prop panoramax panoramax street view API
* Each API has the following methods:
* - `getAPI()`: returns the API object (specific to the provider)
* - `loadAPI(apiKey)`: loads the API and returns a `Promise` that resolves when the API is loaded. Takes an `apiKey` as argument (depending on the provider)
@@ -21,5 +23,6 @@ import * as mapillary from '../api/mapillary';
export default {
google,
cyclomedia,
- mapillary
+ mapillary,
+ panoramax
};
diff --git a/web/client/plugins/StreetView/api/panoramax.js b/web/client/plugins/StreetView/api/panoramax.js
new file mode 100644
index 0000000000..024b28addb
--- /dev/null
+++ b/web/client/plugins/StreetView/api/panoramax.js
@@ -0,0 +1,112 @@
+import axios from 'axios';
+import {PANORAMAX_DEFAULT_API_URL} from '../constants';
+
+const DEFAULT_SRS = 'EPSG:4326';
+
+/**
+ * Load the panoramax API. Does nothing for now but written to respect the same interface as the others plugins.
+ * If would be helpful to load the panoramax library but we loaded it dynamcally via npm package
+ * @returns {Promise}
+ */
+export const loadAPI = () => Promise.resolve();
+
+/**
+ * Get the panoramax API
+ */
+export const getAPI = () => {};
+
+/**
+ * Compute the bbox around the clicked point
+ * @param lat latitude
+ * @param lng longitude
+ * @returns {`${number},${number},${number},${number}`}
+ */
+const getBbox = (lat, lng) => {
+ // We reduce the search perimeter to a bbox of 20 meters around the clicked point
+ // We assume the geometry coordinates are in EPSG:4326 projection as expected by default by panoramax
+ const rayonMetres = 10; // 10m on each side = 20m wide
+
+ // Latitude conversion factor
+ const metresParDegreLat = 111320;
+
+ // 1. Calculation of Delta Lat (constant)
+ const deltaLat = rayonMetres / metresParDegreLat;
+
+ // 2. Calculation of Delta Lng (variable depending on latitude)
+ const cosLat = Math.cos(lat * Math.PI / 180); // Convertir lat en radians
+ const deltaLng = rayonMetres / (metresParDegreLat * cosLat);
+
+ // Bbox construction
+ return `${lng - deltaLng},${lat - deltaLat},${lng + deltaLng},${lat + deltaLat}`;
+};
+
+/**
+ * Search the feature properties at the clicked point
+ * @param point The coordinates of the clicked point
+ * @param providerSettings The provider settings
+ * @returns {Promise}
+ */
+export const getLocation = (point, providerSettings = {}) => {
+ return new Promise((resolve, reject) => {
+ if (point?.intersectedFeatures?.length) {
+
+ // We are interested only in picture features (with id and sequence_id)
+ // At a certain zoom level (when the points are not displayed on the map but the lines) we encounter more sequence features instead of picture features
+ const feature = point.intersectedFeatures[0].features.find(ft => ft.properties.first_sequence && ft.properties.id);
+ if (feature) {
+ resolve({
+ latLng: { lat: point.latlng.lat, lng: point.latlng.lng, h: 0 },
+ // sequence_id and id will be passed to the panoramax viewer to get the corresponding picture
+ properties: {id: feature.properties.id, sequence_id: feature.properties.first_sequence}
+ });
+ return;
+ }
+ }
+
+ if (point) {
+ // In case where no picture feature is found
+ // We directly ask the API to get one feature near that point
+ // Panoramax config params
+ const apiUrl = providerSettings.PanoramaxApiURL || PANORAMAX_DEFAULT_API_URL;
+
+ // Clic coordinates
+ const { lat, lng } = point.latlng;
+
+ // TODO Implements transformation to the specified srs (can be get from the providerSettings),
+ // Now panoramax API only uses EPSG:4326 as coordinates projection
+
+ // We calculate the bbox by constructing a rectangle with a radius of 10 m around the clicked point.
+ const bbox = getBbox(lat, lng);
+
+ // The features will be filtered by their proximity to the clic position on for those located at a distance of 0 to 10m.
+ // By passing a bbox, the features are sorted by their proximity to the center of the bbox
+ // The param limit=1 is set to return only the closest feature to the center of the bbox
+ axios.get(`${apiUrl}/search`, {
+ params: {
+ bbox: bbox,
+ limit: 1
+ }
+ }).then(response => {
+ // For now, the response features are of type Point only, so we assume that we receive points features as responses
+ const features = response.data?.features;
+ if (features && features.length > 0) {
+ const feature = features[0];
+ const [fLng, fLat] = feature.geometry.coordinates;
+ resolve({
+ latLng: { lat: fLat, lng: fLng, h: 0 },
+ // sequence_id and id will be passed to the panoramax viewer to get the corresponding picture
+ properties: {id: feature.id, sequence_id: feature.collection}
+ });
+ } else {
+ reject({ code: "ZERO_RESULTS" });
+ }
+ }).catch(e => {
+ console.error(e);
+ reject({ code: "ZERO_RESULTS" });
+ });
+ return;
+ }
+
+ reject({ code: "ZERO_RESULTS" });
+ });
+};
diff --git a/web/client/plugins/StreetView/components/PanoramaxView/PanoramaxView.jsx b/web/client/plugins/StreetView/components/PanoramaxView/PanoramaxView.jsx
new file mode 100644
index 0000000000..cd9195faaa
--- /dev/null
+++ b/web/client/plugins/StreetView/components/PanoramaxView/PanoramaxView.jsx
@@ -0,0 +1,107 @@
+import React, { useEffect, useRef } from 'react';
+import '@panoramax/web-viewer';
+// Import of viewer's styles
+import '@panoramax/web-viewer/build/index.css';
+import '@photo-sphere-viewer/core/index.css';
+import '@photo-sphere-viewer/markers-plugin/index.css';
+import '@photo-sphere-viewer/virtual-tour-plugin/index.css';
+import Message from "../../../locale/Message";
+import {PANORAMAX_DEFAULT_API_URL} from "../../constants";
+
+const PanoramaxView = (
+ {
+ style,
+ location,
+ providerSettings = {},
+ setLocation,
+ setPov
+ }) => {
+ const viewerRef = useRef(null);
+ const endpoint = providerSettings.PanoramaxApiURL || PANORAMAX_DEFAULT_API_URL;
+
+ // The photo ID is required
+ const pictureId = location?.properties?.id;
+ // The sequence ID is optional but recommended if available
+ // GeoVisio often returns "sequence_id" or it can be found in the links
+ const sequenceId = location?.properties.sequence_id || location?.properties?.sequence || undefined;
+
+ useEffect(() => {
+ const element = viewerRef.current;
+ if (!element) return () => null;
+
+ const handleReady = () => {
+ const psv = element.psv;
+ if (!psv) return;
+
+ // Update of Point Of View (Angle/POV)
+ const onRotate = (e) => {
+ if (setPov && e.detail) {
+ // e.detail.x correspond to heading (0-360°)
+ // e.detail.y correspond to roll, the cursor used in mapstore does not show the roll
+ // e.detail.y correspond to pitch, the cursor used in mapstore does not show the pitch
+ setPov({ heading: e.detail.x, pitch: e.detail.y, zoom: e.detail.z });
+ }
+ };
+
+ // Update cursor position on Map when moving on the pictures
+ const onPictureLoading = (e) => {
+ if (setLocation && e.detail) {
+ const { picId, lon, lat } = e.detail;
+ // Check the picture to change only the cursor position on picture change
+ if (location?.properties?.id !== picId) {
+ setLocation({
+ latLng: { lat, lng: lon, h: 0 },
+ properties: { id: picId }
+ });
+ }
+ }
+ };
+
+ // Binding of the listeners to the panoramax viewer
+ psv.addEventListener('view-rotated', onRotate);
+ psv.addEventListener('picture-loading', onPictureLoading);
+
+ // Cleaning of the listerners on completed actions
+ element._cleanupListeners = () => {
+ psv.removeEventListener('view-rotated', onRotate);
+ psv.removeEventListener('picture-loading', onPictureLoading);
+ };
+ };
+
+ // The component emits 'ready' when its loading is complete
+ element.addEventListener('ready', handleReady);
+
+ // Handling case whn the component is already ready
+ if (element.psv) {
+ handleReady();
+ }
+
+ return () => {
+ element.removeEventListener('ready', handleReady);
+ if (element._cleanupListeners) {
+ element._cleanupListeners();
+ }
+ };
+ }, [setLocation, setPov, location?.properties?.id]);
+
+ if (!pictureId) {
+ return
;
+ }
+
+ return (
+
+ );
+};
+
+export default PanoramaxView;
diff --git a/web/client/plugins/StreetView/constants.js b/web/client/plugins/StreetView/constants.js
index 09f127c899..5d7d2fc0d1 100644
--- a/web/client/plugins/StreetView/constants.js
+++ b/web/client/plugins/StreetView/constants.js
@@ -10,7 +10,8 @@ export const STREET_VIEW_DATA_LAYER_ID = "street-view-data";
export const PROVIDERS = {
GOOGLE: 'google',
CYCLOMEDIA: 'cyclomedia',
- MAPILLARY: "mapillary"
+ MAPILLARY: "mapillary",
+ PANORAMAX: "panoramax"
};
// ///////////////////////////////
@@ -24,3 +25,9 @@ export const CYCLOMEDIA_DEFAULT_MAX_RESOLUTION = 1;
* Unique key to find the credentials in the global state
*/
export const CYCLOMEDIA_CREDENTIALS_REFERENCE = "__cycloMedia_streetView_source_credential__";
+
+
+// ///////////////////////////////
+// PANORAMAX SPECIFIC CONSTANTS
+// ///////////////////////////////
+export const PANORAMAX_DEFAULT_API_URL = "https://api.panoramax.xyz/api";
diff --git a/web/client/plugins/StreetView/containers/PanoramaxViewPanel.jsx b/web/client/plugins/StreetView/containers/PanoramaxViewPanel.jsx
new file mode 100644
index 0000000000..5fe9978d53
--- /dev/null
+++ b/web/client/plugins/StreetView/containers/PanoramaxViewPanel.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { createStructuredSelector } from 'reselect';
+import { setLocation, setPov } from '../actions/streetView';
+
+import { locationSelector } from '../selectors/streetView';
+import PanoramaxView from '../components/PanoramaxView/PanoramaxView';
+
+const PanoramaxViewPanel = connect(createStructuredSelector({
+ location: locationSelector
+}), {
+ setLocation,
+ setPov
+})((props) => {
+ return ;
+});
+
+export default PanoramaxViewPanel;
diff --git a/web/client/plugins/StreetView/containers/StreetViewContainer.jsx b/web/client/plugins/StreetView/containers/StreetViewContainer.jsx
index 5da17b6e42..41cae17be5 100644
--- a/web/client/plugins/StreetView/containers/StreetViewContainer.jsx
+++ b/web/client/plugins/StreetView/containers/StreetViewContainer.jsx
@@ -11,12 +11,14 @@ import { enabledSelector } from '../selectors/streetView';
import GStreetViewPanel from './GStreetViewPanel';
import CyclomediaViewPanel from './CyclomediaViewPanel';
import MapillaryViewPanel from './MapillaryViewPanel';
+import PanoramaxViewPanel from './PanoramaxViewPanel';
import { toggleStreetView } from '../actions/streetView';
const panels = {
google: GStreetViewPanel,
cyclomedia: CyclomediaViewPanel,
- mapillary: MapillaryViewPanel
+ mapillary: MapillaryViewPanel,
+ panoramax: PanoramaxViewPanel
};
diff --git a/web/client/plugins/StreetView/epics/streetView.js b/web/client/plugins/StreetView/epics/streetView.js
index b648f9809c..7009d81e31 100644
--- a/web/client/plugins/StreetView/epics/streetView.js
+++ b/web/client/plugins/StreetView/epics/streetView.js
@@ -23,7 +23,8 @@ import {
enabledSelector,
getStreetViewDataLayer,
useStreetViewDataLayerSelector,
- streetViewDataLayerSelector
+ streetViewDataLayerSelector,
+ streetViewConfigurationSelector
} from "../selectors/streetView";
import {setLocation, UPDATE_STREET_VIEW_LAYER } from '../actions/streetView';
import API from '../api';
@@ -156,8 +157,10 @@ export const streetViewMapClickHandler = (action$, {getState = () => {}}) => {
error({title: "streetView.title", message: "streetView.messages.providerNotSupported"})
);
}
+ // we need to pass the providerSetting to the api getLocation method, where it is need to get the search feature api url
+ const providerSettings = streetViewConfigurationSelector(getState())?.providerSettings;
return Rx.Observable
- .defer(() => getLocation(point))
+ .defer(() => getLocation(point, providerSettings))
.map(setLocation)
.catch((e) => {
if (e.code === "ZERO_RESULTS") {
diff --git a/web/client/plugins/StreetView/selectors/streetView.js b/web/client/plugins/StreetView/selectors/streetView.js
index 6b781a2d66..3c27d15085 100644
--- a/web/client/plugins/StreetView/selectors/streetView.js
+++ b/web/client/plugins/StreetView/selectors/streetView.js
@@ -5,6 +5,7 @@ import { PROVIDERS, CONTROL_NAME, MARKER_LAYER_ID, STREET_VIEW_DATA_LAYER_ID, CY
import { createControlEnabledSelector } from '../../../selectors/controls';
import { additionalLayersSelector } from '../../../selectors/additionallayers';
import { localConfigSelector } from '../../../selectors/localConfig';
+import {Style, Stroke, Fill, Circle as CircleStyle} from 'ol/style';
export const enabledSelector = createControlEnabledSelector(CONTROL_NAME);
export const streetViewStateSelector = state => state?.streetView ?? {};
@@ -95,6 +96,14 @@ const getVectorStyle = (overrides) => ({
]
}
});
+
+const getPanoramaxMVTStyle = () => {
+ // For a better integration we use the same radius, width, color and opacity as the geostyler style
+ return new Style({
+ stroke: new Stroke({ color: '#3165EFCC', width: 2}),
+ image: new CircleStyle({ radius: 6, fill: new Fill({ color: '#3165EF99' }) })
+ });
+};
const GOOGLE_DATA_LAYER_DEFAULTS = {
provider: 'custom',
type: "tileprovider",
@@ -120,6 +129,22 @@ const CYCLOMEDIA_DATA_LAYER_DEFAULTS = {
const MAPILLARY_DATA_LAYER_DEFAULTS = {
type: 'vector'
};
+
+const PANORAMAX_DATA_LAYER_DEFAULTS = {
+ type: 'tileprovider', // utilise le plugin TileProvider OpenLayers
+ provider: 'custom', // “custom” pour donner directement une URL
+ url: 'https://panoramax.openstreetmap.fr/api/map/{z}/{x}/{y}.mvt',
+ format: 'application/vnd.mapbox-vector-tile', // signale que c’est du MVT
+ name: 'panoramax:sequences',
+ visibility: true,
+ // optionnel: bornes de zoom
+ minimumLevel: 0,
+ // Panoramax instances such as IGN and openstreetmap don't have tiles above zoom level 15
+ // By setting 15 as max zoom level, if the map is at a level above 15, the tiles at level 15 will be loaded and then overzoomed
+ maximumLevel: 15,
+ olStyle: getPanoramaxMVTStyle(), // we can't use getVectorStyle() because it only applies to olFeature and the MVT features are of type RenderFeature
+ attribution: 'Panoramax'
+};
/**
* Gets the default data layer configuration for the current provider.
* @memberof selectors.streetview
@@ -144,6 +169,16 @@ const providerDataLayerDefaultsSelector = createSelector(
style: getVectorStyle({ msHeightReference }),
url: configuration?.providerSettings?.ApiURL
};
+ case PROVIDERS.PANORAMAX:
+ const tilesUrl = configuration?.providerSettings?.PanoramaxApiURL ? configuration?.providerSettings?.PanoramaxApiURL + '/map/{z}/{x}/{y}.mvt' : PANORAMAX_DATA_LAYER_DEFAULTS.url;
+ const tilesZoomLevel = {min: configuration?.providerSettings?.minimumLevel, max: configuration?.providerSettings?.maximumLevel};
+ return {
+ ...PANORAMAX_DATA_LAYER_DEFAULTS,
+ // Use specific url or default layer url
+ url: tilesUrl,
+ minimumLevel: tilesZoomLevel.min ?? PANORAMAX_DATA_LAYER_DEFAULTS.minimumLevel,
+ maximumLevel: tilesZoomLevel.max ?? PANORAMAX_DATA_LAYER_DEFAULTS.maximumLevel
+ };
default:
return {};
}
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index 7f911e160e..e1ecfbd4ae 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -4300,6 +4300,9 @@
"projectionNotAvailable": "Die konfigurierte Projektion {srs} ist nicht verfügbar. Bitte kontaktieren Sie den Administrator."
},
"reloadAPI": "API neu laden"
+ },
+ "panoramax": {
+ "emptySelection": "Sie haben kein Bild ausgewählt."
}
},
"sidebarMenu": {
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index c72531bf2a..6650be0c82 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -4271,6 +4271,9 @@
"projectionNotAvailable": "The projection {srs} configured is not available. Please contact the administrator."
},
"reloadAPI": "Reload API"
+ },
+ "panoramax": {
+ "emptySelection": "You have not selected an image."
}
},
"sidebarMenu": {
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index cfe74e30b6..c19efc7cfc 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -4261,6 +4261,9 @@
"projectionNotAvailable": "La proyección {srs} configurada no está disponible. Por favor, contacte al administrador."
},
"reloadAPI": "Recargar API"
+ },
+ "panoramax": {
+ "emptySelection": "No ha seleccionado ninguna imagen."
}
},
"sidebarMenu": {
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index fd691925a7..1c7fba7579 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -4261,7 +4261,9 @@
"invalidCredentials": "Identifiants invalides",
"projectionNotAvailable": "La projection {srs} configurée n'est pas disponible. Veuillez contacter l'administrateur."
},
- "reloadAPI": "Recharger l'API"
+ "reloadAPI": "Recharger l'API"},
+ "panoramax": {
+ "emptySelection": "Vous n'avez pas sélectionné d'image."
}
},
"sidebarMenu": {
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index 746d18460d..d1fd64bd14 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -4263,6 +4263,9 @@
"projectionNotAvailable": "La proiezione {srs} configurata non è disponibile. Contattare l'amministratore."
},
"reloadAPI": "Ricarica API"
+ },
+ "panoramax": {
+ "emptySelection": "Non hai selezionato alcuna immagine."
}
},
"sidebarMenu": {