Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 43 additions & 1 deletion web/client/components/map/openlayers/Map.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -399,18 +402,57 @@ 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'
});
groupIntersectedFeatures[layer.get('msId')] = groupIntersectedFeatures[layer.get('msId')]
? [ ...groupIntersectedFeatures[layer.get('msId')], geoJSONFeature ]
: [ geoJSONFeature ];
}
return null;
});
const intersectedFeatures = Object.keys(groupIntersectedFeatures).map(id => ({ id, features: groupIntersectedFeatures[id] }));
return intersectedFeatures;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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) => {
Expand Down
19 changes: 18 additions & 1 deletion web/client/plugins/StreetView/StreetView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
* <br>**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.
Expand Down
5 changes: 4 additions & 1 deletion web/client/plugins/StreetView/api/index.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -21,5 +23,6 @@ import * as mapillary from '../api/mapillary';
export default {
google,
cyclomedia,
mapillary
mapillary,
panoramax
};
112 changes: 112 additions & 0 deletions web/client/plugins/StreetView/api/panoramax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import axios from 'axios';
import {PANORAMAX_DEFAULT_API_URL} from '../constants';

const DEFAULT_SRS = 'EPSG:4326';

Check failure on line 4 in web/client/plugins/StreetView/api/panoramax.js

View workflow job for this annotation

GitHub Actions / test-front-end (24.x)

'DEFAULT_SRS' is assigned a value but never used

Check failure on line 4 in web/client/plugins/StreetView/api/panoramax.js

View workflow job for this annotation

GitHub Actions / test-front-end (22.x)

'DEFAULT_SRS' is assigned a value but never used

Check failure on line 4 in web/client/plugins/StreetView/api/panoramax.js

View workflow job for this annotation

GitHub Actions / test-front-end (20.x)

'DEFAULT_SRS' is assigned a value but never used

/**
* 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<void>}
*/
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<unknown>}
*/
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" });
});
};
Loading
Loading