diff --git a/README.md b/README.md index 1056d68a0f5..6baed7ef36b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ cd ngeo make serve-ngeo ``` -The ngeo examples are now available on your http://localhost:3000/examples/. +The ngeo examples are now available on your https://localhost:3000/examples/. ### Run GeoMapFish @@ -56,7 +56,7 @@ To run the GeoMapFish examples: make serve-gmf ``` -then visit http://localhost:3000/contribs/gmf/examples/. +then visit https://localhost:3000/contribs/gmf/examples/. To run the GeoMapFish applications: @@ -65,8 +65,8 @@ serve-gmf-apps ``` then visit them using -http://localhost:3000/contribs/gmf/apps/.html, for example: -http://localhost:3000/contribs/gmf/apps/desktop.html +https://localhost:3000/contribs/gmf/apps/.html, for example: +https://localhost:3000/contribs/gmf/apps/desktop.html ### Go further diff --git a/api/dist/apihelp/apihelp.html b/api/dist/apihelp/apihelp.html index 99b19c4553d..3bb250254f5 100644 --- a/api/dist/apihelp/apihelp.html +++ b/api/dist/apihelp/apihelp.html @@ -138,7 +138,7 @@

A map with some additional controls

div: 'map5', zoom: 3, center: [544500, 210100], - layers: ['bank'], + layers: ['osm_open'], addLayerSwitcher: true, addMiniMap: true, miniMapExpanded: true, diff --git a/api/src/Map.js b/api/src/Map.js index 04cab3d7413..9ab17e66a26 100644 --- a/api/src/Map.js +++ b/api/src/Map.js @@ -32,7 +32,7 @@ import {get as getProjection} from 'ol/proj.js'; import constants from './constants.js'; -import {getFeaturesFromLayer} from './Querent.js'; +import {getFeaturesFromIds, getFeaturesFromCoordinates} from './Querent.js'; import * as themes from './Themes.js'; @@ -54,6 +54,12 @@ import * as themes from './Themes.js'; * @property {string[]} [layers] */ +/** + * @type {Array} + */ +const EXCLUDE_PROPERTIES = ['geom', 'geometry', 'boundedBy']; + + /** * @private * @hidden @@ -184,6 +190,33 @@ class Map { this.selectObject(selected.getId()); } }); + + + this.map_.on('singleclick', (event) => { + const resolution = this.map_.getView().getResolution(); + if (resolution === undefined) { + throw new Error('Missing resolution'); + } + const visibleLayers = this.map_.getLayers().getArray().filter(layer => layer.getVisible()); + const visibleLayersName = visibleLayers.map(layer => layer.get('config.name')); + + this.clearSelection(); + + for (const layer of constants.queryableLayers) { + if (visibleLayersName.includes(layer)) { + getFeaturesFromCoordinates(layer, event.coordinate, resolution).then((feature) => { + if (feature) { + this.vectorSource_.addFeature(feature); + const featureId = feature.getId(); + if (featureId === undefined) { + throw new Error('Missing feature ID'); + } + this.selectObject(featureId, true); + } + }); + } + } + }); } /** @@ -195,11 +228,8 @@ class Map { overlayContainer.className = 'ol-popup'; const overlayCloser = document.createElement('div'); overlayCloser.className = 'ol-popup-closer'; - overlayCloser.addEventListener('click', (event) => { - // clear the selected features - this.selectInteraction_.getFeatures().clear(); - // hide the overlay - this.overlay_.setPosition(undefined); + overlayCloser.addEventListener('click', () => { + this.clearSelection(); return false; }); const overlayContent = document.createElement('div'); @@ -250,11 +280,10 @@ class Map { /** * @param {string} layer Name of the layer to fetch the features from * @param {string[]} ids List of ids - * @param {boolean} [highlight=false] Whether to add the features on - * the map or not. + * @param {boolean} [highlight=false] Whether to add the features on the map or not. */ recenterOnObjects(layer, ids, highlight = false) { - getFeaturesFromLayer(layer, ids) + getFeaturesFromIds(layer, ids) .then((features) => { if (!features.length) { console.error('Could not recenter: no objects were found.'); @@ -346,15 +375,32 @@ class Map { } /** - * @param {string} id Identifier. + * @param {string|number} id Identifier. + * @param {boolean} table Display all properties in a table */ - selectObject(id) { + selectObject(id, table = false) { const feature = this.vectorSource_.getFeatureById(id); if (feature) { const coordinates = /** @type {import('ol/geom/Point.js').default} */( feature.getGeometry() ).getCoordinates(); const properties = feature.getProperties(); + let contentHTML = ''; + if (table) { + contentHTML += ''; + for (const key in properties) { + if (!EXCLUDE_PROPERTIES.includes(key)) { + contentHTML += ''; + contentHTML += ``; + contentHTML += ``; + contentHTML += ''; + } + } + contentHTML += '
${key}${properties[key]}
'; + } else { + contentHTML += `
${properties.title}
`; + contentHTML += `

${properties.description}

`; + } const element = this.overlay_.getElement(); if (!element) { throw new Error('Missing element'); @@ -363,18 +409,24 @@ class Map { if (!content) { throw new Error('Missing content'); } - content.innerHTML = ''; - content.innerHTML += `
${properties.title}
`; - content.innerHTML += `

${properties.description}

`; + content.innerHTML = contentHTML; this.overlay_.setPosition(coordinates); - this.view_.setCenter(coordinates); } } + /** + * + */ + clearSelection() { + // clear the selected features + this.selectInteraction_.getFeatures().clear(); + this.vectorSource_.clear(); + // hide the overlay + this.overlay_.setPosition(undefined); + } } - /** * @param {string[]} keys Keys. * @param {Array<*>} values Values. diff --git a/api/src/Querent.js b/api/src/Querent.js index 3b8826b9bf3..885f25c18a3 100644 --- a/api/src/Querent.js +++ b/api/src/Querent.js @@ -2,6 +2,24 @@ import {getOverlayDefs} from './Themes.js'; import {appendParams as olUriAppendParams} from 'ol/uri.js'; import olFormatGML2 from 'ol/format/GML2.js'; import olFormatWFS from 'ol/format/WFS.js'; +import {buffer, createOrUpdateFromCoordinate} from 'ol/extent.js'; + + +/** + * Click tolerance in pixel + * @type {number} + */ +const TOLERANCE = 10; + + +/** + * @param {import('./Themes.js').overlayDefinition} def Overlay definition. + * @return {boolean} Is the overlay queryable. + */ +function querable(def) { + return def.layer.type === 'WMS' && !!def.ogcServer.wfsSupport && !!def.ogcServer.urlWfs; +} + /** * Issues a simple WFS GetFeature request for a single layer to fetch @@ -18,7 +36,7 @@ import olFormatWFS from 'ol/format/WFS.js'; * @return {Promise>>} Promise. * @hidden */ -export function getFeaturesFromLayer(layer, ids) { +export function getFeaturesFromIds(layer, ids) { return new Promise((resolve, reject) => { getOverlayDefs().then((overlayDefs) => { @@ -31,12 +49,7 @@ export function getFeaturesFromLayer(layer, ids) { return; } - if ( - !overlayDef.ogcServer || - !overlayDef.ogcServer.wfsSupport || - !overlayDef.ogcServer.urlWfs || - overlayDef.layer.type !== 'WMS' - ) { + if (!querable(overlayDef)) { reject(`Layer "${layer}" does not support WFS.`); return; } @@ -70,3 +83,63 @@ export function getFeaturesFromLayer(layer, ids) { }); }); } + + +/** + * @param {string} layer Name of the layer to query + * @param {number[]} coordinate Coordinate. + * @param {number} resolution Resolution + * + * @return {Promise>} Promise. + * @hidden + */ +export function getFeaturesFromCoordinates(layer, coordinate, resolution) { + return new Promise((resolve, reject) => { + getOverlayDefs().then((overlayDefs) => { + + const overlayDef = overlayDefs.get(layer); + + if (!overlayDef) { + reject(`Layer "${layer}" was not found in themes.`); + return; + } + + if (!querable(overlayDef)) { + reject(`Layer "${layer}" does not support WFS.`); + return; + } + + const bbox = buffer(createOrUpdateFromCoordinate(coordinate), TOLERANCE * resolution); + + const params = { + 'BBOX': bbox.join(','), + 'MAXFEATURES': 1, + 'REQUEST': 'GetFeature', + 'SERVICE': 'WFS', + 'TYPENAME': layer, + 'VERSION': '1.0.0' + }; + const url = olUriAppendParams(overlayDef.ogcServer.urlWfs, params); + + /** @type {?import('ol/Feature.js').default} */ + let feature; + fetch(url) + .then(response => response.text().then((responseText) => { + const wfsFormat = new olFormatWFS({ + featureNS: overlayDef.ogcServer.namespace, + gmlFormat: new olFormatGML2() + }); + feature = wfsFormat.readFeature(responseText); + })) + .catch((response) => { + console.error(`WFS GetFeature request failed, response: ${response}`); + }) + .then(() => { + if (!feature) { + throw new Error('Missing feature'); + } + resolve(feature); + }); + }); + }); +} diff --git a/api/src/Themes.js b/api/src/Themes.js index 766bbd40f8f..6e23884fd9a 100644 --- a/api/src/Themes.js +++ b/api/src/Themes.js @@ -46,7 +46,6 @@ export function getBackgroundLayers() { const layerWMTS = /** @type {import('gmf/themes.js').GmfLayerWMTS} */(config); promises.push( createWMTSLayer(layerWMTS).then((layer) => { - layer.set('config.layer', layerWMTS.layer); layer.set('config.name', layerWMTS.name); return layer; }) @@ -56,7 +55,6 @@ export function getBackgroundLayers() { const ogcServer = themes.ogcServers[config.ogcServer]; promises.push( createWMSLayer(layerWMS, ogcServer).then((layer) => { - layer.set('config.layer', layerWMS.layers); layer.set('config.name', layerWMS.name); return layer; }) @@ -194,19 +192,21 @@ export function getOverlayLayers(layerNames) { * @hidden */ export function createWMSLayer(config, ogcServer) { - const source = new ImageWMS({ - url: ogcServer.url, - projection: undefined, // should be removed in next OL version - params: { - 'LAYERS': config.layers - }, - serverType: ogcServer.type - }); // @ts-ignore: OL issue const layer = new ImageLayer({ - source + source: new ImageWMS({ + url: ogcServer.url, + projection: undefined, // FIXME: should be removed in next OL version + params: { + 'LAYERS': config.layers + }, + serverType: ogcServer.type + }), + minResolution: config.minResolutionHint, + maxResolution: config.maxResolutionHint }); layer.set('title', config.name); + layer.set('config.name', config.name); return Promise.resolve(layer); } @@ -232,6 +232,7 @@ export function createWMTSLayer(config) { source: source }); layer.set('title', config.name); + layer.set('config.name', config.name); return layer; }); } diff --git a/api/src/constants.js b/api/src/constants.js index 8681c9104a3..d7d9abcb8eb 100644 --- a/api/src/constants.js +++ b/api/src/constants.js @@ -8,6 +8,7 @@ import EPSG21781 from '@geoblocks/proj/src/EPSG_21781.js'; * @property {number[]} resolutions * @property {[number, number, number, number]} [extent] * @property {string} backgroundLayer + * @property {string[]} queryableLayers */ export default /** @type {APIConfig} */({ @@ -28,4 +29,9 @@ export default /** @type {APIConfig} */({ // The name of the GeoMapFish layer to use as background. May be a single value // (WMTS) or a comma-separated list of layer names (WMS). backgroundLayer: 'orthophoto', + + /** + * The list of layers (names) declared as queryable. + */ + queryableLayers: ['osm_open', 'many_attributes'] }); diff --git a/contribs/gmf/examples/editfeature.js b/contribs/gmf/examples/editfeature.js index db7c7aa56e3..6e32f2af348 100644 --- a/contribs/gmf/examples/editfeature.js +++ b/contribs/gmf/examples/editfeature.js @@ -144,7 +144,7 @@ MainController.prototype.handleMapSingleClick_ = function(evt) { const map = this.map; const view = map.getView(); const resolution = view.getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const buffer = resolution * this.pixelBuffer_; @@ -190,7 +190,7 @@ MainController.prototype.insertFeature = function() { const map = this.map; const view = map.getView(); const resolution = view.getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const buffer = resolution * -50; // 50 pixel buffer inside the extent diff --git a/contribs/gmf/src/drawing/drawFeatureComponent.js b/contribs/gmf/src/drawing/drawFeatureComponent.js index a7d959a8575..b92c1bb6511 100644 --- a/contribs/gmf/src/drawing/drawFeatureComponent.js +++ b/contribs/gmf/src/drawing/drawFeatureComponent.js @@ -663,7 +663,7 @@ Controller.prototype.handleMapContextMenu_ = function(evt) { /** @type {import('ngeo/Menu').MenuActionOptions[]} */ let actions = []; const resolution = this.map.getView().getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const vertexInfo = this.featureHelper_.getVertexInfoAtCoordinate(feature, coordinate, resolution); diff --git a/contribs/gmf/src/editing/editFeatureComponent.js b/contribs/gmf/src/editing/editFeatureComponent.js index 25ad5d1f3b6..f1d0936a9c5 100644 --- a/contribs/gmf/src/editing/editFeatureComponent.js +++ b/contribs/gmf/src/editing/editFeatureComponent.js @@ -1145,7 +1145,7 @@ Controller.prototype.handleMapClick_ = function(evt) { const map = this.map; const view = map.getView(); const resolution = view.getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const buffer = resolution * this.tolerance; diff --git a/contribs/gmf/src/layertree/component.js b/contribs/gmf/src/layertree/component.js index 02c94d1c11a..5b1bdf17ea7 100644 --- a/contribs/gmf/src/layertree/component.js +++ b/contribs/gmf/src/layertree/component.js @@ -635,7 +635,7 @@ Controller.prototype.getScale_ = function() { } const view = this.map.getView(); const resolution = view.getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const mpu = view.getProjection().getMetersPerUnit(); @@ -747,7 +747,7 @@ Controller.prototype.getResolutionStyle = function(gmfLayer) { throw new Error('Missing map'); } const resolution = this.map.getView().getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const minResolution = getNodeMinResolution(gmfLayer); @@ -774,7 +774,7 @@ Controller.prototype.zoomToResolution = function(treeCtrl) { const gmfLayer = /** @type {import('gmf/themes.js').GmfLayerWMS} */ (treeCtrl.node); const view = this.map.getView(); const resolution = view.getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const minResolution = getNodeMinResolution(gmfLayer); diff --git a/contribs/gmf/src/profile/component.js b/contribs/gmf/src/profile/component.js index 599eb40b3b4..c51a5ec7780 100644 --- a/contribs/gmf/src/profile/component.js +++ b/contribs/gmf/src/profile/component.js @@ -428,7 +428,7 @@ ProfileController.prototype.updateEventsListening_ = function() { // compute distance to line in pixels const eventToLine = new olGeomLineString([closestPoint, coordinate]); const resolution = this.map_.getView().getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const pixelDist = eventToLine.getLength() / resolution; diff --git a/examples/elevationProfile.js b/examples/elevationProfile.js index d97563e778b..3c6ae4f77cc 100644 --- a/examples/elevationProfile.js +++ b/examples/elevationProfile.js @@ -254,7 +254,7 @@ MainController.prototype.snapToGeometry = function(coordinate, geometry) { const dy = closestPoint[1] - coordinate[1]; const dist = Math.sqrt(dx * dx + dy * dy); const resolution = this.map.getView().getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } const pixelDist = dist / resolution; diff --git a/src/datasource/DataSources.js b/src/datasource/DataSources.js index 4e3fda9c842..b480ace9dfd 100644 --- a/src/datasource/DataSources.js +++ b/src/datasource/DataSources.js @@ -87,7 +87,7 @@ export class DataSource { // (2) Sync resolution with existing data sources const resolution = view.getResolution(); - if (typeof resolution != 'number') { + if (resolution === undefined) { throw new Error('Missing resolution'); } this.syncDataSourcesToResolution_(resolution); @@ -113,7 +113,7 @@ export class DataSource { const view = evt.target; if (view instanceof olView) { const resolution = view.getResolution(); - if (typeof resolution != 'number') { + if (resolution === undefined) { throw new Error('Missing resolution'); } this.syncDataSourcesToResolution_(resolution); @@ -169,7 +169,7 @@ export class DataSource { const dataSource = event.element; if (this.map_) { const resolution = this.map_.getView().getResolution(); - if (typeof resolution != 'number') { + if (resolution === undefined) { throw new Error('Missing resolution'); } this.syncDataSourceToResolution_(dataSource, resolution); diff --git a/src/query/Querent.js b/src/query/Querent.js index bc54cfb9652..c5b516b1069 100644 --- a/src/query/Querent.js +++ b/src/query/Querent.js @@ -223,7 +223,7 @@ export class Querent { wms: /** @type{ngeoDatasourceOGC[]} */([]), }; const resolution = map.getView().getResolution(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } @@ -539,7 +539,7 @@ export class Querent { const projection = view.getProjection(); const srsName = projection.getCode(); const wfsCount = options.wfsCount === true; - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); } @@ -771,7 +771,7 @@ export class Querent { const resolution = view.getResolution(); const projection = view.getProjection(); const projCode = projection.getCode(); - if (!resolution) { + if (resolution === undefined) { throw new Error('Missing resolution'); }