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": {