Skip to content
Open
Show file tree
Hide file tree
Changes from all 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