From 15db5e6d3324ed32b4602c4a9767c18113cbc647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre-=C3=89tienne=20Lord?= <7397743+pelord@users.noreply.github.com> Date: Thu, 8 Apr 2021 12:28:54 -0400 Subject: [PATCH] feat(search): allow multiple search term separated by a term splitter (#821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(search): allow multiple search term separated by a term splitter * refactor(search-bar): validate if the term splitter exists in the term. * feat(search): introducing the score property into the search result metadata based on server score OR on client side score calculation * feat(demo) search with a term splitter * refactor(search): lint * refactor(search-results) moving from entities to stateview entities * feat(search.state): adding a custom strategy based on score * refactor(search source): moving from difference to similarity concept for client side score calculation * refactor(search): lint * wip Co-authored-by: Marc-André Barbeau --- demo/src/app/geo/search/search.component.html | 1 + demo/src/app/geo/search/search.component.ts | 20 ++++--- .../directions-form.component.ts | 19 ++++--- .../search/search-bar/search-bar.component.ts | 20 +++++-- .../search-results.component.ts | 26 +++++---- .../search-pointer-summary.directive.ts | 21 ++++--- .../lib/search/shared/search.interfaces.ts | 3 +- .../src/lib/search/shared/search.service.ts | 57 ++++++++++++------- .../geo/src/lib/search/shared/search.utils.ts | 23 ++++++++ .../src/lib/search/shared/sources/cadastre.ts | 11 ++-- .../shared/sources/icherche.interfaces.ts | 1 + .../src/lib/search/shared/sources/icherche.ts | 9 ++- .../src/lib/search/shared/sources/ilayer.ts | 9 ++- .../lib/search/shared/sources/nominatim.ts | 12 ++-- .../search/shared/sources/storedqueries.ts | 14 +++-- .../search-results-tool.component.html | 5 +- .../search-results-tool.component.ts | 21 +++++-- .../src/lib/search/search.state.ts | 38 ++++++++++++- 18 files changed, 217 insertions(+), 93 deletions(-) diff --git a/demo/src/app/geo/search/search.component.html b/demo/src/app/geo/search/search.component.html index 43fbcaf981..5e34de9cf0 100644 --- a/demo/src/app/geo/search/search.component.html +++ b/demo/src/app/geo/search/search.component.html @@ -28,6 +28,7 @@ (pointerSummaryStatus)="onPointerSummaryStatusChange($event)" [searchSettings]="true" [store]="searchStore" + [termSplitter]="termSplitter" (searchTermChange)="onSearchTermChange($event)" (search)="onSearch($event)" (clearFeature)="removeFeatureFromMap()" diff --git a/demo/src/app/geo/search/search.component.ts b/demo/src/app/geo/search/search.component.ts index 1ef7e53cc5..8ca22114a8 100644 --- a/demo/src/app/geo/search/search.component.ts +++ b/demo/src/app/geo/search/search.component.ts @@ -1,4 +1,4 @@ -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, forkJoin } from 'rxjs'; import { Component, ElementRef, @@ -37,6 +37,8 @@ export class AppSearchComponent implements OnInit, OnDestroy { public igoSearchPointerSummaryEnabled: boolean = false; + public termSplitter = '|'; + public map = new IgoMap({ overlay: true, controls: { @@ -211,15 +213,15 @@ export class AppSearchComponent implements OnInit, OnDestroy { onSearchCoordinate() { this.searchStore.clear(); - const results = this.searchService.reverseSearch(this.lonlat); + const researches = this.searchService.reverseSearch(this.lonlat); - for (const i in results) { - if (results.length > 0) { - results[i].request.subscribe((_results: SearchResult[]) => { - this.onSearch({ research: results[i], results: _results }); - }); - } - } + researches.map((r: Research) => r.source).map((source) => { + const currentResearch = researches.find((r) => r.source === source); + return forkJoin(currentResearch.requests).subscribe((res: SearchResult[][]) => { + const results = [].concat.apply([], res); + this.onSearch({ research: currentResearch, results }); + }); + }); } onOpenGoogleMaps() { diff --git a/packages/geo/src/lib/directions/directions-form/directions-form.component.ts b/packages/geo/src/lib/directions/directions-form/directions-form.component.ts index e9bfc0e098..dd23c63032 100644 --- a/packages/geo/src/lib/directions/directions-form/directions-form.component.ts +++ b/packages/geo/src/lib/directions/directions-form/directions-form.component.ts @@ -9,11 +9,10 @@ import { ChangeDetectorRef } from '@angular/core'; import { FormGroup, FormBuilder, Validators, FormArray } from '@angular/forms'; -import { Subscription, Subject, BehaviorSubject } from 'rxjs'; +import { Subscription, Subject, BehaviorSubject, forkJoin } from 'rxjs'; import { debounceTime, - distinctUntilChanged, - map + distinctUntilChanged } from 'rxjs/operators'; import olFeature from 'ol/Feature'; @@ -436,7 +435,8 @@ export class DirectionsFormComponent implements OnInit, OnDestroy { .reverseSearch(coordinates, { zoom: this.map.viewController.getZoom() }) .map(res => this.routesQueries$$.push( - res.request.pipe(map(f => f)).subscribe(results => { + forkJoin(res.requests).subscribe(r => { + const results = [].concat.apply([], r); results.forEach(result => { if ( groupedLocations.filter(f => f.source === result.source) @@ -444,7 +444,7 @@ export class DirectionsFormComponent implements OnInit, OnDestroy { ) { groupedLocations.push({ source: result.source, - results: results.map(r => r.data) + results: results.map(response => response.data) }); } }); @@ -1156,18 +1156,19 @@ export class DirectionsFormComponent implements OnInit, OnDestroy { const researches = this.searchService.search(term, {searchType: 'Feature'}); researches.map(res => this.search$$ = - res.request.subscribe(results => { + forkJoin(res.requests).subscribe(r => { + const results = [].concat.apply([], r); results - .filter(r => r.data.geometry) + .filter(resp => resp.data.geometry) .forEach(element => { if ( - searchProposals.filter(r => r.source === element.source) + searchProposals.filter(q => q.source === element.source) .length === 0 ) { searchProposals.push({ source: element.source, meta: element.meta, - results: results.map(r => r.data) + results: results.map(p => p.data) }); } }); diff --git a/packages/geo/src/lib/search/search-bar/search-bar.component.ts b/packages/geo/src/lib/search/search-bar/search-bar.component.ts index 5af34145f9..ab2b63d4ed 100644 --- a/packages/geo/src/lib/search/search-bar/search-bar.component.ts +++ b/packages/geo/src/lib/search/search-bar/search-bar.component.ts @@ -13,7 +13,7 @@ import { FloatLabelType, MatFormFieldAppearance } from '@angular/material/form-field'; -import { BehaviorSubject, Subscription, EMPTY, timer } from 'rxjs'; +import { BehaviorSubject, Subscription, EMPTY, timer, forkJoin } from 'rxjs'; import { debounce, distinctUntilChanged } from 'rxjs/operators'; import { LanguageService } from '@igo2/core'; @@ -113,6 +113,8 @@ export class SearchBarComponent implements OnInit, OnDestroy { } readonly term$: BehaviorSubject = new BehaviorSubject(''); + @Input() termSplitter; + /** * Whether this component is disabled */ @@ -404,12 +406,20 @@ export class SearchBarComponent implements OnInit, OnDestroy { return; } - const researches = this.searchService.search(term, { + const terms = this.termSplitter && this.term.match(new RegExp(this.termSplitter, 'g')) ? + term.split(this.termSplitter).filter((t) => t.length >= this.minLength ) : [term]; + + if (!terms.length) { + return; + } + const researches = this.searchService.search(terms, { forceNA: this.forceNA }); - this.researches$$ = researches.map((research) => { - return research.request.subscribe((results: SearchResult[]) => { - this.onResearchCompleted(research, results); + this.researches$$ = researches.map((r: Research) => r.source).map((source) => { + const currentResearch = researches.find((r) => r.source === source); + return forkJoin(currentResearch.requests).subscribe((res: SearchResult[][]) => { + const results = [].concat.apply([], res); + this.onResearchCompleted(currentResearch, results); }); }); } diff --git a/packages/geo/src/lib/search/search-results/search-results.component.ts b/packages/geo/src/lib/search/search-results/search-results.component.ts index 31967fb402..a03b551654 100644 --- a/packages/geo/src/lib/search/search-results/search-results.component.ts +++ b/packages/geo/src/lib/search/search-results/search-results.component.ts @@ -11,10 +11,10 @@ import { } from '@angular/core'; import type { TemplateRef } from '@angular/core'; -import { Observable, EMPTY, timer, BehaviorSubject, Subscription } from 'rxjs'; +import { Observable, EMPTY, timer, BehaviorSubject, Subscription, forkJoin } from 'rxjs'; import { debounce, map } from 'rxjs/operators'; -import { EntityStore, EntityStoreWatcher } from '@igo2/common'; +import { EntityState, EntityStore, EntityStoreWatcher } from '@igo2/common'; import { IgoMap } from '../../map'; @@ -91,6 +91,8 @@ export class SearchResultsComponent implements OnInit, OnDestroy { } public _term: string; + @Input() termSplitter; + @Input() settingsChange$ = new BehaviorSubject(undefined); /** @@ -195,12 +197,12 @@ export class SearchResultsComponent implements OnInit, OnDestroy { * @internal */ private liftResults(): Observable<{source: SearchSource; results: SearchResult[]}[]> { - return this.store.view.all$().pipe( - debounce((results: SearchResult[]) => { + return this.store.stateView.all$().pipe( + debounce((results: {entity: SearchResult, state: EntityState}[]) => { return results.length === 0 ? EMPTY : timer(200); }), - map((results: SearchResult[]) => { - return this.groupResults(results.sort(this.sortByOrder)); + map((results: {entity: SearchResult, state: EntityState}[]) => { + return this.groupResults(results.map(r => r.entity).sort(this.sortByOrder)); }) ); } @@ -249,14 +251,18 @@ export class SearchResultsComponent implements OnInit, OnDestroy { page: ++this.pageIterator[group.source.getId()] }; - const researches = this.searchService.search(this.term, options); - researches.map(research => { - research.request.subscribe((results: SearchResult[]) => { + const terms = this.termSplitter ? this.term.split(this.termSplitter) : [this.term]; + + const researches = this.searchService.search(terms, options); + researches.map((r: Research) => r.source).map((source) => { + const currentResearch = researches.find((r) => r.source === source); + return forkJoin(currentResearch.requests).subscribe((res: SearchResult[][]) => { + const results = [].concat.apply([], res); const newResults = group.results.concat(results); if (!results.length) { newResults[newResults.length - 1].meta.nextPage = false; } - this.moreResults.emit({research, results: newResults}); + this.moreResults.emit({research: currentResearch, results: newResults}); }); }); return; diff --git a/packages/geo/src/lib/search/shared/search-pointer-summary.directive.ts b/packages/geo/src/lib/search/shared/search-pointer-summary.directive.ts index f6f457f0de..6a48263119 100644 --- a/packages/geo/src/lib/search/shared/search-pointer-summary.directive.ts +++ b/packages/geo/src/lib/search/shared/search-pointer-summary.directive.ts @@ -8,7 +8,7 @@ import { AfterContentChecked } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { forkJoin, Subscription } from 'rxjs'; import { MapBrowserPointerEvent as OlMapBrowserPointerEvent } from 'ol/MapBrowserEvent'; import { ListenerFunction } from 'ol/events'; @@ -273,16 +273,15 @@ export class SearchPointerSummaryDirective implements OnInit, OnDestroy, AfterCo private onSearchCoordinate() { this.pointerSearchStore.clear(); - const results = this.searchService.reverseSearch(this.lonLat, { params: { geometry: 'false', icon: 'false' } }, true); - - for (const i in results) { - if (results.length > 0) { - this.reverseSearch$$.push( - results[i].request.subscribe((_results: SearchResult[]) => { - this.onSearch({ research: results[i], results: _results }); - })); - } - } + const researches = this.searchService.reverseSearch(this.lonLat, { params: { geometry: 'false', icon: 'false' } }, true); + + researches.map((r: Research) => r.source).map((source) => { + const currentResearch = researches.find((r) => r.source === source); + return forkJoin(currentResearch.requests).subscribe((res: SearchResult[][]) => { + const results = [].concat.apply([], res); + this.onSearch({ research: currentResearch, results }); + }); + }); } private onSearch(event: { research: Research; results: SearchResult[] }) { diff --git a/packages/geo/src/lib/search/shared/search.interfaces.ts b/packages/geo/src/lib/search/shared/search.interfaces.ts index 32b9a56a38..b9acd05ace 100644 --- a/packages/geo/src/lib/search/shared/search.interfaces.ts +++ b/packages/geo/src/lib/search/shared/search.interfaces.ts @@ -3,7 +3,7 @@ import { Observable } from 'rxjs'; import { SearchSource } from './sources/source'; export interface Research { - request: Observable; + requests: Observable[]; reverse: boolean; source: SearchSource; } @@ -17,6 +17,7 @@ export interface SearchResult { title: string; titleHtml?: string; icon: string; + score?: number; nextPage?: boolean; }; } diff --git a/packages/geo/src/lib/search/shared/search.service.ts b/packages/geo/src/lib/search/shared/search.service.ts index e9a4e95261..b63fe7f60c 100644 --- a/packages/geo/src/lib/search/shared/search.service.ts +++ b/packages/geo/src/lib/search/shared/search.service.ts @@ -34,27 +34,32 @@ export class SearchService { /** * Perform a research by text - * @param term Any text + * @param terms Any text or a list of terms * @returns Researches */ - search(term: string, options: TextSearchOptions = {}): Research[] { - if (!this.termIsValid(term)) { + search( + terms: string[] | string, // for compatibility + options: TextSearchOptions = {} + ): Research[] { + const validatedTerms = this.validateTerms(this.stringToArray(terms)); + + if (validatedTerms.length === 0) { return []; } const proj = this.mapService.getMap()?.projection || 'EPSG:3857'; - const response = stringToLonLat(term, proj, { - forceNA: options.forceNA + validatedTerms.map(term => { + const response = stringToLonLat(term, proj, { + forceNA: options.forceNA + }); + if (response.lonLat) { + return this.reverseSearch(response.lonLat, { distance: response.radius }); + } else if (response.message) { + console.log(response.message); + } }); - if (response.lonLat) { - return this.reverseSearch(response.lonLat, { distance: response.radius, conf: response.conf }); - } else if (response.message) { - console.log(response.message); - } - options.extent = this.mapService - .getMap() - ?.viewController.getExtent('EPSG:4326'); + options.extent = this.mapService.getMap()?.viewController.getExtent('EPSG:4326'); let sources; @@ -73,7 +78,7 @@ export class SearchService { } sources = sources.filter(sourceCanSearch); - return this.searchSources(sources, term, options); + return this.searchSources(sources, validatedTerms, options); } /** @@ -103,15 +108,16 @@ export class SearchService { */ private searchSources( sources: SearchSource[], - term: string, + terms: string[] | string, // for compatibility options: TextSearchOptions ): Research[] { + return sources.map((source: SearchSource) => { return { - request: ((source as any) as TextSearch).search(term, options), + requests: this.stringToArray(terms).map((term) => ((source as any) as TextSearch).search(term, options)), reverse: false, source - }; + } as Research; }); } @@ -128,16 +134,25 @@ export class SearchService { ): Research[] { return sources.map((source: SearchSource) => { return { - request: ((source as any) as ReverseSearch).reverseSearch( + requests: [((source as any) as ReverseSearch).reverseSearch( lonLat, options - ), + )], reverse: true, source }; }); } + /** + * Validate if a list of term is valid + * @param term Search term + * @returns The validated list; + */ + private validateTerms(terms: string[]): string[] { + return terms.filter((term) => this.termIsValid(term)); + } + /** * Validate that a search term is valid * @param term Search term @@ -146,4 +161,8 @@ export class SearchService { private termIsValid(term: string): boolean { return typeof term === 'string' && term !== ''; } + + private stringToArray(terms: string[] | string): string[] { + return typeof terms === 'string' ? [terms] : terms; + } } diff --git a/packages/geo/src/lib/search/shared/search.utils.ts b/packages/geo/src/lib/search/shared/search.utils.ts index 37d68b3bd9..19eb1e3f7e 100644 --- a/packages/geo/src/lib/search/shared/search.utils.ts +++ b/packages/geo/src/lib/search/shared/search.utils.ts @@ -53,3 +53,26 @@ export function featureToSearchResult( } }; } + +export function findDiff(str1: string, str2: string){ + let diff = ''; + str2.split('').forEach((val, i) => { + if (val !== str1.charAt(i)) { + diff += val; + } + }); + return diff; +} + +export function computeTermSimilarity(from, to): number { + const fromToDiff = findDiff(from, to); + const toFromDiff = findDiff(to, from); + const totalDiff = fromToDiff + toFromDiff; + + let delta = 0; + if (totalDiff.length) { + delta = totalDiff.length / from.length * 100; + } + + return 100 - Math.floor(delta); +} diff --git a/packages/geo/src/lib/search/shared/sources/cadastre.ts b/packages/geo/src/lib/search/shared/sources/cadastre.ts index a8fcc2b845..eb6e34538a 100644 --- a/packages/geo/src/lib/search/shared/sources/cadastre.ts +++ b/packages/geo/src/lib/search/shared/sources/cadastre.ts @@ -13,6 +13,8 @@ import { SearchSource, TextSearch } from './source'; import { SearchSourceOptions, TextSearchOptions } from './source.interfaces'; import { LanguageService, StorageService } from '@igo2/core'; +import { computeTermSimilarity } from '../search.utils'; + /** * Cadastre search source */ @@ -67,7 +69,7 @@ export class CadastreSearchSource extends SearchSource implements TextSearch { } return this.http .get(this.searchUrl, { params, responseType: 'text' }) - .pipe(map((response: string) => this.extractResults(response))); + .pipe(map((response: string) => this.extractResults(response, term))); } private computeSearchRequestParams( @@ -86,14 +88,14 @@ export class CadastreSearchSource extends SearchSource implements TextSearch { }); } - private extractResults(response: string): SearchResult[] { + private extractResults(response: string, term: string): SearchResult[] { return response .split('
') .filter((lot: string) => lot.length > 0) - .map((lot: string) => this.dataToResult(lot)); + .map((lot: string) => this.dataToResult(lot, term)); } - private dataToResult(data: string): SearchResult { + private dataToResult(data: string, term: string): SearchResult { const lot = data.split(';'); const numero = lot[0]; const wkt = lot[7]; @@ -111,6 +113,7 @@ export class CadastreSearchSource extends SearchSource implements TextSearch { dataType: FEATURE, id, title: numero, + score: computeTermSimilarity(term.trim(), numero), icon: 'map-marker' }, data: { diff --git a/packages/geo/src/lib/search/shared/sources/icherche.interfaces.ts b/packages/geo/src/lib/search/shared/sources/icherche.interfaces.ts index ddb2870adc..6b4a24e751 100644 --- a/packages/geo/src/lib/search/shared/sources/icherche.interfaces.ts +++ b/packages/geo/src/lib/search/shared/sources/icherche.interfaces.ts @@ -11,6 +11,7 @@ export interface IChercheData { title2?: string; title3?: string; }; + score?: number; } export interface IChercheResponse { diff --git a/packages/geo/src/lib/search/shared/sources/icherche.ts b/packages/geo/src/lib/search/shared/sources/icherche.ts index a63aec8645..3ad3a5eb74 100644 --- a/packages/geo/src/lib/search/shared/sources/icherche.ts +++ b/packages/geo/src/lib/search/shared/sources/icherche.ts @@ -26,6 +26,7 @@ import { IChercheReverseData, IChercheReverseResponse } from './icherche.interfaces'; +import { computeTermSimilarity } from '../search.utils'; @Injectable() export class IChercheSearchResultFormatter { @@ -345,7 +346,7 @@ export class IChercheSearchSource extends SearchSource implements TextSearch { this.options.params.page = params.get('page') || '1'; return this.http.get(`${this.searchUrl}/geocode`, { params }).pipe( - map((response: IChercheResponse) => this.extractResults(response)), + map((response: IChercheResponse) => this.extractResults(response, term)), catchError((err) => { err.error.toDisplay = true; err.error.title = this.languageService.translate.instant( @@ -416,14 +417,15 @@ export class IChercheSearchSource extends SearchSource implements TextSearch { }); } - private extractResults(response: IChercheResponse): SearchResult[] { + private extractResults(response: IChercheResponse, term: string): SearchResult[] { return response.features.map((data: IChercheData) => { - return this.formatter.formatResult(this.dataToResult(data, response)); + return this.formatter.formatResult(this.dataToResult(data, term, response)); }); } private dataToResult( data: IChercheData, + term: string, response?: IChercheResponse ): SearchResult { const properties = this.computeProperties(data); @@ -456,6 +458,7 @@ export class IChercheSearchSource extends SearchSource implements TextSearch { title: data.properties.nom, titleHtml: titleHtml + subtitleHtml + subtitleHtml2, icon: data.icon || 'map-marker', + score: data.score || computeTermSimilarity(term.trim(), data.properties.nom), nextPage: response.features.length % +this.options.params.limit === 0 && +this.options.params.page < 10 diff --git a/packages/geo/src/lib/search/shared/sources/ilayer.ts b/packages/geo/src/lib/search/shared/sources/ilayer.ts index 09e54da187..89dd1eec11 100644 --- a/packages/geo/src/lib/search/shared/sources/ilayer.ts +++ b/packages/geo/src/lib/search/shared/sources/ilayer.ts @@ -22,6 +22,7 @@ import { ILayerServiceResponse, ILayerDataSource } from './ilayer.interfaces'; +import { computeTermSimilarity } from '../search.utils'; @Injectable() export class ILayerSearchResultFormatter { @@ -215,7 +216,7 @@ export class ILayerSearchSource extends SearchSource implements TextSearch { return this.http .get(this.searchUrl, { params }) .pipe( - map((response: ILayerServiceResponse) => this.extractResults(response)) + map((response: ILayerServiceResponse) => this.extractResults(response, term)) ); } @@ -267,15 +268,16 @@ export class ILayerSearchSource extends SearchSource implements TextSearch { } private extractResults( - response: ILayerServiceResponse + response: ILayerServiceResponse, term: string ): SearchResult[] { return response.items.map((data: ILayerData) => - this.dataToResult(data, response) + this.dataToResult(data, term, response) ); } private dataToResult( data: ILayerData, + term: string, response?: ILayerServiceResponse ): SearchResult { const layerOptions = this.computeLayerOptions(data); @@ -294,6 +296,7 @@ export class ILayerSearchSource extends SearchSource implements TextSearch { title: data.properties.title, titleHtml: titleHtml + subtitleHtml, icon: data.properties.type === 'Layer' ? 'layers' : 'map', + score: data.score || computeTermSimilarity(term.trim(), data.properties.name), nextPage: response.items.length % +this.options.params.limit === 0 && +this.options.params.page < 10 diff --git a/packages/geo/src/lib/search/shared/sources/nominatim.ts b/packages/geo/src/lib/search/shared/sources/nominatim.ts index d9dfeab909..dd61658d2e 100644 --- a/packages/geo/src/lib/search/shared/sources/nominatim.ts +++ b/packages/geo/src/lib/search/shared/sources/nominatim.ts @@ -11,6 +11,7 @@ import { SearchResult } from '../search.interfaces'; import { SearchSource, TextSearch } from './source'; import { SearchSourceOptions, TextSearchOptions } from './source.interfaces'; import { NominatimData } from './nominatim.interfaces'; +import { computeTermSimilarity } from '../search.utils'; /** * Nominatim search source @@ -150,7 +151,7 @@ export class NominatimSearchSource extends SearchSource implements TextSearch { } return this.http .get(this.searchUrl, { params }) - .pipe(map((response: NominatimData[]) => this.extractResults(response))); + .pipe(map((response: NominatimData[]) => this.extractResults(response, term))); } private computeSearchRequestParams( @@ -169,11 +170,11 @@ export class NominatimSearchSource extends SearchSource implements TextSearch { }); } - private extractResults(response: NominatimData[]): SearchResult[] { - return response.map((data: NominatimData) => this.dataToResult(data)); + private extractResults(response: NominatimData[], term: string): SearchResult[] { + return response.map((data: NominatimData) => this.dataToResult(data, term)); } - private dataToResult(data: NominatimData): SearchResult { + private dataToResult(data: NominatimData, term: string): SearchResult { const properties = this.computeProperties(data); const geometry = this.computeGeometry(data); const extent = this.computeExtent(data); @@ -185,7 +186,8 @@ export class NominatimSearchSource extends SearchSource implements TextSearch { dataType: FEATURE, id, title: data.display_name, - icon: 'map-marker' + icon: 'map-marker', + score: computeTermSimilarity(term.trim(), data.display_name), }, data: { type: FEATURE, diff --git a/packages/geo/src/lib/search/shared/sources/storedqueries.ts b/packages/geo/src/lib/search/shared/sources/storedqueries.ts index 12d071d92e..2fc00abd65 100644 --- a/packages/geo/src/lib/search/shared/sources/storedqueries.ts +++ b/packages/geo/src/lib/search/shared/sources/storedqueries.ts @@ -26,6 +26,7 @@ import { import * as olformat from 'ol/format'; import { LanguageService, StorageService } from '@igo2/core'; +import { computeTermSimilarity } from '../search.utils'; /** * StoredQueries search source @@ -182,13 +183,13 @@ export class StoredQueriesSearchSource extends SearchSource .get(this.searchUrl, { params, responseType: 'text' }) .pipe( map(response => { - return this.extractResults(this.extractWFSData(response)); + return this.extractResults(this.extractWFSData(response), term); }) ); } else { return this.http.get(this.searchUrl, { params }).pipe( map(response => { - return this.extractResults(this.extractWFSData(response)); + return this.extractResults(this.extractWFSData(response), term); }) ); } @@ -279,14 +280,14 @@ export class StoredQueriesSearchSource extends SearchSource } private extractResults( - response: StoredQueriesResponse + response: StoredQueriesResponse, term: string ): SearchResult[] { return response.features.map((data: StoredQueriesData) => { - return this.dataToResult(data); + return this.dataToResult(data, term); }); } - private dataToResult(data: StoredQueriesData): SearchResult { + private dataToResult(data: StoredQueriesData, term: string): SearchResult { const properties = this.computeProperties(data); const id = [this.getId(), properties.type, data.id].join('.'); const title = data.properties[this.storedQueriesOptions.resultTitle] @@ -310,7 +311,8 @@ export class StoredQueriesSearchSource extends SearchSource id, title: data.properties.title, titleHtml: data.properties[title], - icon: 'map-marker' + icon: 'map-marker', + score: computeTermSimilarity(term.trim(), data.properties.title), } }; } diff --git a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html index 5456d270e9..94ba92a9c6 100644 --- a/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html +++ b/packages/integration/src/lib/search/search-results-tool/search-results-tool.component.html @@ -1,4 +1,4 @@ -
+

{{ 'igo.integration.searchResultsTool.noResults' | translate }}

{{ 'igo.integration.searchResultsTool.doSearch' | translate }}

@@ -7,7 +7,7 @@

{{ 'igo.integration.searchResultsTool.noResults' | translate }} {{ 'igo.integration.searchResultsTool.noResults' | translate }}(undefined); @@ -152,8 +154,16 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } ); - for (const res of this.store.entities$.value) { - if (this.store.state.get(res).selected === true) { + this.searchTermSplitter$$ = this.searchState.searchTermSplitter$.subscribe( + (termSplitter: string) => { + if (termSplitter !== undefined && termSplitter !== null) { + this.termSplitter = termSplitter; + } + } + ); + + for (const res of this.store.stateView.all$().value) { + if (this.store.state.get(res.entity).selected === true) { this.topPanelState = 'expanded'; } } @@ -272,6 +282,7 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { ngOnDestroy() { this.topPanelState$$.unsubscribe(); this.searchTerm$$.unsubscribe(); + this.searchTermSplitter$$.unsubscribe(); if (this.selectedOrResolution$$) { this.selectedOrResolution$$.unsubscribe(); } @@ -371,7 +382,9 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { setTimeout(() => { const igoList = this.elRef.nativeElement.querySelector('igo-list'); let moreResults; - event.research.request.subscribe((source) => { + + forkJoin(event.research.requests).subscribe((res: SearchResult[][]) => { + const source = [].concat.apply([], res); if (!source[0] || !source[0].source) { moreResults = null; } else if (source[0].source.getId() === 'icherche') { diff --git a/packages/integration/src/lib/search/search.state.ts b/packages/integration/src/lib/search/search.state.ts index c5096eb7b7..6e09929974 100644 --- a/packages/integration/src/lib/search/search.state.ts +++ b/packages/integration/src/lib/search/search.state.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { EntityStore } from '@igo2/common'; +import { EntityRecord, EntityStore, EntityStoreFilterCustomFuncStrategy, EntityStoreStrategyFuncOptions } from '@igo2/common'; import { SearchResult, SearchSourceService, SearchSource } from '@igo2/geo'; import { BehaviorSubject } from 'rxjs'; @@ -12,6 +12,8 @@ import { BehaviorSubject } from 'rxjs'; }) export class SearchState { + readonly searchTermSplitter$: BehaviorSubject = new BehaviorSubject(undefined); + readonly searchTerm$: BehaviorSubject = new BehaviorSubject(undefined); readonly searchType$: BehaviorSubject = new BehaviorSubject(undefined); @@ -36,7 +38,39 @@ export class SearchState { .map((source: SearchSource) => (source.constructor as any).type); } - constructor(private searchSourceService: SearchSourceService) {} + constructor(private searchSourceService: SearchSourceService) { + this.store.addStrategy(this.createCustomFilterTermStrategy(), false); + } + + private createCustomFilterTermStrategy(): EntityStoreFilterCustomFuncStrategy { + const filterClauseFunc = (record: EntityRecord) => { + return record.entity.meta.score === 100; + }; + return new EntityStoreFilterCustomFuncStrategy({filterClauseFunc} as EntityStoreStrategyFuncOptions); + } + + /** + * Activate custom strategy + * + */ + activateCustomFilterTermStrategy() { + const strategy = this.store.getStrategyOfType(EntityStoreFilterCustomFuncStrategy); + if (strategy !== undefined) { + strategy.activate(); + } + } + + /** + * Deactivate custom strategy + * + */ + deactivateCustomFilterTermStrategy() { + const strategy = this.store.getStrategyOfType(EntityStoreFilterCustomFuncStrategy); + if (strategy !== undefined) { + strategy.deactivate(); + } + } + enableSearch() { this.searchDisabled$.next(false);