From 6e126f1ff61bf3280c481dff9e100a7c9132548c Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Thu, 30 Mar 2017 16:13:22 +0200 Subject: [PATCH 1/6] fixed #1634 added support to custom search services and minor fixes --- docma-config.json | 1 - docs/developer-guide/map-plugin.md | 2 +- web/client/api/searchText.js | 93 +++++++++--- .../mapcontrols/search/SearchBar.jsx | 6 +- web/client/epics/__tests__/search-test.js | 133 ++++++++++++++---- web/client/epics/search.js | 116 ++++++++------- web/client/plugins/Map.jsx | 2 +- web/client/plugins/Search.jsx | 11 +- 8 files changed, 253 insertions(+), 111 deletions(-) diff --git a/docma-config.json b/docma-config.json index eb40f9dd6a..e43df8879f 100644 --- a/docma-config.json +++ b/docma-config.json @@ -76,7 +76,6 @@ "label": "Download", "href": "index.html", "items": [ - { "label": "mvn clean install" }, { "label": "MapStore 2 Releases", "href": "https://github.com/geosolutions-it/MapStore2/releases", diff --git a/docs/developer-guide/map-plugin.md b/docs/developer-guide/map-plugin.md index 82621d8f94..b49c871245 100644 --- a/docs/developer-guide/map-plugin.md +++ b/docs/developer-guide/map-plugin.md @@ -90,7 +90,7 @@ module.exports = { }], "toolsOptions": { "test": { - "label": "ciao" + "label": "Hello" } ... } diff --git a/web/client/api/searchText.js b/web/client/api/searchText.js index 9f0034cd31..a32dc32ed9 100644 --- a/web/client/api/searchText.js +++ b/web/client/api/searchText.js @@ -9,40 +9,91 @@ const WFS = require('./WFS'); const assign = require('object-assign'); const GeoCodeUtils = require('../utils/GeoCodeUtils'); const {generateTemplateString} = require('../utils/TemplateUtils'); -/* -const toNominatim = (fc) => - fc.features && fc.features.map( (f) => ({ - boundingbox: f.properties.bbox, - lat: 1, - lon: 1, - display_name: `${f.properties.STATE_NAME} (${f.properties.STATE_ABBR})` - })); -*/ - -module.exports = { +const axios = require('axios'); +const urlUtil = require('url'); +let Services = { nominatim: (searchText, options = {}) => require('./Nominatim') .geocode(searchText, options) .then( res => GeoCodeUtils.nominatimToGeoJson(res.data)), - wfs: (searchText, {url, typeName, queriableAttributes, outputFormat="application/json", predicate ="ILIKE", staticFilter="", blacklist = [], item, ...params }) => { + wfs: (searchText, {url, typeName, queriableAttributes = [], outputFormat="application/json", predicate ="ILIKE", staticFilter="", blacklist = [], item, ...params }) => { // split into words and remove blacklisted words const staticFilterParsed = generateTemplateString(staticFilter || "")(item); let searchWords = searchText.split(" ").filter(w => w).filter( w => blacklist.indexOf(w.toLowerCase()) < 0 ); - // if the searchtext is empty use the full searchText + // if the array searchWords is empty, then use the full searchText if (searchWords.length === 0 ) { - searchWords = [searchText]; + searchWords = !!searchText ? [searchText] : []; + } + let filter; + if (searchWords.length > 0 ) { + filter = "(".concat( searchWords.map( (w) => queriableAttributes.map( attr => `${attr} ${predicate} '%${w.replace("'", "''")}%'`).join(" OR ")).join(') AND (')).concat(")"); } + + filter = filter ? filter.concat(staticFilterParsed) : staticFilterParsed || null; + return WFS .getFeatureSimple(url, assign({ - maxFeatures: 10, - startIndex: 0, - typeName, - outputFormat, - // create a filter like : `(ATTR ilike '%word1%') AND (ATTR ilike '%word2%')` - cql_filter: "(".concat( searchWords.map( (w) => queriableAttributes.map( attr => `${attr} ${predicate} '%${w.replace("'", "''")}%'`).join(" OR ")).join(') AND (')).concat(")") .concat(staticFilterParsed) - }, params)) + maxFeatures: 10, + typeName, + outputFormat, + // create a filter like : `(ATTR ilike '%word1%') AND (ATTR ilike '%word2%')` + cql_filter: filter + }, params)) .then( response => response.features ); + }, + bzVie: (searchText, {pathname, lang}) => { + let params = assign({}, {query: searchText, lang}); + let url = urlUtil.format({ + pathname, + query: params + }); + return axios.post(url).then( (res) => { + if (res && res.data && res.data.success) { + return res.data.vie.map((item) => { + return { + "type": "Feature", + "properties": { + "code": item.codice, + "desc": item.descrizione + } + }; + }); + } + return []; + }); + }, + bzCivico: (searchText, {pathname, item}) => { + let params = assign({}, {query: searchText, idVia: item.properties.code}); + let url = urlUtil.format({ + pathname, + query: params + }); + return axios.post(url).then( (res) => { + if (res && res.data && res.data.success) { + return res.data.vie.map((nestedItem) => { + return { + "type": "Feature", + "properties": { + "code": nestedItem.codice, + "desc": nestedItem.descrizione + } + }; + }); + } + return []; + }); } }; + +const Utils = { + setService: (type, fun) => { + Services[type] = fun; + }, + getService: (type) => { + return !!Services[type] ? Services[type] : null; + } +}; + +module.exports = {API: {Services, Utils}}; diff --git a/web/client/components/mapcontrols/search/SearchBar.jsx b/web/client/components/mapcontrols/search/SearchBar.jsx index 3e7ba314d7..2bbb2f21fb 100644 --- a/web/client/components/mapcontrols/search/SearchBar.jsx +++ b/web/client/components/mapcontrols/search/SearchBar.jsx @@ -40,7 +40,7 @@ require('./searchbar.css'); * @prop {number} blurResetDelay time to wait before to trigger onPurgeResults after blur event, if `hideOnBlur` is true * @prop {searchText} the text to display in the component * @prop {object[]} selectedItems the items selected. Must have `text` property to display - * @prop {boolean} autoFocusOnSelect if true, the comonent gets focus when items are added, or deleted but some item is still selected. Useful for continue writing after selecting an item (with nested services for instance) + * @prop {boolean} autoFocusOnSelect if true, the component gets focus when items are added, or deleted but some item is still selected. Useful for continue writing after selecting an item (with nested services for instance) * @prop {boolean} loading if true, shows the loading tool * @prop {object} error if not null, an error icon will be display * @prop {object} style css style to apply to the component @@ -154,9 +154,9 @@ let SearchBar = React.createClass({ }, render() { // const innerGlyphicon = ; - let placeholder; + let placeholder = "search.placeholder"; if (!this.props.placeholder && this.context.messages) { - let placeholderLocMessage = LocaleUtils.getMessageById(this.context.messages, this.props.placeholderMsgId); + let placeholderLocMessage = LocaleUtils.getMessageById(this.context.messages, this.props.placeholderMsgId || placeholder); if (placeholderLocMessage) { placeholder = placeholderLocMessage; } diff --git a/web/client/epics/__tests__/search-test.js b/web/client/epics/__tests__/search-test.js index 1e3d33b0ff..586a7d02aa 100644 --- a/web/client/epics/__tests__/search-test.js +++ b/web/client/epics/__tests__/search-test.js @@ -18,6 +18,37 @@ const rootEpic = combineEpics(searchEpic, searchItemSelected); const epicMiddleware = createEpicMiddleware(rootEpic); const mockStore = configureMockStore([epicMiddleware]); +const SEARCH_NESTED = 'SEARCH NESTED'; +const TEST_NESTED_PLACEHOLDER = 'TEST_NESTED_PLACEHOLDER'; +const STATE_NAME = 'STATE_NAME'; + +const nestedService = { + nestedPlaceholder: TEST_NESTED_PLACEHOLDER +}; +const TEXT = "Dinagat Islands"; +const item = { + "type": "Feature", + "bbox": [125, 10, 126, 11], + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": TEXT + }, + "__SERVICE__": { + searchTextTemplate: "${properties.name}", + displayName: "${properties.name}", + type: "wfs", + options: { + staticFilter: "${properties.name}" + }, + nestedPlaceholder: SEARCH_NESTED, + nestedPlaceholderMsgId: TEST_NESTED_PLACEHOLDER, + then: [nestedService] + } +}; + describe('search Epics', () => { let store; beforeEach(() => { @@ -36,7 +67,7 @@ describe('search Epics', () => { options: { url: 'base/web/client/test-resources/wfs/Wyoming.json', typeName: 'topp:states', - queriableAttributes: ['STATE_NAME'] + queriableAttributes: [STATE_NAME] } }] }; @@ -81,31 +112,6 @@ describe('search Epics', () => { }); it('searchItemSelected epic with nested services', () => { - let nestedService = { - nestedPlaceholder: "TEST_NESTED_PLACEHOLDER" - }; - const TEXT = "Dinagat Islands"; - const item = { - "type": "Feature", - "bbox": [125, 10, 126, 11], - "geometry": { - "type": "Point", - "coordinates": [125.6, 10.1] - }, - "properties": { - "name": TEXT - }, - "__SERVICE__": { - searchTextTemplate: "${properties.name}", - displayName: "${properties.name}", - type: "wfs", - options: { - staticFilter: "${properties.name}" - }, - nestedPlaceholder: "SEARCH NESTED", - then: [nestedService] - } - }; let action = selectSearchItem(item, { size: { width: 200, @@ -129,11 +135,82 @@ describe('search Epics', () => { } }); expect(actions[4].items).toEqual({ - placeholder: "SEARCH NESTED", + placeholder: SEARCH_NESTED, + placeholderMsgId: TEST_NESTED_PLACEHOLDER, text: TEXT }); expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); - expect(actions[5].searchText).toBe("Dinagat Islands"); + expect(actions[5].searchText).toBe(TEXT); + }); + it('searchItemSelected with geomService', () => { + const itemWithoutGeom = { + "type": "Feature", + "bbox": [125, 10, 126, 11], + "properties": { + "name": TEXT + }, + "__SERVICE__": { + searchTextTemplate: "${properties.name}", + displayName: "${properties.name}", + type: "wfs", + options: { + staticFilter: "${properties.name}" + }, + "geomService": { + type: 'wfs', + options: { + url: 'base/web/client/test-resources/wfs/Wyoming.json', + typeName: 'topp:states', + queriableAttributes: [STATE_NAME] + } + }, + nestedPlaceholder: SEARCH_NESTED, + nestedPlaceholderMsgId: TEST_NESTED_PLACEHOLDER + } + }; + + let action = selectSearchItem(itemWithoutGeom, { + size: { + width: 200, + height: 200 + }, + services: [{ + type: 'wfs', + options: { + url: 'base/web/client/test-resources/wfs/Wyoming.json', + typeName: 'topp:states', + queriableAttributes: [STATE_NAME] + } + }], + projection: "EPSG:4326" + }); + + store.dispatch( action ); + setTimeout(() => { + let actions = store.getActions(); + expect(actions.length).toBe(6); + expect(actions[1].type).toBe(CHANGE_MAP_VIEW); + expect(actions[2].type).toBe(TEXT_SEARCH_ADD_MARKER); + expect(actions[3].type).toBe(TEXT_SEARCH_RESULTS_PURGE); + expect(actions[4].type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED); + expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); + + expect(actions[4].services[0]).toEqual({ + ...nestedService, + options: { + item + } + }); + expect(actions[4].services[0].geometry).toExist(); + expect(actions[4].items).toEqual({ + placeholder: SEARCH_NESTED, + placeholderMsgId: TEST_NESTED_PLACEHOLDER, + text: TEXT + }); + expect(actions[5].searchText).toBe(TEXT); + expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); + + }, 400); }); }); diff --git a/web/client/epics/search.js b/web/client/epics/search.js index a439b92731..b0e01acbf9 100644 --- a/web/client/epics/search.js +++ b/web/client/epics/search.js @@ -6,8 +6,6 @@ * LICENSE file in the root directory of this source tree. */ -// var GeoCodingApi = require('../api/Nominatim'); - const {TEXT_SEARCH_STARTED, TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, @@ -20,14 +18,15 @@ const {TEXT_SEARCH_STARTED, searchTextChanged, resultsPurge } = require('../actions/search'); + const mapUtils = require('../utils/MapUtils'); const CoordinatesUtils = require('../utils/CoordinatesUtils'); const Rx = require('rxjs'); -const services = require('../api/searchText'); +const {API} = require('../api/searchText'); const {changeMapView} = require('../actions/map'); -const pointOnSurface = require('turf-point-on-surface'); const toBbox = require('turf-bbox'); const {generateTemplateString} = require('../utils/TemplateUtils'); +const assign = require('object-assign'); const {get} = require('lodash'); @@ -44,21 +43,26 @@ const searchEpic = action$ => .debounceTime(250) .switchMap( action => // create a stream of streams from array - Rx.Observable.from((action.services || [ {type: "nominatim"} ]) - // Create an stream for each Promise - .map( (service) => Rx.Observable.defer(() => services[service.type](action.searchText, service.options) - .then( (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) )) - .retryWhen(errors => errors.delay(200).scan((count, err) => { - if ( count >= 2) { - throw err; - } - return count + 1; - }, 0)) - )) + Rx.Observable.from( + (action.services || [ {type: "nominatim"} ]) + // Create an stream for each Service + .map( + (service) => Rx.Observable.defer(() => API.Utils.getService(service.type)(action.searchText, service.options) + .then( + (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) + )) + .retryWhen(errors => errors.delay(200).scan((count, err) => { + if ( count >= 2) { + throw err; + } + return count + 1; + }, 0)) + ) // map + ) // from // merge all results from the streams .mergeAll() .scan( (oldRes, newRes) => [...oldRes, ...newRes].sort( (a, b) => get(b, "__PRIORITY__") - get(a, "__PRIORITY__") ) .slice(0, 15)) - .map((results) => searchResultLoaded(results, false, services)) + .map((results) => searchResultLoaded(results, false, API.Services)) .takeUntil(action$.ofType([ TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, TEXT_SEARCH_ITEM_SELECTED])) .startWith(searchTextLoading(true)) .concat([searchTextLoading(false)]) @@ -78,42 +82,50 @@ const searchEpic = action$ => * @memberof epics.search * @return {Observable} */ + const searchItemSelected = action$ => action$.ofType(TEXT_SEARCH_ITEM_SELECTED) .switchMap(action => { - const item = action.item; + let itemSelectionStream = Rx.Observable.of(action.item) + // retrieve geometry from geomService or pass the item directly + .concatMap((item) => { + if (item && item.__SERVICE__ && item.__SERVICE__.geomService) { + let staticFilter = generateTemplateString(item.__SERVICE__.geomService.options.staticFilter || "")(item); + return Rx.Observable.fromPromise( + API.Utils.getService(item.__SERVICE__.geomService.type)("", assign( {}, item.__SERVICE__.geomService.options, { staticFilter } )) + .then(res => assign({}, item, {geometry: res[0].geometry} ) ) + ); + } + return Rx.Observable.of(action.item); + }).mergeMap((item) => { + let bbox = item.bbox || item.properties.bbox || toBbox(item); + let mapSize = action.mapConfig.size; + // zoom by the max. extent defined in the map's config + let newZoom = mapUtils.getZoomForExtent(CoordinatesUtils.reprojectBbox(bbox, "EPSG:4326", action.mapConfig.projection), mapSize, 0, 21, null); - let mapSize = action.mapConfig.size; - // zoom by the max. extent defined in the map's config - let bbox = item.bbox || item.properties.bbox || toBbox(item); - var newZoom = mapUtils.getZoomForExtent(CoordinatesUtils.reprojectBbox(bbox, "EPSG:4326", action.mapConfig.projection), mapSize, 0, 21, null); + // center by the max. extent defined in the map's config + let newCenter = mapUtils.getCenterForExtent(bbox, "EPSG:4326"); + let actions = [ + changeMapView(newCenter, newZoom, { + bounds: { + minx: bbox[0], + miny: bbox[1], + maxx: bbox[2], + maxy: bbox[3] + }, + crs: "EPSG:4326", + rotation: 0 + }, action.mapConfig.size, null, action.mapConfig.projection), + addMarker(item) + ]; + return actions; + }); - // center by the max. extent defined in the map's config - let newCenter = mapUtils.getCenterForExtent(bbox, "EPSG:4326"); - // let markerCoordinates = {lat: newCenter.y, lng: newCenter.x}; - const point = pointOnSurface(item); - if (point && point.geometry && point.geometry.coordinates) { - // markerCoordinates = {lat: point.geometry.coordinates[1], lng: point.geometry.coordinates[0]}; - } - let actions = [ - changeMapView(newCenter, newZoom, { - bounds: { - minx: bbox[0], - miny: bbox[1], - maxx: bbox[2], - maxy: bbox[3] - }, - crs: "EPSG:4326", - rotation: 0 - }, action.mapConfig.size, null, action.mapConfig.projection), - addMarker(item), - resultsPurge()]; + const item = action.item; let nestedServices = item && item.__SERVICE__ && item.__SERVICE__.then; - // if a nested service is present, select the item and the nested service - if (nestedServices) { - actions.push(selectNestedService( + let nestedServicesStream = nestedServices ? Rx.Observable.of(selectNestedService( nestedServices.map((nestedService) => ({ ...nestedService, options: { @@ -122,19 +134,19 @@ const searchItemSelected = action$ => } })), { text: generateTemplateString(item.__SERVICE__.displayName || "")(item), - placeholder: item.__SERVICE__.nestedPlaceholder && generateTemplateString(item.__SERVICE__.nestedPlaceholder || "")(item) + placeholder: item.__SERVICE__.nestedPlaceholder && generateTemplateString(item.__SERVICE__.nestedPlaceholder || "")(item), + placeholderMsgId: item.__SERVICE__.nestedPlaceholderMsgId && generateTemplateString(item.__SERVICE__.nestedPlaceholderMsgId || "")(item) }, - generateTemplateString(item.__SERVICE__.searchTextTemplate || "")(item) - )); - } + generateTemplateString(item.__SERVICE__.searchTextTemplate || "")(item) + )) : Rx.Observable.empty(); // if the service has a searchTextTemplate, use it to modify the search text to display let searchTextTemplate = item.__SERVICE__ && item.__SERVICE__.searchTextTemplate; - if ( searchTextTemplate ) { - actions.push(searchTextChanged(generateTemplateString(searchTextTemplate)(item))); - } - return Rx.Observable.from(actions); + let searchTextStream = searchTextTemplate ? Rx.Observable.of(searchTextChanged(generateTemplateString(searchTextTemplate)(item))) : Rx.Observable.empty(); + + return Rx.Observable.merge(itemSelectionStream, Rx.Observable.of(resultsPurge()), nestedServicesStream, searchTextStream); }); + /** * Actions for search * @name epics.search diff --git a/web/client/plugins/Map.jsx b/web/client/plugins/Map.jsx index f352c6e2ae..1ad394b0d0 100644 --- a/web/client/plugins/Map.jsx +++ b/web/client/plugins/Map.jsx @@ -97,7 +97,7 @@ let plugins; * }], * "toolsOptions": { * "test": { - * "label": "ciao" + * "label": "Hello" * } * ... * } diff --git a/web/client/plugins/Search.jsx b/web/client/plugins/Search.jsx index eb18c20af7..6ac6f90be0 100644 --- a/web/client/plugins/Search.jsx +++ b/web/client/plugins/Search.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2016, GeoSolutions Sas. + * Copyright 2017, GeoSolutions Sas. * All rights reserved. * * This source code is licensed under the BSD-style license found in the @@ -25,8 +25,7 @@ const searchSelector = createSelector([ error: searchState && searchState.error, loading: searchState && searchState.loading, searchText: searchState ? searchState.searchText : "", - selectedItems: searchState && searchState.selectedItems, - selectedServices: searchState && searchState.selectedServices + selectedItems: searchState && searchState.selectedItems })); const SearchBar = connect(searchSelector, { @@ -102,8 +101,11 @@ const ToggleButton = require('./searchbar/ToggleButton'); * "blackist": [... an array of strings to exclude from the final search filter ] * }, * "nestedPlaceholder": "Write other text to refine the search...", - * "then": [ ... an array of services to use when one item of this service is selected] + * "nestedPlaceholderMsgId": "id contained in the localization files i.e. search.nestedplaceholder", + * "then": [ ... an array of services to use when one item of this service is selected], + * "geomService": { optional service to retrieve the geometry} * } + * * ``` * The typical nested service needs to have some additional parameters: * ``` @@ -151,6 +153,7 @@ const SearchPlugin = connect((state) => ({ {...this.props} searchOptions={this.getCurrentServices()} placeholder={this.getServiceOverrides("placeholder")} + placeholderMsgId={this.getServiceOverrides("placeholderMsgId")} />); if (this.props.withToggle === true) { return [].concat(this.props.enabled ? [search] : null); From 28ba27a80fbe07d7b503e7bce3a120dff46899d1 Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Thu, 30 Mar 2017 17:07:32 +0200 Subject: [PATCH 2/6] clean up code --- web/client/api/__tests__/searchText-test.js | 42 ++++++++++++++++++++ web/client/api/searchText.js | 44 --------------------- web/client/epics/search.js | 12 +++--- 3 files changed, 48 insertions(+), 50 deletions(-) create mode 100644 web/client/api/__tests__/searchText-test.js diff --git a/web/client/api/__tests__/searchText-test.js b/web/client/api/__tests__/searchText-test.js new file mode 100644 index 0000000000..62ab8701d5 --- /dev/null +++ b/web/client/api/__tests__/searchText-test.js @@ -0,0 +1,42 @@ +/** + * Copyright 2017, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +const expect = require('expect'); +const {API} = require('../searchText'); +const axios = require('axios'); + +describe('Test correctness of the searchText APIs', () => { + + const myFun = (param) => { + // do stuff + return param; + }; + function fun() { + return axios.get('base/web/client/test-resources/featureCollectionZone.js'); + } + + it('setter and getter services', (done) => { + let servName = "myService"; + API.Utils.setService(servName, myFun); + try { + expect(API.Services).toExist(); + expect(API.Services[servName]).toExist(); + expect(API.Utils.getService(servName)).toExist(); + done(); + } catch(ex) { + done(ex); + } + }); + + let serviceType = 'myCustomService'; + it('setService', (done) => { + API.Utils.setService(serviceType, fun); + expect(API.Utils.getService(serviceType)).toBe(fun); + done(); + }); +}); diff --git a/web/client/api/searchText.js b/web/client/api/searchText.js index a32dc32ed9..d643ae3c64 100644 --- a/web/client/api/searchText.js +++ b/web/client/api/searchText.js @@ -10,8 +10,6 @@ const assign = require('object-assign'); const GeoCodeUtils = require('../utils/GeoCodeUtils'); const {generateTemplateString} = require('../utils/TemplateUtils'); -const axios = require('axios'); -const urlUtil = require('url'); let Services = { nominatim: (searchText, options = {}) => require('./Nominatim') @@ -42,48 +40,6 @@ let Services = { cql_filter: filter }, params)) .then( response => response.features ); - }, - bzVie: (searchText, {pathname, lang}) => { - let params = assign({}, {query: searchText, lang}); - let url = urlUtil.format({ - pathname, - query: params - }); - return axios.post(url).then( (res) => { - if (res && res.data && res.data.success) { - return res.data.vie.map((item) => { - return { - "type": "Feature", - "properties": { - "code": item.codice, - "desc": item.descrizione - } - }; - }); - } - return []; - }); - }, - bzCivico: (searchText, {pathname, item}) => { - let params = assign({}, {query: searchText, idVia: item.properties.code}); - let url = urlUtil.format({ - pathname, - query: params - }); - return axios.post(url).then( (res) => { - if (res && res.data && res.data.success) { - return res.data.vie.map((nestedItem) => { - return { - "type": "Feature", - "properties": { - "code": nestedItem.codice, - "desc": nestedItem.descrizione - } - }; - }); - } - return []; - }); } }; diff --git a/web/client/epics/search.js b/web/client/epics/search.js index b0e01acbf9..ec6127cc10 100644 --- a/web/client/epics/search.js +++ b/web/client/epics/search.js @@ -46,10 +46,10 @@ const searchEpic = action$ => Rx.Observable.from( (action.services || [ {type: "nominatim"} ]) // Create an stream for each Service - .map( - (service) => Rx.Observable.defer(() => API.Utils.getService(service.type)(action.searchText, service.options) - .then( - (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) + .map((service) => + Rx.Observable.defer(() => + API.Utils.getService(service.type)(action.searchText, service.options) + .then( (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) )) .retryWhen(errors => errors.delay(200).scan((count, err) => { if ( count >= 2) { @@ -62,7 +62,7 @@ const searchEpic = action$ => // merge all results from the streams .mergeAll() .scan( (oldRes, newRes) => [...oldRes, ...newRes].sort( (a, b) => get(b, "__PRIORITY__") - get(a, "__PRIORITY__") ) .slice(0, 15)) - .map((results) => searchResultLoaded(results, false, API.Services)) + .map((results) => searchResultLoaded(results, false)) .takeUntil(action$.ofType([ TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, TEXT_SEARCH_ITEM_SELECTED])) .startWith(searchTextLoading(true)) .concat([searchTextLoading(false)]) @@ -87,10 +87,10 @@ const searchItemSelected = action$ => action$.ofType(TEXT_SEARCH_ITEM_SELECTED) .switchMap(action => { let itemSelectionStream = Rx.Observable.of(action.item) - // retrieve geometry from geomService or pass the item directly .concatMap((item) => { if (item && item.__SERVICE__ && item.__SERVICE__.geomService) { let staticFilter = generateTemplateString(item.__SERVICE__.geomService.options.staticFilter || "")(item); + // retrieve geometry from geomService or pass the item directly return Rx.Observable.fromPromise( API.Utils.getService(item.__SERVICE__.geomService.type)("", assign( {}, item.__SERVICE__.geomService.options, { staticFilter } )) .then(res => assign({}, item, {geometry: res[0].geometry} ) ) From afdc3cda3719325b8df001e37c763dff08da1f94 Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Thu, 30 Mar 2017 17:17:17 +0200 Subject: [PATCH 3/6] fixed indentation --- web/client/epics/search.js | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/web/client/epics/search.js b/web/client/epics/search.js index ec6127cc10..f159ea6acd 100644 --- a/web/client/epics/search.js +++ b/web/client/epics/search.js @@ -42,32 +42,31 @@ const searchEpic = action$ => action$.ofType(TEXT_SEARCH_STARTED) .debounceTime(250) .switchMap( action => - // create a stream of streams from array - Rx.Observable.from( - (action.services || [ {type: "nominatim"} ]) - // Create an stream for each Service - .map((service) => - Rx.Observable.defer(() => - API.Utils.getService(service.type)(action.searchText, service.options) - .then( (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) - )) - .retryWhen(errors => errors.delay(200).scan((count, err) => { - if ( count >= 2) { - throw err; - } - return count + 1; - }, 0)) - ) // map - ) // from - // merge all results from the streams - .mergeAll() - .scan( (oldRes, newRes) => [...oldRes, ...newRes].sort( (a, b) => get(b, "__PRIORITY__") - get(a, "__PRIORITY__") ) .slice(0, 15)) - .map((results) => searchResultLoaded(results, false)) - .takeUntil(action$.ofType([ TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, TEXT_SEARCH_ITEM_SELECTED])) - .startWith(searchTextLoading(true)) - .concat([searchTextLoading(false)]) - .catch(e => Rx.Observable.from([searchResultError(e), searchTextLoading(false)])) - + // create a stream of streams from array + Rx.Observable.from( + (action.services || [ {type: "nominatim"} ]) + // Create an stream for each Service + .map((service) => + Rx.Observable.defer(() => + API.Utils.getService(service.type)(action.searchText, service.options) + .then( (response= []) => response.map(result => ({...result, __SERVICE__: service, __PRIORITY__: service.priority || 0})) + )) + .retryWhen(errors => errors.delay(200).scan((count, err) => { + if ( count >= 2) { + throw err; + } + return count + 1; + }, 0)) + ) // map + ) // from + // merge all results from the streams + .mergeAll() + .scan( (oldRes, newRes) => [...oldRes, ...newRes].sort( (a, b) => get(b, "__PRIORITY__") - get(a, "__PRIORITY__") ) .slice(0, 15)) + .map((results) => searchResultLoaded(results, false)) + .takeUntil(action$.ofType([ TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_RESET, TEXT_SEARCH_ITEM_SELECTED])) + .startWith(searchTextLoading(true)) + .concat([searchTextLoading(false)]) + .catch(e => Rx.Observable.from([searchResultError(e), searchTextLoading(false)])) ); /** @@ -86,6 +85,7 @@ const searchEpic = action$ => const searchItemSelected = action$ => action$.ofType(TEXT_SEARCH_ITEM_SELECTED) .switchMap(action => { + // itemSelectionStream --> emits actions for zoom and marker add let itemSelectionStream = Rx.Observable.of(action.item) .concatMap((item) => { if (item && item.__SERVICE__ && item.__SERVICE__.geomService) { From 37bb493a4d55d3b414c191b683321c8c6aa2a6d8 Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Thu, 30 Mar 2017 17:47:14 +0200 Subject: [PATCH 4/6] fixed test --- web/client/epics/__tests__/search-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/epics/__tests__/search-test.js b/web/client/epics/__tests__/search-test.js index 586a7d02aa..d04640ace6 100644 --- a/web/client/epics/__tests__/search-test.js +++ b/web/client/epics/__tests__/search-test.js @@ -211,6 +211,6 @@ describe('search Epics', () => { expect(actions[5].searchText).toBe(TEXT); expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); - }, 400); + }, 800); }); }); From 1ad2e3a9c365e8821bb1c50f95ff7cc5a9bf91d9 Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Fri, 31 Mar 2017 10:16:34 +0200 Subject: [PATCH 5/6] fixed test --- web/client/epics/__tests__/search-test.js | 68 ++++++++--------------- web/client/epics/search.js | 2 +- 2 files changed, 25 insertions(+), 45 deletions(-) diff --git a/web/client/epics/__tests__/search-test.js b/web/client/epics/__tests__/search-test.js index d04640ace6..66279ac8c7 100644 --- a/web/client/epics/__tests__/search-test.js +++ b/web/client/epics/__tests__/search-test.js @@ -124,29 +124,31 @@ describe('search Epics', () => { let actions = store.getActions(); expect(actions.length).toBe(6); - expect(actions[1].type).toBe(CHANGE_MAP_VIEW); - expect(actions[2].type).toBe(TEXT_SEARCH_ADD_MARKER); - expect(actions[3].type).toBe(TEXT_SEARCH_RESULTS_PURGE); - expect(actions[4].type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED); - expect(actions[4].services[0]).toEqual({ + expect(actions.filter(m => m.type === CHANGE_MAP_VIEW)[0].type).toBe(CHANGE_MAP_VIEW); + expect(actions.filter(m => m.type === TEXT_SEARCH_ADD_MARKER)[0].type).toBe(TEXT_SEARCH_ADD_MARKER); + expect(actions.filter(m => m.type === TEXT_SEARCH_RESULTS_PURGE)[0].type).toBe(TEXT_SEARCH_RESULTS_PURGE); + + let testSearchNestedServicesSelectedAction = actions.filter(m => m.type === TEXT_SEARCH_NESTED_SERVICES_SELECTED)[0]; + expect(testSearchNestedServicesSelectedAction.type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED); + expect(testSearchNestedServicesSelectedAction.services[0]).toEqual({ ...nestedService, options: { item } }); - expect(actions[4].items).toEqual({ + expect(testSearchNestedServicesSelectedAction.items).toEqual({ placeholder: SEARCH_NESTED, placeholderMsgId: TEST_NESTED_PLACEHOLDER, text: TEXT }); - expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); - expect(actions[5].searchText).toBe(TEXT); + expect(actions.filter(m => m.type === TEXT_SEARCH_TEXT_CHANGE)[0].type).toBe(TEXT_SEARCH_TEXT_CHANGE); + expect(actions.filter(m => m.type === TEXT_SEARCH_TEXT_CHANGE)[0].searchText).toBe(TEXT); }); - it('searchItemSelected with geomService', () => { + it('testing the geometry service', (done) => { + // use the done function for asynchronus calls const itemWithoutGeom = { "type": "Feature", - "bbox": [125, 10, 126, 11], "properties": { "name": TEXT }, @@ -164,53 +166,31 @@ describe('search Epics', () => { typeName: 'topp:states', queriableAttributes: [STATE_NAME] } - }, - nestedPlaceholder: SEARCH_NESTED, - nestedPlaceholderMsgId: TEST_NESTED_PLACEHOLDER + } } }; + // needed for the changeMapView action let action = selectSearchItem(itemWithoutGeom, { size: { width: 200, height: 200 }, - services: [{ - type: 'wfs', - options: { - url: 'base/web/client/test-resources/wfs/Wyoming.json', - typeName: 'topp:states', - queriableAttributes: [STATE_NAME] - } - }], projection: "EPSG:4326" }); store.dispatch( action ); + // a set timeout is needed in order to dispatch the actions setTimeout(() => { let actions = store.getActions(); - expect(actions.length).toBe(6); - expect(actions[1].type).toBe(CHANGE_MAP_VIEW); - expect(actions[2].type).toBe(TEXT_SEARCH_ADD_MARKER); - expect(actions[3].type).toBe(TEXT_SEARCH_RESULTS_PURGE); - expect(actions[4].type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED); - expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); - - expect(actions[4].services[0]).toEqual({ - ...nestedService, - options: { - item - } - }); - expect(actions[4].services[0].geometry).toExist(); - expect(actions[4].items).toEqual({ - placeholder: SEARCH_NESTED, - placeholderMsgId: TEST_NESTED_PLACEHOLDER, - text: TEXT - }); - expect(actions[5].searchText).toBe(TEXT); - expect(actions[5].type).toBe(TEXT_SEARCH_TEXT_CHANGE); - - }, 800); + expect(actions.length).toBe(5); + let addMarkerAction = actions.filter(m => m.type === TEXT_SEARCH_ADD_MARKER)[0]; + + expect(addMarkerAction).toExist(); + expect(addMarkerAction.markerPosition.geometry).toExist(); + + done(); + // setting 0 as delay arises script error + }, 100); }); }); diff --git a/web/client/epics/search.js b/web/client/epics/search.js index f159ea6acd..a1e8a6380e 100644 --- a/web/client/epics/search.js +++ b/web/client/epics/search.js @@ -97,7 +97,7 @@ const searchItemSelected = action$ => ); } return Rx.Observable.of(action.item); - }).mergeMap((item) => { + }).concatMap((item) => { let bbox = item.bbox || item.properties.bbox || toBbox(item); let mapSize = action.mapConfig.size; From 91553ae678be7f7a6593829031fd57e5b2b1a92f Mon Sep 17 00:00:00 2001 From: Matteo Velludini Date: Fri, 31 Mar 2017 10:30:15 +0200 Subject: [PATCH 6/6] improved a test --- web/client/epics/__tests__/search-test.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/client/epics/__tests__/search-test.js b/web/client/epics/__tests__/search-test.js index 66279ac8c7..254e0a82a5 100644 --- a/web/client/epics/__tests__/search-test.js +++ b/web/client/epics/__tests__/search-test.js @@ -124,12 +124,14 @@ describe('search Epics', () => { let actions = store.getActions(); expect(actions.length).toBe(6); - expect(actions.filter(m => m.type === CHANGE_MAP_VIEW)[0].type).toBe(CHANGE_MAP_VIEW); - expect(actions.filter(m => m.type === TEXT_SEARCH_ADD_MARKER)[0].type).toBe(TEXT_SEARCH_ADD_MARKER); - expect(actions.filter(m => m.type === TEXT_SEARCH_RESULTS_PURGE)[0].type).toBe(TEXT_SEARCH_RESULTS_PURGE); + let expectedActions = [CHANGE_MAP_VIEW, TEXT_SEARCH_ADD_MARKER, TEXT_SEARCH_RESULTS_PURGE, TEXT_SEARCH_NESTED_SERVICES_SELECTED, TEXT_SEARCH_TEXT_CHANGE ]; + let actionsType = actions.map(a => a.type); + + expectedActions.forEach((a) => { + expect(actionsType.indexOf(a)).toNotBe(-1); + }); let testSearchNestedServicesSelectedAction = actions.filter(m => m.type === TEXT_SEARCH_NESTED_SERVICES_SELECTED)[0]; - expect(testSearchNestedServicesSelectedAction.type).toBe(TEXT_SEARCH_NESTED_SERVICES_SELECTED); expect(testSearchNestedServicesSelectedAction.services[0]).toEqual({ ...nestedService, options: { @@ -141,7 +143,6 @@ describe('search Epics', () => { placeholderMsgId: TEST_NESTED_PLACEHOLDER, text: TEXT }); - expect(actions.filter(m => m.type === TEXT_SEARCH_TEXT_CHANGE)[0].type).toBe(TEXT_SEARCH_TEXT_CHANGE); expect(actions.filter(m => m.type === TEXT_SEARCH_TEXT_CHANGE)[0].searchText).toBe(TEXT); });