diff --git a/js/extension/actions/__tests__/urbanisme-test.js b/js/extension/actions/__tests__/urbanisme-test.js index 0b31696..cd7aa75 100644 --- a/js/extension/actions/__tests__/urbanisme-test.js +++ b/js/extension/actions/__tests__/urbanisme-test.js @@ -15,7 +15,12 @@ import { SET_URBANISME_DATA, setAttributes, toggleGFIPanel, - TOGGLE_VIEWER_PANEL + TOGGLE_VIEWER_PANEL, + featureInfoClick, + URBANISME_FEATURE_INFO_CLICK, + highlightFeature, + URBANISME_HIGHLIGHT_FEATURE, + resetFeatureHighlight, URBANISME_RESET_FEATURE_HIGHLIGHT } from "../urbanisme"; describe('Test correctness of the urbanisme actions', () => { @@ -45,4 +50,27 @@ describe('Test correctness of the urbanisme actions', () => { expect(action.type).toBe(TOGGLE_VIEWER_PANEL); expect(action.enabled).toEqual(true); }); + it('featureInfoClick', () => { + const action = featureInfoClick({}, 'test'); + expect(action).toExist(); + expect(action.type).toBe(URBANISME_FEATURE_INFO_CLICK); + expect(action.point).toEqual({}); + expect(action.layer).toBe('test'); + expect(action.filterNameList).toEqual([]); + expect(action.overrideParams).toEqual({}); + expect(action.itemId).toBe(null); + }); + it('highlightFeature', () => { + const action = highlightFeature({}, {}, 'test'); + expect(action).toExist(); + expect(action.type).toBe(URBANISME_HIGHLIGHT_FEATURE); + expect(action.point).toEqual({}); + expect(action.feature).toEqual({}); + expect(action.featureCrs).toBe('test'); + }); + it('resetFeatureHighlight', () => { + const action = resetFeatureHighlight(); + expect(action).toExist(); + expect(action.type).toBe(URBANISME_RESET_FEATURE_HIGHLIGHT); + }); }); diff --git a/js/extension/actions/urbanisme.js b/js/extension/actions/urbanisme.js index 190cd62..0401ef4 100644 --- a/js/extension/actions/urbanisme.js +++ b/js/extension/actions/urbanisme.js @@ -5,16 +5,19 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import { printError } from "@mapstore/actions/print"; +import {printError} from "@mapstore/actions/print"; -import { getUrbanismePrintSpec, retryDownload } from "../utils/UrbanismeUtils"; -import { printPDF } from "@js/extension/api"; +import {getUrbanismePrintSpec, retryDownload} from "../utils/UrbanismeUtils"; +import {printPDF} from "@js/extension/api"; export * from "./setUp"; export const TOGGLE_TOOL = "URBANISME:TOGGLE_TOOL"; export const TOGGLE_VIEWER_PANEL = "URBANISME:TOGGLE_VIEWER_PANEL"; export const SET_URBANISME_DATA = "URBANISME:SET_URBANISME_DATA"; export const LOADING = "URBANISME:LOADING"; +export const URBANISME_FEATURE_INFO_CLICK = 'URBANISME:FEATURE_INFO_CLICK'; +export const URBANISME_HIGHLIGHT_FEATURE = 'URBANISME:HIGHLIGHT_FEATURE'; +export const URBANISME_RESET_FEATURE_HIGHLIGHT = 'URBANISME:RESET_FEATURE_HIGHLIGHT'; /** * Sets the status of loading of a resource called "name" and "value" as the information status @@ -77,8 +80,8 @@ export const toggleGFIPanel = enabled => { export const printSubmit = attributes => { return (dispatch, getState) => { const state = getState() || {}; - const { outputFilename, layout = "A4 portrait", ...dataAttributes } = - attributes || {}; + const {outputFilename, layout = "A4 portrait", ...dataAttributes} = + attributes || {}; const { layers, scaleForZoom, @@ -110,3 +113,38 @@ export const printSubmit = attributes => { }); }; }; + +/** + * Carries data needed for Get Feature Info request + * @param {object} point point clicked in this shape {latlng: {lat:1, lng:2}, pixel:{x:33 y:33}, modifiers:{} } + * @param {string} layer the name of the layer without workspace + * @param {object[]} [filterNameList=[]] list of layers to perform the GFI request + * @param {object} [overrideParams={}] a map based on name as key and object as value for overriding request params + * @param {string} [itemId=null] id of the item needed for filtering results + */ +export function featureInfoClick(point, layer, filterNameList = [], overrideParams = {}, itemId = null) { + return { + type: URBANISME_FEATURE_INFO_CLICK, + point, + layer, + filterNameList, + overrideParams, + itemId + }; +} + + +export function highlightFeature(point, feature, featureCrs) { + return { + type: URBANISME_HIGHLIGHT_FEATURE, + point, + feature, + featureCrs + }; +} + +export function resetFeatureHighlight() { + return { + type: URBANISME_RESET_FEATURE_HIGHLIGHT + }; +} diff --git a/js/extension/constants.js b/js/extension/constants.js index 28a1634..69e65e4 100644 --- a/js/extension/constants.js +++ b/js/extension/constants.js @@ -7,6 +7,7 @@ */ export const URBANISME_RASTER_LAYER_ID = "__URBANISME_RASTER_LAYER__"; +export const URBANISME_VECTOR_LAYER_ID = "__URBANISME_VECTOR_LAYER__"; export const CONTROL_NAME = "urbanisme"; export const DEFAULT_URBANISME_LAYER = "urbanisme_parcelle"; export const DEFAULT_CADASTRAPP_URL = "/cadastrapp/services"; @@ -15,6 +16,8 @@ export const URBANISME_TOOLS = { NRU: "NRU", ADS: "ADS" }; +export const URBANISME_OWNER = "URBANISME"; + export const ADS_DEFAULTS = { parcelle: `Eléments d\'informations applicables à la parcelle cadastrale`, secteur: `Secteur d\'instruction :`, diff --git a/js/extension/epics/__tests__/urbanisme-test.js b/js/extension/epics/__tests__/urbanisme-test.js index 3192319..3d9ab74 100644 --- a/js/extension/epics/__tests__/urbanisme-test.js +++ b/js/extension/epics/__tests__/urbanisme-test.js @@ -10,26 +10,72 @@ import expect from 'expect'; import { testEpic, addTimeoutEpic } from '@mapstore/epics/__tests__/epicTestUtils'; import { toggleControl, TOGGLE_CONTROL, setControlProperty } from '@mapstore/actions/controls'; import { clickOnMap } from '@mapstore/actions/map'; -import { PURGE_MAPINFO_RESULTS, TOGGLE_HIGHLIGHT_FEATURE, - TOGGLE_MAPINFO_STATE, FEATURE_INFO_CLICK, HIDE_MAPINFO_MARKER, loadFeatureInfo } from '@mapstore/actions/mapInfo'; -import { ADD_LAYER, REMOVE_LAYER } from '@mapstore/actions/layers'; +import { PURGE_MAPINFO_RESULTS, TOGGLE_MAPINFO_STATE, loadFeatureInfo } from '@mapstore/actions/mapInfo'; -import { setUpPluginEpic, toggleLandPlanningEpic, - cleanUpUrbanismeEpic, clickOnMapEventEpic, closeOnMeasureEnabledEpic, getFeatureInfoEpic, onClosePanelEpic, onToogleToolEpic } from '../urbanisme'; +import { + setUpPluginEpic, + toggleLandPlanningEpic, + cleanUpUrbanismeEpic, + clickOnMapEventEpic, + closeOnMeasureEnabledEpic, + getFeatureInfoEpic, + onClosePanelEpic, + onToogleToolEpic, + updateAdditionalLayerEpic, highlightFeatureEpic +} from '../urbanisme'; import { setUp, LOADING, SET_CONFIG, TOGGLE_TOOL, TOGGLE_VIEWER_PANEL, - SET_URBANISME_DATA, toggleGFIPanel, toggleUrbanismeTool + SET_URBANISME_DATA, + toggleGFIPanel, + toggleUrbanismeTool, + URBANISME_FEATURE_INFO_CLICK, + URBANISME_RESET_FEATURE_HIGHLIGHT, highlightFeature, resetFeatureHighlight, URBANISME_HIGHLIGHT_FEATURE } from '../../actions/urbanisme'; -import {DEFAULT_CADASTRAPP_URL, DEFAULT_URBANISMEAPP_URL, URBANISME_RASTER_LAYER_ID} from '../../constants'; +import { + DEFAULT_CADASTRAPP_URL, + DEFAULT_URBANISMEAPP_URL, + URBANISME_RASTER_LAYER_ID, + URBANISME_VECTOR_LAYER_ID +} from '../../constants'; import axios from 'axios'; import MockAdapter from "axios-mock-adapter"; import {setAPIURL} from "@js/extension/api"; +import {REMOVE_ADDITIONAL_LAYER, UPDATE_ADDITIONAL_LAYER} from "@mapstore/actions/additionallayers"; const CADASTRAPP_URL = DEFAULT_CADASTRAPP_URL; const URBANISMEAPP_URL = DEFAULT_URBANISMEAPP_URL; + +const layersList = [ + { + id: URBANISME_RASTER_LAYER_ID, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_RASTER_LAYER_ID, + type: 'wms', + name: 'urbanisme_parcelle', + url: 'layer_url', + visibility: true, + search: {} + } + }, + { + id: URBANISME_VECTOR_LAYER_ID, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_VECTOR_LAYER_ID, + features: [], + type: 'vector', + name: 'selectedPlot', + visibility: true + } + } +]; + describe('Urbanisme EPICS', () => { let mockAxios; setAPIURL(); @@ -65,12 +111,9 @@ describe('Urbanisme EPICS', () => { expect(actions.length).toBe(4); actions.map(action=> { switch (action.type) { - case ADD_LAYER: - expect(action.layer).toBeTruthy(); - expect(action.layer.id).toBe(URBANISME_RASTER_LAYER_ID); - break; - case TOGGLE_HIGHLIGHT_FEATURE: - expect(action.enabled).toBe(true); + case UPDATE_ADDITIONAL_LAYER: + expect(action.options).toBeTruthy(); + expect([URBANISME_RASTER_LAYER_ID, URBANISME_VECTOR_LAYER_ID].includes(action.options.id)).toBeTruthy(); break; case TOGGLE_MAPINFO_STATE: break; @@ -89,7 +132,7 @@ describe('Urbanisme EPICS', () => { const state = { controls: { urbanisme: { enabled: false }}, urbanisme: { config: { cadastreWMSURL: "/cadastreWMSURL"}}, - layers: {flat: [{id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}]}, + additionallayers: layersList, mapInfo: {enabled: false} }; testEpic( @@ -100,8 +143,8 @@ describe('Urbanisme EPICS', () => { expect(actions.length).toBe(3); actions.map(action=>{ switch (action.type) { - case REMOVE_LAYER: - expect(action.layerId).toBe(URBANISME_RASTER_LAYER_ID); + case REMOVE_ADDITIONAL_LAYER: + expect([URBANISME_RASTER_LAYER_ID, URBANISME_VECTOR_LAYER_ID].includes(action.id)).toBeTruthy(); break; case PURGE_MAPINFO_RESULTS: break; @@ -120,7 +163,7 @@ describe('Urbanisme EPICS', () => { const state = { controls: { urbanisme: { enabled: true}}, urbanisme: { activeTool: "NRU" }, - layers: {flat: [{id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}]}, + additionallayers: layersList, mapInfo: {enabled: false} }; testEpic( @@ -131,9 +174,7 @@ describe('Urbanisme EPICS', () => { expect(actions.length).toBe(3); actions.map(action=>{ switch (action.type) { - case TOGGLE_HIGHLIGHT_FEATURE: - break; - case FEATURE_INFO_CLICK: + case URBANISME_FEATURE_INFO_CLICK: expect(action.point).toBeTruthy(); break; case LOADING: @@ -154,7 +195,7 @@ describe('Urbanisme EPICS', () => { const state = { controls: { urbanisme: { enabled: false}}, urbanisme: { activeTool: "NRU", showGFIPanel: true }, - layers: {flat: [{id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}]}, + additionallayers: layersList, mapInfo: {enabled: false} }; testEpic( @@ -174,7 +215,7 @@ describe('Urbanisme EPICS', () => { case SET_URBANISME_DATA: expect(action.property).toBe(null); break; - case TOGGLE_HIGHLIGHT_FEATURE: + case URBANISME_RESET_FEATURE_HIGHLIGHT: break; default: expect(true).toBe(false); @@ -237,10 +278,7 @@ describe('Urbanisme EPICS', () => { expect(actions.length).toBe(1); actions.map(action=>{ switch (action.type) { - case HIDE_MAPINFO_MARKER: - break; - case TOGGLE_HIGHLIGHT_FEATURE: - expect(action.enabled).toBe(false); + case URBANISME_RESET_FEATURE_HIGHLIGHT: break; default: expect(true).toBe(false); @@ -251,7 +289,7 @@ describe('Urbanisme EPICS', () => { state); }); - it('getFeatureInfoEpic load feature info', (done) => { + it('getFeatureInfoEpic load feature info NRU tool OLD', (done) => { mockAxios.onGet(`${CADASTRAPP_URL}/getCommune`).reply(200, [{libcom_min: "min"}]); mockAxios.onGet(`${CADASTRAPP_URL}/getParcelle`).reply(200, [{parcelle: "parcelle", ccopre: "ccopre", ccosec: "ccosec", dnupla: "dnupla", dnvoiri: "dnvoiri", cconvo: "cconvo", dvoilib: "dvoilib", dcntpa: "dcntpa"}]); @@ -263,10 +301,13 @@ describe('Urbanisme EPICS', () => { mockAxios.onGet('/urbanisme/renseignUrbaInfos').reply(200, { date_pci: '2020/10/11', date_ru: '06/2020'}); const urbanismeLayer = {id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}; - const layerMetaData = {features: [{id: "urbanisme_1", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}]}; + const layerMetaData = { + features: [{id: "urbanisme_1", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}], + featuresCrs: "EPSG:4326" + }; const state = { controls: { measure: { enabled: true}, urbanisme: { enabled: true}}, urbanisme: { activeTool: "NRU"}, - layers: {flat: [urbanismeLayer]} + additionalLayers: [urbanismeLayer] }; const attributes = {"commune": "min", "parcelle": "parcelle", "numero": "dnupla", "contenanceDGFiP": "dcntpa", "codeSection": "ccopreccosec", "adresseCadastrale": "dnvoiri cconvo dvoilib", "libelles": ["Test"], "nomProprio": "", "codeProprio": "codeProprio", "adresseProprio": " ", "surfaceSIG": "surfc", "datePCI": "0/10/11", "dateRU": "06/2020"}; testEpic( @@ -295,6 +336,7 @@ describe('Urbanisme EPICS', () => { }, state); }); + it('getFeatureInfoEpic returns even with empty data', (done) => { mockAxios.onGet(`${CADASTRAPP_URL}/getCommune`).reply(200, []); mockAxios.onGet(`${CADASTRAPP_URL}/getParcelle`).reply(200, []); @@ -305,11 +347,14 @@ describe('Urbanisme EPICS', () => { }); mockAxios.onGet('/urbanisme/renseignUrbaInfos').reply(200, { date_pci: '2020/10/11', date_ru: '06/2020'}); - const urbanismeLayer = {id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}; - const layerMetaData = {features: [{id: "urbanisme_1", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}]}; + const urbanismeLayer = layersList[0].options; + const layerMetaData = { + features: [{id: "urbanisme_1", type: "Feature", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}], + featuresCrs: "EPSG:4326" + }; const state = { controls: { measure: { enabled: true}, urbanisme: { enabled: true}}, urbanisme: { activeTool: "NRU"}, - layers: {flat: [urbanismeLayer]} + additionalLayers: [urbanismeLayer] }; const attributes = {"datePCI": "0/10/11", "dateRU": "06/2020", libelles: []}; testEpic( @@ -339,16 +384,20 @@ describe('Urbanisme EPICS', () => { state); }); - it('getFeatureInfoEpic load feature info', (done) => { + it('getFeatureInfoEpic load feature info ADS tool', (done) => { mockAxios.onGet(`${URBANISMEAPP_URL}/adsSecteurInstruction`).reply(200, {nom: "nom", ini_instru: "ini"}); mockAxios.onGet(`${URBANISMEAPP_URL}/adsAutorisation`).reply(200, {numdossier: [{numdossier: "test"}]}); mockAxios.onGet(`${URBANISMEAPP_URL}/quartier`).reply(200, {numnom: "num", parcelle: "test"}); - const urbanismeLayer = {id: URBANISME_RASTER_LAYER_ID, name: "URBANISME_PARCELLE"}; - const layerMetaData = {features: [{id: "urbanisme_1", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}]}; - const state = { controls: { measure: { enabled: true}, urbanisme: { enabled: true}}, + const urbanismeLayer = layersList[0].options; + const layerMetaData = { + features: [{id: "urbanisme_1", type: "Feature", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}], + featuresCrs: "EPSG:4326" + }; + const state = { + controls: { measure: { enabled: true}, urbanisme: { enabled: true}}, urbanisme: { activeTool: "ADS"}, - layers: {flat: [urbanismeLayer]} + additionalLayers: [urbanismeLayer] }; const attributes = {"nom": "nom", "ini_instru": "ini", "num_dossier": ["test"], "num_nom": "num", "id_parcelle": "test"}; testEpic( @@ -381,16 +430,13 @@ describe('Urbanisme EPICS', () => { it('onToogleToolEpic clean up activities of previous tool', (done) => { testEpic( onToogleToolEpic, - 4, + 3, toggleUrbanismeTool('NRU'), actions => { - expect(actions.length).toBe(4); + expect(actions.length).toBe(3); actions.map(action=>{ switch (action.type) { - case HIDE_MAPINFO_MARKER: - break; - case TOGGLE_HIGHLIGHT_FEATURE: - expect(action.enabled).toBe(false); + case URBANISME_RESET_FEATURE_HIGHLIGHT: break; case SET_URBANISME_DATA: expect(action.property).toEqual(null); @@ -406,4 +452,136 @@ describe('Urbanisme EPICS', () => { }, {}); }); + it('updateAdditionalLayerEpic on feature highlight test', (done) => { + const state = { + controls: { urbanisme: { enabled: true }, measure: {enabled: false}}, + urbanisme: { config: { cadastreWMSURL: "/cadastreWMSURL"}}, + mapInfo: {enabled: false} + }; + const clickedPoint = { + pixel: { + x: 941, + y: 490 + }, + latlng: { + lat: 48.11045432031648, + lng: -1.6808223724365234 + }, + rawPos: [ + -187108.2906135758, + 6125250.209089858 + ], + modifiers: { + alt: false, + ctrl: false, + metaKey: false, + shift: false + } + }; + const layerMetaData = { + features: [{id: "urbanisme_1", type: "Feature", geometry: {type: "Polygon", coordinates: [[[-1, 1], [-2, 2], [-3, 3], [-4, 4]]]}, properties: {id_parc: "350238000BM0027"}}], + featuresCrs: "EPSG:4326" + }; + testEpic( + updateAdditionalLayerEpic, + 1, + highlightFeature(clickedPoint, [layerMetaData?.features[0]], layerMetaData.featuresCrs), + actions => { + expect(actions.length).toBe(1); + actions.map(action=>{ + switch (action.type) { + case UPDATE_ADDITIONAL_LAYER: + expect(action.id).toBe(URBANISME_VECTOR_LAYER_ID); + expect(action.options.features.length).toBe(2); + expect(action.options.features[0].id).toBe('urbanisme_1'); + expect(action.options.features[1].id).toBe('get-feature-info-point'); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, state); + }); + + it('updateAdditionalLayerEpic on feature highlight reset test', (done) => { + const state = { + controls: { urbanisme: { enabled: true }, measure: {enabled: false}}, + urbanisme: { config: { cadastreWMSURL: "/cadastreWMSURL"}}, + mapInfo: {enabled: false} + }; + testEpic( + updateAdditionalLayerEpic, + 1, + resetFeatureHighlight(), + actions => { + expect(actions.length).toBe(1); + actions.map(action=>{ + switch (action.type) { + case UPDATE_ADDITIONAL_LAYER: + expect(action.id).toBe(URBANISME_VECTOR_LAYER_ID); + expect(action.options.features.length).toBe(0); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, state); + }); + + it('highlightFeatureEpic test', (done) => { + const urbanismeLayer = layersList[0].options; + const layerMetaData = { + features: [{id: "urbanisme_1", type: "Feature", geometry: {type: "Polygon", coordinates: [[-1, 1], [-2, 2], [-3, 3], [-4, 4]]}, properties: {id_parc: "350238000BM0027"}}], + featuresCrs: "EPSG:4326" + }; + const state = { + controls: { urbanisme: { enabled: true}}, + urbanisme: { + config: { cadastreWMSURL: "/cadastreWMSURL"}, + activeTool: "ADS", + clickPoint: { + pixel: { + x: 941, + y: 490 + }, + latlng: { + lat: 48.11045432031648, + lng: -1.6808223724365234 + }, + rawPos: [ + -187108.2906135758, + 6125250.209089858 + ], + modifiers: { + alt: false, + ctrl: false, + metaKey: false, + shift: false + } + }}, + additionalLayers: [urbanismeLayer] + }; + testEpic( + highlightFeatureEpic, + 1, + loadFeatureInfo(1, "Response", {service: "WMS", id: URBANISME_RASTER_LAYER_ID}, layerMetaData, urbanismeLayer), + actions => { + expect(actions.length).toBe(1); + actions.map(action=>{ + switch (action.type) { + case URBANISME_HIGHLIGHT_FEATURE: + expect(action.point).toEqual(state.urbanisme.clickPoint); + expect(action.feature).toEqual([layerMetaData.features[0]]); + expect(action.featureCrs).toBe(layerMetaData.featuresCrs); + break; + default: + expect(true).toBe(false); + } + }); + done(); + }, state); + }); }); + diff --git a/js/extension/epics/mapSelection.js b/js/extension/epics/mapSelection.js new file mode 100644 index 0000000..57390ab --- /dev/null +++ b/js/extension/epics/mapSelection.js @@ -0,0 +1,47 @@ +import {getLayerJSONFeature} from '@mapstore/observables/wfs'; +import {urbanismeLayerSelector} from "@js/extension/selectors/urbanisme"; + + +/** + * Generate a simple point geometry using position data + * @param {object} point/position data from the map + * @return {{coordinates: [number, string], projection: string, type: string}|*} geometry of type Point + */ +export const getPointFeature = point => { + const geometry = point?.geometricFilter?.value?.geometry; + if (geometry) { + return geometry; + } + let lng = point.lng || point.latlng.lng; + let lngCorrected = lng - 360 * Math.floor(lng / 360 + 0.5); + return { + coordinates: [lngCorrected, point.lat || point.latlng.lat], + projection: "EPSG:4326", + type: "Point" + }; +}; + +function createRequest(geometry, layer) { + return getLayerJSONFeature(layer, { + filterType: "OGC", // CQL doesn't support LineString yet + featureTypeName: layer?.search?.name ?? layer?.name, + typeName: layer?.search?.name ?? layer?.name, // the layer name is not used + ogcVersion: '1.1.0', + spatialField: { + attribute: "geom", // TODO: get the geom attribute from config + geometry, + operation: "INTERSECTS" + } + }); +} + +export const getUrbanismeFeatures = (geometry, getState) => { + const layer = urbanismeLayerSelector(getState()); + return createRequest(geometry, layer) + .map(({features = [], ...rest} = {}) => { + return { + ...rest, + features: features + }; + }); +}; diff --git a/js/extension/epics/urbanisme.js b/js/extension/epics/urbanisme.js index a86bf89..78406cf 100644 --- a/js/extension/epics/urbanisme.js +++ b/js/extension/epics/urbanisme.js @@ -7,71 +7,87 @@ */ import * as Rx from "rxjs"; -import { get, isEmpty } from "lodash"; +import {get, isEmpty, omit} from "lodash"; +import uuid from 'uuid'; +import {SET_CONTROL_PROPERTY, TOGGLE_CONTROL, toggleControl} from "@mapstore/actions/controls"; +import {ANNOTATIONS} from "@mapstore/utils/AnnotationsUtils"; +import {error} from "@mapstore/actions/notifications"; +import {CLICK_ON_MAP} from "@mapstore/actions/map"; +import {removeAdditionalLayer, updateAdditionalLayer} from '@mapstore/actions/additionallayers'; import { - TOGGLE_CONTROL, - toggleControl, - SET_CONTROL_PROPERTY -} from "@mapstore/actions/controls"; -import { ANNOTATIONS } from "@mapstore/utils/AnnotationsUtils"; -import { error } from "@mapstore/actions/notifications"; -import { CLICK_ON_MAP } from "@mapstore/actions/map"; -import { addLayer, removeLayer } from "@mapstore/actions/layers"; -import { - toggleMapInfoState, - toggleHighlightFeature, - purgeMapInfoResults, - featureInfoClick, + errorFeatureInfo, + exceptionsFeatureInfo, + getVectorInfo, LOAD_FEATURE_INFO, - hideMapinfoMarker + loadFeatureInfo, + newMapInfoRequest, + noQueryableLayers, + purgeMapInfoResults, SET_MAP_TRIGGER, setMapTrigger, + TOGGLE_MAPINFO_STATE, + toggleMapInfoState } from "@mapstore/actions/mapInfo"; -import { localConfigSelector } from '../../../MapStore2/web/client/selectors/localConfig'; +import {localConfigSelector} from '@mapstore/selectors/localConfig'; import proj4 from 'proj4'; -import { - createControlEnabledSelector, - measureSelector -} from "@mapstore/selectors/controls"; -import { wrapStartStop } from "@mapstore/observables/epics"; +import {createControlEnabledSelector, measureSelector} from "@mapstore/selectors/controls"; +import {wrapStartStop} from "@mapstore/observables/epics"; import { - SET_UP, - setConfiguration, + featureInfoClick, + highlightFeature, loading, + resetFeatureHighlight, + SET_UP, setAttributes, - toggleUrbanismeTool, - toggleGFIPanel, + setConfiguration, + TOGGLE_TOOL, TOGGLE_VIEWER_PANEL, - TOGGLE_TOOL + toggleGFIPanel, + toggleUrbanismeTool, + URBANISME_FEATURE_INFO_CLICK, + URBANISME_HIGHLIGHT_FEATURE, + URBANISME_RESET_FEATURE_HIGHLIGHT } from "../actions/urbanisme"; import { - configSelector, - configLoadSelector, - urbanismeLayerSelector, - urbanimseControlSelector, activeToolSelector, + clickPointSelector, + configLoadSelector, + configSelector, + identifyOptionsSelector, + itemIdSelector, + lpGFIPanelSelector, + overrideParamsSelector, printingSelector, - lpGFIPanelSelector + urbanimseControlSelector, + urbanismeLayerSelector } from "../selectors/urbanisme"; import { - getConfiguration, + getAdsAutorisation, + getAdsSecteurInstruction, getCommune, + getConfiguration, getFIC, getParcelle, - getRenseignUrba, - getRenseignUrbaInfos, getQuartier, - getAdsSecteurInstruction, - getAdsAutorisation + getRenseignUrba, + getRenseignUrbaInfos } from "../api"; import { CONTROL_NAME, + DEFAULT_URBANISME_LAYER, + URBANISME_OWNER, URBANISME_RASTER_LAYER_ID, URBANISME_TOOLS, - DEFAULT_URBANISME_LAYER + URBANISME_VECTOR_LAYER_ID } from "../constants"; +import {localizedLayerStylesEnvSelector} from "@mapstore/selectors/localizedLayerStyles"; +import {buildIdentifyRequest, clickedPointToGeoJson, filterRequestParams} from "@mapstore/utils/MapInfoUtils"; +import {getFeatureInfo} from "@mapstore/api/identify"; +import {reprojectGeoJson} from "@mapstore/utils/CoordinatesUtils"; +import {highlightStyleSelector, mapInfoDisabledSelector, mapTriggerSelector} from "@mapstore/selectors/mapInfo"; +import {styleFeatures} from "@js/extension/utils/UrbanismeUtils"; /** * Ensures that config for the urbanisme tool is fetched and loaded @@ -86,7 +102,7 @@ export const setUpPluginEpic = (action$, store) => // adds projections from localConfig.json // The extension do not see the state proj4 of MapStore (can not reproject in custom CRS as mapstore does) // so they have to be registered again in the extension. - const { projectionDefs = [] } = localConfigSelector(store.getState()) ?? {}; + const {projectionDefs = []} = localConfigSelector(store.getState()) ?? {}; projectionDefs.forEach((proj) => { proj4.defs(proj.code, proj.def); }); @@ -100,7 +116,10 @@ export const setUpPluginEpic = (action$, store) => loading(false, 'configLoading'), e => { console.log(e); // eslint-disable-line no-console - return Rx.Observable.of(error({ title: "Error", message: "Unable to setup urbanisme app" }), loading(false, 'configLoading')); + return Rx.Observable.of(error({ + title: "Error", + message: "Unable to setup urbanisme app" + }), loading(false, 'configLoading')); } ) @@ -117,33 +136,50 @@ export const setUpPluginEpic = (action$, store) => export const toggleLandPlanningEpic = (action$, store) => action$ .ofType(TOGGLE_CONTROL) - .filter(({ control }) => control === CONTROL_NAME) + .filter(({control}) => control === CONTROL_NAME) .switchMap(() => { const state = store.getState(); - const { cadastreWMSURL: url, layer: name = DEFAULT_URBANISME_LAYER } = configSelector(state) || {}; + const {cadastreWMSURL: url, layer: name = DEFAULT_URBANISME_LAYER} = configSelector(state) || {}; const enabled = urbanimseControlSelector(state); const mapInfoEnabled = get(state, "mapInfo.enabled"); const isMeasureEnabled = measureSelector(state); + const mapHoverTrigger = mapTriggerSelector(state); if (enabled) { return Rx.Observable.from([ - addLayer({ - id: URBANISME_RASTER_LAYER_ID, - type: "wms", - name, - url, - visibility: true, - search: {} - }), - toggleHighlightFeature(true) + updateAdditionalLayer( + URBANISME_RASTER_LAYER_ID, + URBANISME_OWNER, + 'overlay', + { + id: URBANISME_RASTER_LAYER_ID, + type: "wms", + name, + url, + visibility: true, + search: {} + }, true), + updateAdditionalLayer( + URBANISME_VECTOR_LAYER_ID, + URBANISME_OWNER, + 'overlay', + { + id: URBANISME_VECTOR_LAYER_ID, + features: [], + type: "vector", + name: "selectedPlot", + visibility: true + }) ]).concat([ ...(mapInfoEnabled ? [toggleMapInfoState()] : []), - ...(isMeasureEnabled ? [toggleControl("measure")] : []) + ...(isMeasureEnabled ? [toggleControl("measure")] : []), + ...(mapHoverTrigger === 'hover' ? [setMapTrigger("click")] : []) ]); } const layer = urbanismeLayerSelector(state); return !isEmpty(layer) ? Rx.Observable.from([ - removeLayer(URBANISME_RASTER_LAYER_ID), + removeAdditionalLayer({id: URBANISME_RASTER_LAYER_ID, owner: URBANISME_OWNER}), + removeAdditionalLayer({id: URBANISME_VECTOR_LAYER_ID, owner: URBANISME_OWNER}), purgeMapInfoResults() ]).concat(!mapInfoEnabled ? [toggleMapInfoState()] : []) : Rx.Observable.empty(); @@ -155,29 +191,26 @@ export const toggleLandPlanningEpic = (action$, store) => * @param {observable} action$ manages `CLICK_ON_MAP` * @return {observable} */ -export const clickOnMapEventEpic = (action$, { getState }) => +export const clickOnMapEventEpic = (action$, {getState}) => action$ .ofType(CLICK_ON_MAP) .filter(() => !isEmpty(urbanismeLayerSelector(getState()))) - .switchMap(({ point, layer }) => { + .switchMap(({point, layer}) => { const state = getState(); const isPrinting = printingSelector(state); const activeTool = activeToolSelector(state); const urbanismeEnabled = urbanimseControlSelector(state); - const mapInfoEnabled = get(state, "mapInfo.enabled", false); + const mapInfoEnabled = !mapInfoDisabledSelector(state); if (mapInfoEnabled) { return urbanismeEnabled ? Rx.Observable.of(toggleControl(CONTROL_NAME)) : Rx.Observable.empty(); } return !isEmpty(activeTool) && !isPrinting - ? Rx.Observable.concat( - Rx.Observable.of(toggleHighlightFeature(true)), - Rx.Observable.of( - featureInfoClick(point, layer), - setAttributes(null), - loading(true, "dataLoading") - ) + ? Rx.Observable.of( + featureInfoClick(point, layer), + setAttributes(null), + loading(true, "dataLoading") ) : Rx.Observable.empty(); }); @@ -188,20 +221,20 @@ export const clickOnMapEventEpic = (action$, { getState }) => * @param {observable} action$ manages `TOGGLE_CONTROL` * @return {observable} */ -export const cleanUpUrbanismeEpic = (action$, { getState }) => +export const cleanUpUrbanismeEpic = (action$, {getState}) => action$ .ofType(TOGGLE_CONTROL) - .filter(({ control }) => { + .filter(({control}) => { const isUrbanismeEnabled = urbanimseControlSelector(getState()); const isAnnotationsEnabled = createControlEnabledSelector(ANNOTATIONS)( getState() ); return ( (control === CONTROL_NAME && !isUrbanismeEnabled) || - (control === ANNOTATIONS && isAnnotationsEnabled && isUrbanismeEnabled) + (control === ANNOTATIONS && isAnnotationsEnabled && isUrbanismeEnabled) ); }) - .switchMap(({ control }) => { + .switchMap(({control}) => { const state = getState(); const activeTool = activeToolSelector(state); const gfiPanelEnabled = lpGFIPanelSelector(state); @@ -211,7 +244,7 @@ export const cleanUpUrbanismeEpic = (action$, { getState }) => ...(!isEmpty(activeTool) ? [toggleUrbanismeTool(null)] : []), ...(gfiPanelEnabled ? [toggleGFIPanel(false)] : []), setAttributes(null), - toggleHighlightFeature(false) + resetFeatureHighlight() ]); } else if (control === ANNOTATIONS) { observable$ = Rx.Observable.of(toggleControl(CONTROL_NAME)); @@ -225,11 +258,11 @@ export const cleanUpUrbanismeEpic = (action$, { getState }) => * @param {observable} action$ manages `SET_CONTROL_PROPERTY` * @return {observable} */ -export const closeOnMeasureEnabledEpic = (action$, { getState }) => +export const closeOnMeasureEnabledEpic = (action$, {getState}) => action$ .ofType(SET_CONTROL_PROPERTY) .filter( - ({ control }) => control === "measure" && measureSelector(getState()) + ({control}) => control === "measure" && measureSelector(getState()) ) .switchMap(() => { const urbanismeEnabled = urbanimseControlSelector(getState()); @@ -238,6 +271,41 @@ export const closeOnMeasureEnabledEpic = (action$, { getState }) => : Rx.Observable.empty(); }); +/** + * Ensures that the urbanisme plugin is closed when Identify tool is activated + * @memberof epics.urbanisme + * @param {observable} action$ manages `SET_CONTROL_PROPERTY` + * @return {observable} + */ +export const closeOnIdentifyEnabledEpic = (action$, {getState}) => + action$ + .ofType(TOGGLE_MAPINFO_STATE) + .filter(() => !mapInfoDisabledSelector(getState())) + .switchMap(() => { + const urbanismeEnabled = urbanimseControlSelector(getState()); + return urbanismeEnabled + ? Rx.Observable.of(toggleControl(CONTROL_NAME)) + : Rx.Observable.empty(); + }); + +/** + * Ensures that the urbanisme plugin is closed when Map info trigger + * is changed to "hover" + * @memberof epics.urbanisme + * @param {observable} action$ manages `SET_CONTROL_PROPERTY` + * @return {observable} + */ +export const closeOnMapHoverEnabledEpic = (action$, {getState}) => + action$ + .ofType(SET_MAP_TRIGGER) + .filter(() => mapTriggerSelector(getState()) === 'hover') + .switchMap(() => { + const urbanismeEnabled = urbanimseControlSelector(getState()); + return urbanismeEnabled + ? Rx.Observable.of(toggleControl(CONTROL_NAME)) + : Rx.Observable.empty(); + }); + /** * Ensures that upon closing viewer panel, highlight of feature is disabled and map marker is hidden * @memberof epics.urbanisme @@ -247,9 +315,9 @@ export const closeOnMeasureEnabledEpic = (action$, { getState }) => export const onClosePanelEpic = action$ => action$ .ofType(TOGGLE_VIEWER_PANEL) - .filter(({ enabled }) => !enabled) + .filter(({enabled}) => !enabled) .switchMap(() => - Rx.Observable.of(hideMapinfoMarker(), toggleHighlightFeature(false)) + Rx.Observable.of(resetFeatureHighlight()) ); /** @@ -263,8 +331,7 @@ export const onToogleToolEpic = action$ => .ofType(TOGGLE_TOOL) .switchMap(() => Rx.Observable.from([ - hideMapinfoMarker(), - toggleHighlightFeature(false), + resetFeatureHighlight(), setAttributes(null), toggleGFIPanel(false) ]) @@ -275,22 +342,21 @@ export const onToogleToolEpic = action$ => * @param {observable} action$ manages `LOAD_FEATURE_INFO` * @return {observable} */ -export const getFeatureInfoEpic = (action$, { getState }) => +export const getFeatureInfoEpic = (action$, {getState}) => action$ .ofType(LOAD_FEATURE_INFO) .filter( - ({ layer }) => + ({layer}) => layer.id === URBANISME_RASTER_LAYER_ID && - !printingSelector(getState()) && - !isEmpty(activeToolSelector(getState())) + !printingSelector(getState()) && + !isEmpty(activeToolSelector(getState())) ) - .switchMap(({ layerMetadata }) => { + .switchMap(({layerMetadata}) => { const {idParcelleKey} = configSelector(getState()) ?? {}; const parcelleId = layerMetadata.features?.[0]?.properties?.[idParcelleKey ?? "id_parc"] || ""; const activeTool = activeToolSelector(getState()); if (isEmpty(parcelleId)) { return Rx.Observable.of( - hideMapinfoMarker(), loading(false, "dataLoading"), setAttributes(null) ); @@ -309,8 +375,8 @@ export const getFeatureInfoEpic = (action$, { getState }) => getFIC(parcelleId, 1), getRenseignUrbaInfos(codeCommune) ).switchMap( - ([commune, parcelle, lisbelle, propPrio, proprioSurf, dates]) => - Rx.Observable.of( + ([commune, parcelle, lisbelle, propPrio, proprioSurf, dates]) => { + return Rx.Observable.of( setAttributes({ ...commune, ...parcelle, @@ -319,31 +385,35 @@ export const getFeatureInfoEpic = (action$, { getState }) => ...proprioSurf, ...dates }) - ) + ); + } ); } else if (activeTool === URBANISME_TOOLS.ADS) { observable$ = Rx.Observable.forkJoin( getAdsSecteurInstruction(parcelleId), getAdsAutorisation(parcelleId), getQuartier(parcelleId) - ).switchMap(([adsSecteurInstruction, adsAutorisation, quartier]) => - Rx.Observable.of( + ).switchMap(([adsSecteurInstruction, adsAutorisation, quartier]) => { + return Rx.Observable.of( setAttributes({ ...adsSecteurInstruction, ...adsAutorisation, ...quartier }) - ) + ); + } ); } - return observable$.startWith(toggleGFIPanel(true)).let( + return observable$.startWith( + toggleGFIPanel(true) + ).let( wrapStartStop( loading(true, "dataLoading"), loading(false, "dataLoading"), e => { console.log(e); // eslint-disable-line no-console return Rx.Observable.of( - error({ title: "Error", message: "Unable to fetch data" }), + error({title: "Error", message: "Unable to fetch data"}), loading(false, "dataLoading") ); } @@ -351,6 +421,154 @@ export const getFeatureInfoEpic = (action$, { getState }) => ); }); +/** + * Ensures that loaded feature is highlighted + * @memberof epics.urbanisme + * @param {observable} action$ manages `LOAD_FEATURE_INFO` + * @return {observable} + */ +export const highlightFeatureEpic = (action$, {getState}) => + action$ + .ofType(LOAD_FEATURE_INFO) + .filter( + ({layer}) => + layer.id === URBANISME_RASTER_LAYER_ID && + !printingSelector(getState()) && + !isEmpty(activeToolSelector(getState())) + ) + .switchMap(({layerMetadata}) => { + const {idParcelleKey} = configSelector(getState()) ?? {}; + const clickedPoint = clickPointSelector(getState()); + const parcelleId = layerMetadata.features?.[0]?.properties?.[idParcelleKey ?? "id_parc"] || ""; + if (isEmpty(parcelleId)) { + return Rx.Observable.of( + resetFeatureHighlight(), + ); + } + return Rx.Observable.of( + highlightFeature(clickedPoint, [layerMetadata?.features[0]], layerMetadata.featuresCrs) + ); + }); + +/** + * Triggers data load on FEATURE_INFO_CLICK events + */ +export const getUrbanismeFeatureInfoOnFeatureInfoClick = (action$, { + getState = () => { + } +}) => + action$.ofType(URBANISME_FEATURE_INFO_CLICK) + .switchMap(({point, filterNameList = [], overrideParams = {}}) => { + // Reverse - To query layer in same order as in TOC + let queryableLayers = [urbanismeLayerSelector(getState())].filter(e => e); + if (queryableLayers.length === 0) { + return Rx.Observable.of(purgeMapInfoResults(), noQueryableLayers()); + } + + // TODO: make it in the application getState() + const excludeParams = ["SLD_BODY"]; + const includeOptions = [ + "buffer", + "cql_filter", + "filter", + "propertyName" + ]; + const out$ = Rx.Observable.from((queryableLayers.filter(l => { + // filtering a subset of layers + return filterNameList.length ? (filterNameList.filter(name => name.indexOf(l.name) !== -1).length > 0) : true; + }))) + .mergeMap(layer => { + let env = localizedLayerStylesEnvSelector(getState()); + let { + url, + request, + metadata + } = buildIdentifyRequest(layer, {...identifyOptionsSelector(getState()), env, point}); + // request override + if (itemIdSelector(getState()) && overrideParamsSelector(getState())) { + request = {...request, ...overrideParamsSelector(getState())[layer.name]}; + } + if (overrideParams[layer.name]) { + request = {...request, ...overrideParams[layer.name]}; + } + if (url) { + const basePath = url; + const requestParams = request; + const lMetaData = metadata; + const appParams = filterRequestParams(layer, includeOptions, excludeParams); + const itemId = itemIdSelector(getState()); + const reqId = uuid.v1(); + const param = {...appParams, ...requestParams}; + return getFeatureInfo(basePath, param, layer, {attachJSON: true, itemId}) + .map((response) => + response.data.exceptions + ? exceptionsFeatureInfo(reqId, response.data.exceptions, requestParams, lMetaData) + : loadFeatureInfo(reqId, response.data, requestParams, { + ...lMetaData, + features: response.features, + featuresCrs: response.featuresCrs + }, layer) + ) + .catch((e) => Rx.Observable.of(errorFeatureInfo(reqId, e.data || e.statusText || e.status, requestParams, lMetaData))) + .startWith(newMapInfoRequest(reqId, param)); + } + return Rx.Observable.of(getVectorInfo(layer, request, metadata, queryableLayers)); + }); + // NOTE: multiSelection is inside the event + // TODO: move this flag in the application state + if (point && point.modifiers && point.modifiers.ctrl === true && point.multiSelection) { + return out$; + } + return out$.startWith(purgeMapInfoResults()); + }); + +/** + * Updates additional layer with vector data to show highlighted feature and click marker + */ +export const updateAdditionalLayerEpic = (action$, { + getState = () => { + } +}) => + action$.ofType(URBANISME_HIGHLIGHT_FEATURE, URBANISME_RESET_FEATURE_HIGHLIGHT) + .switchMap(({ point = {}, feature, featureCrs, type }) => { + const state = getState(); + const enabled = urbanimseControlSelector(state); + if (enabled && type === URBANISME_HIGHLIGHT_FEATURE) { + const styledFeatures = styleFeatures(feature, omit(highlightStyleSelector(state), ["radius"])); + const features = styledFeatures && featureCrs ? styledFeatures.map( f => reprojectGeoJson( + f, + featureCrs, + )) : styledFeatures; + const markerFeature = clickedPointToGeoJson(point.latlng); + return Rx.Observable.of( + updateAdditionalLayer( + URBANISME_VECTOR_LAYER_ID, + URBANISME_OWNER, + 'overlay', + { + id: URBANISME_VECTOR_LAYER_ID, + features: features.concat(markerFeature), + type: "vector", + name: "selectedPlot", + visibility: true + }) + ); + } + return Rx.Observable.of( + updateAdditionalLayer( + URBANISME_VECTOR_LAYER_ID, + URBANISME_OWNER, + 'overlay', + { + id: URBANISME_VECTOR_LAYER_ID, + features: [], + type: "vector", + name: "selectedPlot", + visibility: true + }) + ); + }); + export default { toggleLandPlanningEpic, setUpPluginEpic, @@ -358,6 +576,11 @@ export default { getFeatureInfoEpic, onClosePanelEpic, cleanUpUrbanismeEpic, + onToogleToolEpic, + getUrbanismeFeatureInfoOnFeatureInfoClick, + highlightFeatureEpic, + updateAdditionalLayerEpic, closeOnMeasureEnabledEpic, - onToogleToolEpic + closeOnMapHoverEnabledEpic, + closeOnIdentifyEnabledEpic }; diff --git a/js/extension/reducers/__tests__/urbanisme-test.js b/js/extension/reducers/__tests__/urbanisme-test.js index 3fa73a6..f7d954e 100644 --- a/js/extension/reducers/__tests__/urbanisme-test.js +++ b/js/extension/reducers/__tests__/urbanisme-test.js @@ -7,7 +7,14 @@ */ import expect from 'expect'; -import {setConfiguration, toggleUrbanismeTool, loading, toggleGFIPanel, setAttributes } from '../../actions/urbanisme'; +import { + setConfiguration, + toggleUrbanismeTool, + loading, + toggleGFIPanel, + setAttributes, + featureInfoClick, highlightFeature, resetFeatureHighlight +} from '../../actions/urbanisme'; import urbanismeState from '../urbanisme'; import {URBANISME_TOOLS} from "@js/extension/constants"; @@ -36,4 +43,22 @@ describe('Urbanisme reducers', () => { const state = urbanismeState({}, setAttributes(attributes)); expect(state.attributes).toEqual(attributes); }); + it('URBANISME_FEATURE_INFO_CLICK', () => { + const state = urbanismeState({}, featureInfoClick( {}, 'test', [], {}, 'test')); + expect(state.clickPoint).toEqual({}); + expect(state.clickLayer).toBe('test'); + expect(state.filterNameList).toEqual([]); + expect(state.overrideParams).toEqual({}); + expect(state.itemId).toEqual('test'); + }); + it('URBANISME_HIGHLIGHT_FEATURE', () => { + const state = urbanismeState({}, highlightFeature( {}, {}, 'test')); + expect(state.highlightedFeature).toEqual({}); + expect(state.featureCrs).toBe('test'); + }); + it('URBANISME_RESET_FEATURE_HIGHLIGHT', () => { + const state = urbanismeState({}, resetFeatureHighlight()); + expect(state.highlightedFeature).toBe(null); + expect(state.featureCrs).toBe(null); + }); }); diff --git a/js/extension/reducers/urbanisme.js b/js/extension/reducers/urbanisme.js index 2b8039a..427f68f 100644 --- a/js/extension/reducers/urbanisme.js +++ b/js/extension/reducers/urbanisme.js @@ -12,7 +12,10 @@ import { LOADING, TOGGLE_VIEWER_PANEL, TOGGLE_TOOL, - SET_URBANISME_DATA, SET_UP + SET_URBANISME_DATA, SET_UP, + URBANISME_FEATURE_INFO_CLICK, + URBANISME_HIGHLIGHT_FEATURE, + URBANISME_RESET_FEATURE_HIGHLIGHT } from "../actions/urbanisme"; const initialState = { @@ -35,6 +38,25 @@ export default function urbanisme(state = initialState, action) { case TOGGLE_VIEWER_PANEL: { return set("showGFIPanel", action.enabled, state); } + case URBANISME_FEATURE_INFO_CLICK: { + return {...state, + clickPoint: action.point, + clickLayer: action.layer, + itemId: action.itemId, + overrideParams: action.overrideParams, + filterNameList: action.filterNameList + }; + } + case URBANISME_HIGHLIGHT_FEATURE: + return {...state, + highlightedFeature: action.feature, + featureCrs: action.featureCrs + }; + case URBANISME_RESET_FEATURE_HIGHLIGHT: + return {...state, + highlightedFeature: null, + featureCrs: null + }; default: return state; } diff --git a/js/extension/selectors/__tests__/urbanisme-test.js b/js/extension/selectors/__tests__/urbanisme-test.js index 0925135..dc7184e 100644 --- a/js/extension/selectors/__tests__/urbanisme-test.js +++ b/js/extension/selectors/__tests__/urbanisme-test.js @@ -8,15 +8,20 @@ import expect from 'expect'; -import { configSelector, +import { + configSelector, activeToolSelector, printingSelector, urbanimseControlSelector, attributesSelector, lpGFIPanelSelector, - urbanismeLayerSelector + urbanismeLayerSelector, + clickPointSelector, + urbanismePlotFeaturesSelector, + urbanismePlotFeatureCrsSelector, + urbanismeVectorLayerSelector } from '../urbanisme'; -import { URBANISME_RASTER_LAYER_ID, URBANISME_TOOLS } from '../../constants'; +import {URBANISME_RASTER_LAYER_ID, URBANISME_TOOLS, URBANISME_VECTOR_LAYER_ID} from '../../constants'; describe('Urbanisme selectors', () => { it('test configSelector', () => { @@ -79,15 +84,33 @@ describe('Urbanisme selectors', () => { }); it('test urbanismeLayerSelector', () => { const state = { - layers: { - flat: [{ + additionallayers: [ + { id: URBANISME_RASTER_LAYER_ID, - type: "wms", - name: "urbanisme_parcelle", - url: "/geoserver/wms", - visibility: true - }] - }, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_RASTER_LAYER_ID, + type: 'wms', + name: 'urbanisme_parcelle', + url: 'layer_url', + visibility: true, + search: {} + } + }, + { + id: URBANISME_VECTOR_LAYER_ID, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_VECTOR_LAYER_ID, + features: [], + type: 'vector', + name: 'selectedPlot', + visibility: true + } + } + ], urbanisme: { config: {prop: "A"} } @@ -96,4 +119,75 @@ describe('Urbanisme selectors', () => { expect(layer).toBeTruthy(); expect(layer.id).toEqual(URBANISME_RASTER_LAYER_ID); }); + it('test urbanismeVectorLayerSelector', () => { + const state = { + additionallayers: [ + { + id: URBANISME_RASTER_LAYER_ID, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_RASTER_LAYER_ID, + type: 'wms', + name: 'urbanisme_parcelle', + url: 'layer_url', + visibility: true, + search: {} + } + }, + { + id: URBANISME_VECTOR_LAYER_ID, + owner: 'URBANISME', + actionType: 'overlay', + options: { + id: URBANISME_VECTOR_LAYER_ID, + features: [], + type: 'vector', + name: 'selectedPlot', + visibility: true + } + } + ], + urbanisme: { + config: {prop: "A"} + } + }; + const layer = urbanismeVectorLayerSelector(state); + expect(layer).toBeTruthy(); + expect(layer.id).toEqual(URBANISME_VECTOR_LAYER_ID); + }); + it('test clickPointSelector', () => { + const state = { + urbanisme: { + config: {prop: "A"}, + clickPoint: { prop: "B"} + } + }; + const point = clickPointSelector(state); + expect(point).toBeTruthy(); + expect(point.prop).toBe("B"); + }); + it('test urbanismePlotFeaturesSelector', () => { + const state = { + urbanisme: { + config: {prop: "A"}, + highlightedFeature: [{ prop: "test"}] + } + }; + const feature = urbanismePlotFeaturesSelector(state); + expect(feature).toBeTruthy(); + expect(feature.length).toBe(1); + }); + it('test urbanismePlotFeatureCrsSelector', () => { + const state = { + urbanisme: { + config: {prop: "A"}, + highlightedFeature: [{ prop: "test"}], + featureCrs: 'test' + } + }; + const crs = urbanismePlotFeatureCrsSelector(state); + expect(crs).toBeTruthy(); + expect(crs).toBe('test'); + }); }); diff --git a/js/extension/selectors/urbanisme.js b/js/extension/selectors/urbanisme.js index 287576b..5f6d33d 100644 --- a/js/extension/selectors/urbanisme.js +++ b/js/extension/selectors/urbanisme.js @@ -5,9 +5,16 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import { getLayerFromId } from "@mapstore/selectors/layers"; +import {createStructuredSelector} from 'reselect'; -import { URBANISME_RASTER_LAYER_ID } from "../constants"; +import {additionalLayersSelector} from "@mapstore/selectors/additionallayers"; + +import {URBANISME_RASTER_LAYER_ID, URBANISME_VECTOR_LAYER_ID} from "../constants"; + +import {get} from 'lodash'; +import {mapSelector} from "@mapstore/selectors/map"; +import {currentLocaleSelector} from "@mapstore/selectors/locale"; +import {generalInfoFormatSelector} from "@mapstore/selectors/mapInfo"; export const configLoadSelector = state => state?.urbanisme?.configLoading; @@ -23,4 +30,45 @@ export const attributesSelector = state => state?.urbanisme?.attributes || {}; export const urbanimseControlSelector = state => state?.controls?.urbanisme?.enabled || false; -export const urbanismeLayerSelector = state => getLayerFromId(state, URBANISME_RASTER_LAYER_ID); +export const urbanismeLayerSelector = state => { + const additionalLayers = additionalLayersSelector(state) ?? []; + return additionalLayers.filter(({id}) => id === URBANISME_RASTER_LAYER_ID)?.[0]?.options; +}; + +export const urbanismeVectorLayerSelector = state => { + const additionalLayers = additionalLayersSelector(state) ?? []; + return additionalLayers.filter(({id}) => id === URBANISME_VECTOR_LAYER_ID)?.[0]?.options; +}; + +export const clickPointSelector = state => state.urbanisme?.clickPoint; +export const clickLayerSelector = state => state.urbanisme?.clickLayer; +export const itemIdSelector = state => state.urbanisme?.itemId; +export const overrideParamsSelector = state => state.urbanisme?.overrideParams || {}; +export const filterNameListSelector = state => state.urbanisme?.filterNameList || []; + +/** + * Defines the general options of the identifyTool to build the request + */ +export const identifyOptionsSelector = createStructuredSelector({ + format: generalInfoFormatSelector, + map: mapSelector, + point: clickPointSelector, + currentLocale: currentLocaleSelector, + maxItems: (state) => get(state, "mapInfo.configuration.maxItems") +}); + +/** + * Gets the current features to draw. + * @param {object} state the application state + */ +export function urbanismePlotFeaturesSelector(state) { + return state.urbanisme?.highlightedFeature || []; +} + +/** + * Gets the CRS for highlighted feature. + * @param {object} state the application state + */ +export const urbanismePlotFeatureCrsSelector = state => state.urbanisme?.featureCrs; + + diff --git a/js/extension/utils/UrbanismeUtils.js b/js/extension/utils/UrbanismeUtils.js index b0c066a..9423710 100644 --- a/js/extension/utils/UrbanismeUtils.js +++ b/js/extension/utils/UrbanismeUtils.js @@ -15,10 +15,13 @@ import { getMapfishLayersSpecification } from "@mapstore/utils/PrintUtils"; import { getScales, dpi2dpu } from "@mapstore/utils/MapUtils"; -import { reproject, normalizeSRS } from "@mapstore/utils/CoordinatesUtils"; -import { layerSelectorWithMarkers, getLayerFromName } from "@mapstore/selectors/layers"; -import { clickedPointWithFeaturesSelector } from "@mapstore/selectors/mapInfo"; -import { URBANISME_RASTER_LAYER_ID } from "@js/extension/constants"; +import {reproject, normalizeSRS} from "@mapstore/utils/CoordinatesUtils"; +import { getLayerFromName } from "@mapstore/selectors/layers"; +import { + clickPointSelector, + urbanismeLayerSelector, urbanismePlotFeaturesSelector +} from "@js/extension/selectors/urbanisme"; +import {geoJSONToLayer} from "@mapstore/utils/LayersUtils"; /** * Sets the state of the viewer panel (open/close) @@ -94,15 +97,18 @@ export const getUrbanismePrintSpec = state => { && l.visibility && !l.loadingError && (l.type === 'osm' ? ["EPSG:900913", "EPSG:3857"].includes(projection) : true ) // remove osm layer if projection is not compatible - ) || l.id === URBANISME_RASTER_LAYER_ID - ); - const { latlng = {} } = clickedPointWithFeaturesSelector(state); + ) + ).concat([urbanismeLayerSelector(state)]); + + const { latlng = {} } = clickPointSelector(state); const projectedCenter = reproject({ x: latlng.lng, y: latlng.lat }, "EPSG:4326", projection); // Only first feature of NRU/ADS is used - const clickedPointFeatures = layerSelectorWithMarkers(state).filter( - l => l.name === "GetFeatureInfoHighLight" - ); + const clickedPointFeatures = [geoJSONToLayer({ + type: 'FeatureCollection', + features: urbanismePlotFeaturesSelector(state) + }, 'selectedPlot')]; + const baseLayers = getMapfishLayersSpecification([...layersFiltered], {...spec, projection}, "map"); const vectorLayers = getMapfishLayersSpecification([...clickedPointFeatures], spec, "map"); // Update layerSpec to suit Urbanisme print specification @@ -130,3 +136,18 @@ export const getUrbanismePrintSpec = state => { .reverse(); return { layers: layerSpec, scaleForZoom, projectedCenter, dpi, projection }; }; + +/** + * Applies style to the features list + * @param features + * @param style + * @returns {*} + */ +export const styleFeatures = (features, style) => { + return features.map((feature) => { + return { + ...feature, + style + }; + }); +};