Skip to content

Commit

Permalink
Merge pull request #947 from geoadmin/develop
Browse files Browse the repository at this point in the history
New Release v1.31.0 - #minor
  • Loading branch information
pakb authored Jun 21, 2024
2 parents d017fb3 + e8aa05a commit 8ee61f3
Show file tree
Hide file tree
Showing 26 changed files with 495 additions and 378 deletions.
285 changes: 200 additions & 85 deletions src/api/features/features.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { WMSGetFeatureInfo } from 'ol/format'
import GeoJSON from 'ol/format/GeoJSON'

import LayerFeature from '@/api/features/LayerFeature.class'
import ExternalGroupOfLayers from '@/api/layers/ExternalGroupOfLayers.class'
import ExternalLayer from '@/api/layers/ExternalLayer.class'
import ExternalWMSLayer from '@/api/layers/ExternalWMSLayer.class'
import GeoAdminLayer from '@/api/layers/GeoAdminLayer.class'
import { YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA } from '@/api/layers/LayerTimeConfigEntry.class'
import {
ALL_YEARS_TIMESTAMP,
CURRENT_YEAR_TIMESTAMP,
} from '@/api/layers/LayerTimeConfigEntry.class'
import { API_BASE_URL, DEFAULT_FEATURE_COUNT_SINGLE_POINT } from '@/config'
import allCoordinateSystems, { LV95 } from '@/utils/coordinates/coordinateSystems'
import { projExtent } from '@/utils/coordinates/coordinateUtils'
Expand All @@ -32,7 +36,7 @@ function getApi3TimeInstantParam(layer) {
// timestamp therefore we need to set it to null in this case.
if (
layer.timeConfig?.currentYear &&
layer.timeConfig.currentYear !== YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA
![ALL_YEARS_TIMESTAMP, CURRENT_YEAR_TIMESTAMP].includes(layer.timeConfig.currentYear)
) {
return layer.timeConfig.currentYear
}
Expand Down Expand Up @@ -158,23 +162,24 @@ export async function identifyOnGeomAdminLayer({
},
}
)
// firing a getFeature (async/parallel) on each identified feature
const featureRequests = []
// firing a getHtmlPopup (async/parallel) on each identified feature
const features = []
if (identifyResponse.data?.results?.length > 0) {
// for each feature that has been identified, we will now load their metadata and tooltip content
identifyResponse.data.results.forEach((feature) => {
featureRequests.push(
getFeature(layer, feature.id, projection, {
for (const feature of identifyResponse.data.results) {
const featureData = await getFeatureHtmlPopup(layer, feature.id, {
lang,
screenWidth,
screenHeight,
mapExtent,
})
features.push(
parseGeomAdminFeature(layer, feature, featureData, projection, {
lang,
coordinates: coordinate.join(','),
mapExtent: mapExtent.join(','),
imageDisplay,
})
)
})
}
}
// waiting on the result of all parallel getFeature requests
return await Promise.all(featureRequests)
return features
}

/**
Expand Down Expand Up @@ -260,6 +265,27 @@ async function identifyOnExternalLayer(config) {
tolerance,
outputProjection: projection,
})
} else if (layer instanceof ExternalGroupOfLayers) {
// firing one request per sub-layer
const allRequests = [
layer.layers.map((subLayer) =>
identifyOnExternalLayer({
...config,
layer: subLayer,
})
),
]
const allResponses = await Promise.allSettled(allRequests)
// logging any error
allResponses
.filter((response) => response.status !== 'fulfilled')
.forEach((failedResponse) => {
log.error('Error while identify an external sub-layer', failedResponse)
})
return allResponses
.filter((response) => response.status === 'fulfilled' && response.value)
.map((response) => response.value)
.flat()
} else {
throw new GetFeatureInfoError(
`Unsupported external layer type to build getFeatureInfo request: ${layer.type}`
Expand Down Expand Up @@ -361,7 +387,8 @@ async function identifyOnExternalWmsLayer(config) {
// there might exist more implementation of WMS, but I stopped there looking for more
// (please add more if you think one of our customer/external layer providers uses another flavor of WMS)
}
if (layer.timeConfig?.currentYear) {
// In WMS "all" years mean no TIME parameter
if (layer.timeConfig?.currentYear && layer.timeConfig.currentYear !== ALL_YEARS_TIMESTAMP) {
params.TIME = layer.timeConfig.currentYear
}
// WMS 1.3.0 uses i,j to describe pixel coordinate where we want feature info
Expand Down Expand Up @@ -529,108 +556,196 @@ export const identify = (config) => {
})
}

/**
* @param {GeoAdminLayer} layer The layer from which the feature is part of
* @param {String | Number} featureId The feature ID in the BGDI
* @returns {string}
*/
function generateFeatureUrl(layer, featureId) {
return `${API_BASE_URL}rest/services/${layer.getTopicForIdentifyAndTooltipRequests()}/MapServer/${layer.id}/${featureId}`
}

/**
* Generates parameters used to request endpoint to get a single feature's data and endpoint to get
* a single feature's HTML popup. As some layers have a resolution dependent answer, we have to give
* the map extent and the current screen size with each request.
*
* @param {Object} [options]
* @param {String} [options.lang='en'] ISO code of the current lang. Default is `'en'`
* @param {Number} [options.screenWidth] Current screen width in pixels
* @param {Number} [options.screenHeight] Current screen height in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent]
*/
function generateFeatureParams(options = {}) {
const { lang = 'en', screenWidth = null, screenHeight = null, mapExtent = null } = options
let imageDisplay = null
if (screenWidth && screenHeight) {
imageDisplay = `${screenWidth},${screenHeight},96`
}
return {
sr: LV95.epsgNumber,
lang,
imageDisplay,
mapExtent: mapExtent?.join(',') ?? null,
}
}

/**
* @param {GeoAdminLayer} layer The layer to which this feature belongs to
* @param {Object} featureMetadata The backend response (either identify, or feature-resource) for
* this feature
* @param featureHtmlPopup The backend response for the getHtmlPopup endpoint for this feature
* @param {CoordinateSystem} outputProjection In which projection the feature should be in.
* @param {Object} [options]
* @param {String} [options.lang] The lang the title of the feature should be look up. Some features
* do provide a title per lang, instead of an all-purpose title. In this case we need the lang ISO
* code to be able to decide which title the feature will have. Default is `en`
* @returns {LayerFeature}
*/
function parseGeomAdminFeature(
layer,
featureMetadata,
featureHtmlPopup,
outputProjection,
options = {}
) {
const { lang = 'en' } = options
const featureGeoJSONGeometry = featureMetadata.geometry
let featureExtent = []
if (featureMetadata.bbox) {
featureExtent.push(...featureMetadata.bbox)
}
let featureName = featureMetadata.id
if (featureMetadata.properties) {
const { name = null, title = null, label = null } = featureMetadata.properties
const titleInCurrentLang = featureMetadata.properties[`title_${lang}`]
if (label) {
featureName = label
} else if (name) {
featureName = name
} else if (title) {
featureName = title
} else if (titleInCurrentLang) {
featureName = titleInCurrentLang
}
}

if (outputProjection.epsg !== LV95.epsg) {
if (featureExtent.length === 4) {
featureExtent = projExtent(LV95, outputProjection, featureExtent)
}
}

return new LayerFeature({
layer,
id: featureMetadata.id,
name: featureName,
data: featureHtmlPopup,
coordinates: getGeoJsonFeatureCoordinates(featureGeoJSONGeometry, LV95, outputProjection),
extent: featureExtent,
geometry: featureGeoJSONGeometry,
})
}

/**
* Loads a feature metadata and tooltip content from this two endpoint of the backend
*
* - http://api3.geo.admin.ch/services/sdiservices.html#identify-features
* - https://api3.geo.admin.ch/services/sdiservices.html#feature-resource
* - http://api3.geo.admin.ch/services/sdiservices.html#htmlpopup-resource
*
* @param {GeoAdminLayer} layer The layer from which the feature is part of
* @param {String | Number} featureID The feature ID in the BGDI
* @param {String | Number} featureId The feature ID in the BGDI
* @param {CoordinateSystem} outputProjection Projection in which the coordinates (and possible
* extent) of the features should be expressed
* @param {String} lang The language for the HTML popup
* @param {Object} [options]
* @param {String} [options.lang] The language for the HTML popup. Default is `en`.
* @param {Number} [options.screenWidth] Width of the screen in pixels
* @param {Number} [options.screenHeight] Height of the screen in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent] Current extent of the map,
* described in LV95.
* @returns {Promise<LayerFeature>}
*/
const getFeature = (layer, featureID, outputProjection, options = {}) => {
const { lang = 'en', coordinates = null, imageDisplay = null, mapExtent = null } = options
const getFeature = (layer, featureId, outputProjection, options = {}) => {
return new Promise((resolve, reject) => {
if (!layer?.id) {
reject('Needs a valid layer with an ID')
}
if (!featureID) {
if (!featureId) {
reject('Needs a valid feature ID')
}
// combining the two requests in one promise
const topic = layer.getTopicForIdentifyAndTooltipRequests()
const featureUrl = `${API_BASE_URL}rest/services/${topic}/MapServer/${layer.id}/${featureID}`
const params = {
sr: LV95.epsgNumber,
lang: lang,
}
if (coordinates) {
params.coord = coordinates
}
if (imageDisplay) {
params.imageDisplay = imageDisplay
}
if (mapExtent) {
params.mapExtent = mapExtent
if (!outputProjection) {
reject('An output projection is required')
}
axios
.all([
axios.get(featureUrl, {
axios.get(generateFeatureUrl(layer, featureId), {
params: {
geometryFormat: 'geojson',
...params,
...generateFeatureParams(options),
},
}),
axios.get(`${featureUrl}/htmlPopup`, {
params,
}),
getFeatureHtmlPopup(layer, featureId, options),
])
.then((responses) => {
const featureMetadata = responses[0].data.feature
? responses[0].data.feature
: responses[0].data
const featureHtmlPopup = responses[1].data
const featureGeoJSONGeometry = featureMetadata.geometry
let featureExtent = []
if (featureMetadata.bbox) {
featureExtent.push(...featureMetadata.bbox)
}
let featureName = featureID
if (featureMetadata.properties) {
const { name = null, title = null, label = null } = featureMetadata.properties
const titleInCurrentLang = featureMetadata.properties[`title_${lang}`]
if (label) {
featureName = label
} else if (name) {
featureName = name
} else if (title) {
featureName = title
} else if (titleInCurrentLang) {
featureName = titleInCurrentLang
}
}

if (outputProjection.epsg !== LV95.epsg) {
if (featureExtent.length === 4) {
featureExtent = projExtent(LV95, outputProjection, featureExtent)
}
}

.then(([getFeatureResponse, featureHtmlPopup]) => {
const featureMetadata = getFeatureResponse.data.feature ?? getFeatureResponse.data
resolve(
new LayerFeature({
parseGeomAdminFeature(
layer,
id: featureID,
name: featureName,
data: featureHtmlPopup,
coordinates: getGeoJsonFeatureCoordinates(
featureGeoJSONGeometry,
LV95,
outputProjection
),
extent: featureExtent,
geometry: featureGeoJSONGeometry,
})
featureMetadata,
featureHtmlPopup,
outputProjection,
options
)
)
})
.catch((error) => {
log.error(
'Error while requesting a feature to the backend',
layer,
featureID,
featureId,
error
)
reject(error)
})
})
}

/**
* Retrieves the HTML popup of a feature (the backend builds it for us).
*
* As the request's outcome is dependent on the resolution, we have to give the screen size and map
* extent with the request.
*
* @param {GeoAdminLayer} layer
* @param {String} featureId
* @param {Object} options
* @param {String} [options.lang] The language for the HTML popup. Default is `en`.
* @param {Number} [options.screenWidth] Width of the screen in pixels
* @param {Number} [options.screenHeight] Height of the screen in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent] Current extent of the map,
* described in LV95.
* @returns {Promise<String>}
*/
export function getFeatureHtmlPopup(layer, featureId, options) {
return new Promise((resolve, reject) => {
if (!layer?.id) {
reject('Needs a valid layer with an ID')
}
if (!featureId) {
reject('Needs a valid feature ID')
}
axios
.get(`${generateFeatureUrl(layer, featureId)}/htmlPopup`, {
params: generateFeatureParams(options),
})
.then((response) => {
resolve(response.data)
})
.catch((error) => {
log.error(
'Error while requesting a the HTML popup of a feature to the backend',
layer,
featureId,
error
)
reject(error)
Expand Down
13 changes: 4 additions & 9 deletions src/api/layers/LayerTimeConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import LayerTimeConfigEntry, {
ALL_YEARS_WMS_TIMESTAMP,
YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA,
} from '@/api/layers/LayerTimeConfigEntry.class'
import LayerTimeConfigEntry, { ALL_YEARS_TIMESTAMP } from '@/api/layers/LayerTimeConfigEntry.class'

/**
* @class
Expand All @@ -25,8 +22,8 @@ export default class LayerTimeConfig {
this.behaviour = behaviour
/** @type {LayerTimeConfigEntry[]} */
this.timeEntries = [...timeEntries]
if (this.behaviour === ALL_YEARS_WMS_TIMESTAMP) {
this.timeEntries.splice(0, 0, new LayerTimeConfigEntry(ALL_YEARS_WMS_TIMESTAMP))
if (this.behaviour === ALL_YEARS_TIMESTAMP) {
this.timeEntries.splice(0, 0, new LayerTimeConfigEntry(ALL_YEARS_TIMESTAMP))
}
/*
* Here we will define what is the first "currentTimeEntry" for this configuration.
Expand Down Expand Up @@ -57,9 +54,7 @@ export default class LayerTimeConfig {
this.updateCurrentTimeEntry(behaviour)
}

this.years = this.timeEntries
.map((entry) => entry.year)
.filter((year) => year !== YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA)
this.years = this.timeEntries.map((entry) => entry.year)
}

/**
Expand Down
Loading

0 comments on commit 8ee61f3

Please sign in to comment.