Skip to content

Commit

Permalink
Merge pull request #944 from geoadmin/feat_PB-669_less_get_feature_re…
Browse files Browse the repository at this point in the history
…quests

PB-669 : separate html popup request from getFeature
  • Loading branch information
pakb authored Jun 20, 2024
2 parents 2ec0f89 + a415f3d commit 7566663
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 104 deletions.
253 changes: 171 additions & 82 deletions src/api/features/features.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,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 @@ -529,108 +530,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
8 changes: 7 additions & 1 deletion src/router/storeSync/LayerParamConfig.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
parseLayersParam,
transformLayerIntoUrlString,
} from '@/router/storeSync/layersParamParser'
import { flattenExtent } from '@/utils/coordinates/coordinateUtils.js'
import { getExtentOfGeometries } from '@/utils/geoJsonUtils'
import log from '@/utils/logging'

Expand Down Expand Up @@ -132,7 +133,12 @@ function dispatchLayersFromUrlIntoStore(to, store, urlParamValue) {
store.getters.getLayerConfigById(parsedLayer.id),
featureId,
store.state.position.projection,
{ lang: store.state.i18n.lang }
{
lang: store.state.i18n.lang,
screenWidth: store.state.ui.width,
screenHeight: store.state.ui.height,
mapExtent: flattenExtent(store.getters.extent),
}
)
)
})
Expand Down
12 changes: 6 additions & 6 deletions src/store/modules/features.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,12 +648,12 @@ export default {
// we avoid requesting the drawings and external layers, they're not handled here
if (rootState.layers.config.find((layer) => layer.id === feature.layer.id)) {
featuresPromises.push(
getFeature(
feature.layer,
feature.id,
rootState.position.projection,
rootState.i18n.lang
)
getFeature(feature.layer, feature.id, rootState.position.projection, {
lang: rootState.i18n.lang,
screenWidth: rootState.ui.width,
screenHeight: rootState.ui.height,
mapExtent: flattenExtent(rootState.getters.mapExtent),
})
)
}
})
Expand Down
6 changes: 3 additions & 3 deletions tests/cypress/fixtures/features/features.fixture.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"results": [
{
"geometry": {
"type": "Point",
"coordinates": [
"type": "MultiPoint",
"coordinates": [[
2600000.0,
1200000.0
]
]]
},
"layerBodId": "test.wms.layer",
"bbox": [
Expand Down
Loading

0 comments on commit 7566663

Please sign in to comment.