diff --git a/package-lock.json b/package-lock.json index 50e17fad..2c52600c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5680,65 +5680,6 @@ "node": ">=0.10.0" } }, - "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", - "integrity": "sha512-r3n0onD3BTOVUNPhR4lhVK4/pABGpbA7bW3eumZnYdKaHkf1qEC+Mag6DPbGNuuh0eG8AaYj+YqmVHSiGslaTQ==", - "dev": true, - "dependencies": { - "@babel/generator": "7.17.7", - "@babel/parser": "^7.20.5", - "@babel/traverse": "7.23.2", - "@babel/types": "7.17.0", - "javascript-natural-sort": "0.7.1", - "lodash": "^4.17.21" - }, - "peerDependencies": { - "@vue/compiler-sfc": "3.x", - "prettier": "2.x - 3.x" - }, - "peerDependenciesMeta": { - "@vue/compiler-sfc": { - "optional": true - } - } - }, - "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", diff --git a/src/app/pages/menu/menu-pages/map/map.module.ts b/src/app/pages/menu/menu-pages/map/map.module.ts index 05241a08..10513447 100644 --- a/src/app/pages/menu/menu-pages/map/map.module.ts +++ b/src/app/pages/menu/menu-pages/map/map.module.ts @@ -5,8 +5,6 @@ import { IgoLanguageModule } from '@igo2/core/language'; import { MapComponent } from './map.component'; -import { MapComponent } from './map.component'; - @NgModule({ declarations: [MapComponent], imports: [CommonModule, IgoLanguageModule], diff --git a/src/app/pages/portal/filter-button/filter-button.component.html b/src/app/pages/portal/filter-button/filter-button.component.html new file mode 100644 index 00000000..79b26d6d --- /dev/null +++ b/src/app/pages/portal/filter-button/filter-button.component.html @@ -0,0 +1,13 @@ + diff --git a/src/app/pages/portal/filter-button/filter-button.component.scss b/src/app/pages/portal/filter-button/filter-button.component.scss new file mode 100644 index 00000000..d8ceec33 --- /dev/null +++ b/src/app/pages/portal/filter-button/filter-button.component.scss @@ -0,0 +1,7 @@ +:host { + #filter-button { + border-radius: 0 !important; + color: #095797 !important; + border-color: #095797 !important; + } +} diff --git a/src/app/pages/portal/filter-button/filter-button.component.ts b/src/app/pages/portal/filter-button/filter-button.component.ts new file mode 100644 index 00000000..862068dd --- /dev/null +++ b/src/app/pages/portal/filter-button/filter-button.component.ts @@ -0,0 +1,30 @@ +import { AsyncPipe } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatButton } from '@angular/material/button'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { PanelsHandlerState } from '../panels/panels-handler/panels-handler.state'; + +@Component({ + selector: 'app-filter-button', + templateUrl: './filter-button.component.html', + styleUrls: ['./filter-button.component.scss'], + standalone: true, + imports: [MatButton, MatTooltip, TranslateModule, AsyncPipe] +}) +export class FilterButtonComponent { + @Input() tooltipDisabled: boolean; + @Output() filterToggled = new EventEmitter(); + + public dialogRef = null; + public legendButtonTooltip: unknown; + + constructor(public panelsHandlerState: PanelsHandlerState) {} + + toggleFilter(): void { + this.panelsHandlerState.shownComponent$; + this.filterToggled.emit(); + } +} diff --git a/src/app/pages/portal/legend-button/legend-button.component.html b/src/app/pages/portal/legend-button/legend-button.component.html index 3115d18b..84d9f7a9 100644 --- a/src/app/pages/portal/legend-button/legend-button.component.html +++ b/src/app/pages/portal/legend-button/legend-button.component.html @@ -3,6 +3,7 @@ mat-raised-button (click)="toggleLegend()" [matTooltip]="legendButtonTooltip" + matTooltipPosition="left" [matTooltipDisabled]="tooltipDisabled" > {{ 'legend.button' | translate }} diff --git a/src/app/pages/portal/map-overlay/map-overlay.component.ts b/src/app/pages/portal/map-overlay/map-overlay.component.ts index 2dbd3d13..a4d5634d 100644 --- a/src/app/pages/portal/map-overlay/map-overlay.component.ts +++ b/src/app/pages/portal/map-overlay/map-overlay.component.ts @@ -1,4 +1,4 @@ -import { NgClass, NgStyle } from '@angular/common'; +import { NgClass, NgFor, NgIf, NgStyle } from '@angular/common'; import { AfterViewInit, Component, OnDestroy } from '@angular/core'; import { Context, ContextService } from '@igo2/context'; @@ -14,7 +14,7 @@ import { MapOverlay } from './map-overlay.interface'; templateUrl: './map-overlay.component.html', styleUrls: ['./map-overlay.component.scss'], standalone: true, - imports: [NgClass, NgStyle] + imports: [NgFor, NgClass, NgIf, NgStyle] }) export class MapOverlayComponent implements AfterViewInit, OnDestroy { public mapOverlay: MapOverlay[] = []; @@ -44,14 +44,14 @@ export class MapOverlayComponent implements AfterViewInit, OnDestroy { } private handleContextChange(context: Context) { - let mapOverlay = []; + let mapOverlay: MapOverlay[] = []; if (context !== undefined) { this.mapOverlay = []; if (context['mapOverlay']) { mapOverlay = context['mapOverlay']; - } else if (this.configService.getConfig('mapOverlay')) { - mapOverlay = this.configService.getConfig('mapOverlay'); + } else { + mapOverlay = this.configService.getConfig('mapOverlay', []); } for (const overlay of mapOverlay) { // If no media define use default to desktop, display only if current media is on context definition diff --git a/src/app/pages/portal/map-overlay/map-overlay.enum.ts b/src/app/pages/portal/map-overlay/map-overlay.enum.ts new file mode 100644 index 00000000..db110e92 --- /dev/null +++ b/src/app/pages/portal/map-overlay/map-overlay.enum.ts @@ -0,0 +1,11 @@ +export enum MapOverlayCssClass { + TopLeft = 'top-left', + CenterLeft = 'center-left', + BottomLeft = 'bottom-left', + TopCenter = 'top-center', + CenterCenter = 'center-center', + BottomCenter = 'bottom-center', + TopRight = 'top-right', + CenterRight = 'center-right', + BottoMRight = 'bottom-right' +} diff --git a/src/app/pages/portal/map-overlay/map-overlay.interface.ts b/src/app/pages/portal/map-overlay/map-overlay.interface.ts index e8149a95..10f800a5 100644 --- a/src/app/pages/portal/map-overlay/map-overlay.interface.ts +++ b/src/app/pages/portal/map-overlay/map-overlay.interface.ts @@ -1,20 +1,17 @@ +import { Media } from '@igo2/core/media'; + +import { MapOverlayCssClass } from './map-overlay.enum'; + export interface MapOverlay { - media: Array /* Media device to display the mapOverlay - - options: mobile - tablet - desktop - - default: desktop - cssClass: string; /* Css class to define position of the element - - options: top-left - center-left - bottom-left - top-center - center-center - bottom-center - top-right - center-right - bottom-right - */; + /** + * Media device to display the mapOverlay + * Desktop is the default value. + */ + media: Array; + /** + * Css class to define position of the element + */ + cssClass: MapOverlayCssClass; fixed?: boolean; // Is element is fixed, won't be affect by animation, default to false link?: string; // Link to open when element is clicked imgSrc?: string; // source of the image to show diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.html b/src/app/pages/portal/panels/bottompanel/bottompanel.component.html deleted file mode 100644 index 30e0cac6..00000000 --- a/src/app/pages/portal/panels/bottompanel/bottompanel.component.html +++ /dev/null @@ -1,114 +0,0 @@ -
- - - @if (showSearchBar) { - - - } - - - @if (legendPanelOpened) { -
- -

{{ 'legend.title' | translate }}

- - -
- } - - @if (!searchInit && (queryStore.empty$ | async) && !legendPanelOpened) { -
-
-
- {{ 'igo.integration.searchResultsTool.noResults' | translate }} -
-
- {{ 'igo.integration.searchResultsTool.doSearch' | translate }} -
-

-
-
- } - - @if (mapQueryClick) { -
- - -
- } - - @if (searchInit) { -
- - -
- } -
-
diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss b/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss deleted file mode 100644 index bab92dcd..00000000 --- a/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss +++ /dev/null @@ -1,129 +0,0 @@ -@import '../../portal.variables.scss'; - -:host { - background-color: rgb(255, 255, 255); - width: 100%; -} - -::ng-deep igo-search-results { - position: relative; - left: $portal-left !important; - width: calc($search-bar-width - 2 * $igo-margin); - display: contents; - height: calc(100% - $igo-icon-size - $igo-margin); -} - -igo-search-results-tool { - display: block; - position: relative; - margin: 0 calc(4 * $igo-margin); - top: calc(2 * $igo-margin); - height: 100%; - width: 100%; -} - -::ng-deep app-search-results-tool > div > section > h4 > strong { - font-size: 21px !important; -} - -//// MOBILE - -igo-search-bar { - margin: auto; - width: 100%; - padding: 8px; -} - -::ng-deep app-bottompanel .mat-expansion-panel-content { - overflow-y: scroll !important; - overflow-x: clip; - height: 264px; // expanded panel height - max-width: 100%; -} - -::ng-deep .igo-search-bar-container .mat-icon { - fill: #ffffff; -} - -#bottomPanelMobile { - position: relative; - display: block; - bottom: 0; -} - -app-feature-info { - overflow-x: clip; -} - -:host ::ng-deep .mat-expansion-panel { - .mat-expansion-indicator { - &::after { - transform: rotate(-135deg) !important; - } - } - - &.mat-expanded { - .mat-expansion-indicator { - transform: rotate(180deg) !important; - } - } -} - -// Legend - -.mat-icon, -mat-button-wrapper, -.mat-mdc-icon-button .mat-icon, -.mat-icon-button { - line-height: 26px; - height: 24px; - width: 24px; - float: right; -} -.mat-icon svg { - height: 20px; - width: 20px; -} - -::ng-deep .legend.button:focus-visible { - display: none; -} - -// angular material - -::ng-deep .tooltip-above { - position: relative !important; - top: 30%; - z-index: 5000 !important; - background-color: white !important; - color: #223654 !important; - margin: 0 8px !important; - padding: 8px 12px !important; - font-size: 14px !important; - line-height: 24px !important; - border: 1px solid #c5cad2; - border-radius: 0 !important; - overflow: visible !important; - box-shadow: 0px 1px 7px #22365450 !important; - white-space: pre-line; -} - -::ng-deep .tooltip-above::after { - content: '' !important; - position: absolute !important; - top: 18% !important; - right: 100% !important; - margin-top: -5px !important; - border-width: 10px !important; - border-style: solid !important; - border-color: transparent transparent transparent white !important; - transform: rotate(180deg); -} - -:host ::ng-deep .mat-expansion-indicator { - padding: 10px; -} - -:host ::ng-deep .mat-expansion-panel-header { - padding: 0 8px !important; -} diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts b/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts deleted file mode 100644 index 2e24e903..00000000 --- a/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts +++ /dev/null @@ -1,622 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - OnDestroy, - OnInit, - Output -} from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { - MatExpansionPanel, - MatExpansionPanelHeader -} from '@angular/material/expansion'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; - -import { ActionStore, EntityStore } from '@igo2/common'; -import { ConfigService } from '@igo2/core/config'; -import { StorageService } from '@igo2/core/storage'; -import { - FEATURE, - Feature, - FeatureMotion, - IgoMap, - Layer, - LayerLegendListComponent, - MapService, - Research, - SearchBarComponent, - SearchResult, - SearchResultsComponent, - SearchService, - featureFromOl, - featureToOl, - featuresAreTooDeepInView, - getCommonVectorSelectedStyle, - getCommonVectorStyle -} from '@igo2/geo'; -import { - MapState, - QueryState, - SearchState, - StorageState -} from '@igo2/integration'; - -import olFeature from 'ol/Feature'; -import type { default as OlGeometry } from 'ol/geom/Geometry'; -import olPoint from 'ol/geom/Point'; - -import { TranslateModule } from '@ngx-translate/core'; -import { BehaviorSubject, Subscription, combineLatest, tap } from 'rxjs'; - -import { FeatureInfoComponent } from '../feature/feature-info/feature-info.component'; - -@Component({ - selector: 'app-bottompanel', - templateUrl: './bottompanel.component.html', - styleUrls: ['./bottompanel.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - MatExpansionPanel, - MatExpansionPanelHeader, - SearchBarComponent, - MatTooltip, - MatIconButton, - MatIcon, - LayerLegendListComponent, - FeatureInfoComponent, - SearchResultsComponent, - AsyncPipe, - TranslateModule - ] -}) -export class BottomPanelComponent implements OnInit, OnDestroy { - title$: BehaviorSubject = new BehaviorSubject(undefined); - - @Input() - get legendPanelOpened(): boolean { - return this._legendPanelOpened; - } - set legendPanelOpened(value: boolean) { - this._legendPanelOpened = value; - } - private _legendPanelOpened: boolean; - - @Output() closeLegend = new EventEmitter(); - - @Input() - get map(): IgoMap { - return this.mapState.map; - } - - @Input() hideToggle = false; - - @Input() mobile: boolean; // to pass the input to featureDetails tooltip - - @Input() mapQueryClick: boolean; - - @Input() searchState: SearchState; - - @Output() mapQuery = new EventEmitter(); - - get queryStore(): EntityStore { - return this.queryState.store; - } - - private focusedResult$: BehaviorSubject = new BehaviorSubject( - undefined - ); - - resultSelected$ = new BehaviorSubject>(undefined); - - @Output() selectFeature = new EventEmitter(); - - @Input() - get feature(): Feature { - return this._feature; - } - set feature(value: Feature) { - this._feature = value; - this.cdRef.detectChanges(); - this.selectFeature.emit(); - } - private _feature: Feature; - - public selectedFeature: Feature; - public hasFeatureEmphasisOnSelection = false; - - @Input() - get term(): string { - return this._term; - } - set term(value: string) { - this._term = value; - this.pageIterator = []; - } - public _term: string; - - @Input() - get searchInit(): boolean { - return this._searchInit; - } - set searchInit(value: boolean) { - this._searchInit = value; - } - private _searchInit: boolean; - - public store = new ActionStore([]); - public showSearchBar: boolean; - get termSplitter(): string { - return this.searchState.searchTermSplitter$.value; - } - public forceCoordsNA: boolean = false; - - public clearedSearchbar = false; - - public lonlat; - public mapProjection: string; - - get searchStore(): EntityStore { - return this.searchState.store; - } - - public pageIterator: { sourceId: string }[] = []; - - get storageService(): StorageService { - return this.storageState.storageService; - } - private abstractFocusedResult: Feature; - private abstractSelectedResult: Feature; - public withZoomButton = false; - - zoomAuto$: BehaviorSubject = new BehaviorSubject(false); - - get zoomAuto(): boolean { - return this._zoomAuto; - } - set zoomAuto(value) { - if (value !== !this._zoomAuto) { - return; - } - this._zoomAuto = value; - this.zoomAuto$.next(value); - this.storageService.set('zoomAuto', value); - } - private _zoomAuto = false; - - private resultOrResolution$$: Subscription; - - private shownResultsEmphasisGeometries: Feature[] = []; - - @Input() - get layers(): Layer[] { - return this._layers; - } - set layers(value: Layer[]) { - this._layers = value; - } - private _layers: Layer[]; - - public mapLayersShownInLegend: Layer[]; - - @Input() panelOpenState: boolean; - - @Output() panelOpened = new EventEmitter(); - - @Output() closeQuery = new EventEmitter(); - - constructor( - private configService: ConfigService, - private mapService: MapService, - private searchService: SearchService, - private queryState: QueryState, - private cdRef: ChangeDetectorRef, - private mapState: MapState, - private storageState: StorageState, - private elRef: ElementRef - ) { - this.mapService.setMap(this.map); - this.showSearchBar = this.configService.getConfig( - 'searchBar.showSearchBar', - true - ); - this.zoomAuto = this.storageService.get('zoomAuto') as boolean; - } - - ngOnInit() { - this.closePanel(); - this.forceCoordsNA = this.configService.getConfig('app.forceCoordsNA'); - - this.queryStore.entities$.subscribe((entities) => { - if (entities.length > 0) { - this.openPanel(); - this.mapQuery.emit(true); - this.clearSearch(); - this.searchInit = false; - } else { - if (!this.legendPanelOpened && !this.searchInit) { - this.closePanel(); - } - } - }); - - this.map.propertyChange$.subscribe(() => { - this.mapLayersShownInLegend = this.map.layers.filter( - (layer) => layer.showInLayerList !== false - ); - }); - - let latestResult; - let trigger; - if (this.hasFeatureEmphasisOnSelection) { - this.resultOrResolution$$ = combineLatest([ - this.focusedResult$.pipe( - tap((res) => { - latestResult = res; - trigger = 'focused'; - }) - ), - this.resultSelected$.pipe( - tap((res) => { - latestResult = res; - trigger = 'selected'; - }) - ), - this.map.viewController.resolution$, - this.store.entities$ - ]).subscribe(() => this.buildResultEmphasis(latestResult, trigger)); - } - } - - ngOnDestroy() { - this.searchInit = false; - this.mapQuery.emit(false); - this.store.destroy(); - this.store.entities$.unsubscribe(); - this.map.propertyChange$.unsubscribe; - this.queryState.store.destroy(); - this.clearSearch(); - } - - onSearchTermChange(term = '') { - this.term = term; - this.clearedSearchbar = false; - const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); - this.searchState.setSearchTerm(term); - if (termWithoutHashtag.length < 2) { - this.searchStore.clear(); - this.selectedFeature = undefined; - this.searchInit = false; - this.clearSearch(); - } else { - if (this.mapQueryClick) { - this.queryState.store.softClear(); - this.mapQuery.emit(false); - this.searchInit = true; - } - } - } - - onSearch(event: { research: Research; results: SearchResult[] }) { - this.openPanel(); - if (this.mapQueryClick) { - // to clear the mapQuery if a search is initialized - this.queryState.store.softClear(); - this.map.queryResultsOverlay.clear(); - this.mapQuery.emit(false); - } - this.legendPanelOpened = false; - this.queryState.store.softClear(); - this.searchInit = true; - this.clearedSearchbar = false; - this.store.clear(); - const results = event.results; - this.searchStore.state.updateAll({ focused: false, selected: false }); - const newResults = this.searchStore.entities$.value - .filter((result: SearchResult) => result.source !== event.research.source) - .concat(results); - this.searchStore.updateMany(newResults); - - setTimeout(() => { - const igoList = this.elRef.nativeElement.querySelector('igo-list'); - let moreResults; - event.research.request.subscribe((source) => { - if (!source[0] || !source[0].source) { - moreResults = null; - } else if (source[0].source.getId() === 'icherche') { - moreResults = igoList.querySelector('.icherche .moreResults'); - } else if (source[0].source.getId() === 'ilayer') { - moreResults = igoList.querySelector('.ilayer .moreResults'); - } else if (source[0].source.getId() === 'nominatim') { - moreResults = igoList.querySelector('.nominatim .moreResults'); - } else { - moreResults = igoList.querySelector( - '.' + source[0].source.getId() + ' .moreResults' - ); - } - if ( - moreResults !== null && - !this.isScrolledIntoView(igoList, moreResults) - ) { - igoList.scrollTop = - moreResults.offsetTop + - moreResults.offsetHeight - - igoList.clientHeight; - } - }); - }, 250); - } - - isScrolledIntoView(elemSource, elem) { - const padding = 6; - const docViewTop = elemSource.scrollTop; - const docViewBottom = docViewTop + elemSource.clientHeight; - - const elemTop = elem.offsetTop; - const elemBottom = elemTop + elem.clientHeight + padding; - return elemBottom <= docViewBottom && elemTop >= docViewTop; - } - /** - * Try to add a feature to the map when it's being focused - * @internal - * @param result A search result that could be a feature - */ - - onResultFocus(result: SearchResult) { - this.focusedResult$.next(result); - if (result.meta.dataType === FEATURE && result.data.geometry) { - result.data.meta.style = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: result.data as Feature | olFeature }, - this.searchState.searchOverlayStyleFocus, - result.style?.focus ? result.style.focus : {} - ) - ); - - const feature = - this.map.searchResultsOverlay.dataSource.ol.getFeatureById( - result.meta.id - ); - if (feature) { - feature.setStyle(result.data.meta.style); - return; - } - this.map.searchResultsOverlay.addFeature( - result.data as Feature, - FeatureMotion.None - ); - } - this.tryAddFeatureToMap(result); - this.selectedFeature = (result as SearchResult).data; - if (this.selectedFeature !== undefined) { - this.closePanel(); - } - } - - /** - * Try to add a feature to the map overlay - * @param layer A search result that could be a feature - */ - private tryAddFeatureToMap(layer: SearchResult) { - if (this.searchState.setSelectedResult !== undefined) { - this.closePanel(); - } - if (layer.meta.dataType !== FEATURE) { - return undefined; - } - - // Somethimes features have no geometry. It happens with some GetFeatureInfo - if (layer.data.geometry === undefined) { - return; - } - - this.map.searchResultsOverlay.setFeatures( - [layer.data] as Feature[], - FeatureMotion.Default - ); - this.closePanel(); - this.hasFeatureEmphasisOnSelection = this.configService.getConfig( - 'hasFeatureEmphasisOnSelection' - ); - } - - /* - * Remove a feature to the map overlay - */ - removeFeatureFromMap() { - this.map.searchResultsOverlay.clear(); - this.closePanel(); - } - - onSearchCoordinate() { - this.searchStore.clear(); - const results = 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 }); - }); - } - } - } - - onSearchBarClick(event) { - /// prevents panel to close on clear search - if (!this.panelOpenState && this.clearedSearchbar === false) { - this.openPanel(); - } - event.stopPropagation(); - } - - clearQuery(): void { - this.queryState.store.softClear(); - this.queryState.store.clear(); - this.mapQuery.emit(false); - this.removeFeatureFromMap(); - } - - closePanelOnCloseQuery() { - this.mapQuery.emit(false); - this.closeQuery.emit(); - this.cdRef.detectChanges(); - if (this.searchInit || this.legendPanelOpened) { - this.openPanel(); - } - } - - clearSearchBar(event) { - this.searchInit = false; - this.clearSearch(); - this.closePanel(); - this.clearedSearchbar = true; - if (event) { - event.stopPropagation(); //prevents panel toggling on click or focus - } - } - - clearSearch() { - this.map.searchResultsOverlay.clear(); - this.searchStore.clear(); - this.searchState.setSelectedResult(undefined); - this.searchState.deactivateCustomFilterTermStrategy(); - this.term = ''; - } - - closePanelLegend() { - /* this flushes the legend whenever a user closes the panel. if not, - the user has to click twice on the legend button to open the legend with the button - */ - this.legendPanelOpened = false; - this.closePanel(); - this.closeLegend.emit(); - this.map.propertyChange$.unsubscribe; - } - - panelOpenedFromFeature(event) { - this.panelOpened.emit(event); - } - - mapQueryFromFeature(event) { - this.mapQuery.emit(event); - } - - closePanel() { - if (!this.searchInit && !this.mapQueryClick && !this.legendPanelOpened) { - this.panelOpened.emit(false); - } - } - - openPanel() { - this.panelOpened.emit(true); - } - - private clearFeatureEmphasis(trigger: 'selected' | 'focused' | 'shown') { - if (trigger === 'focused' && this.abstractFocusedResult) { - this.map.searchResultsOverlay.removeFeature(this.abstractFocusedResult); - this.abstractFocusedResult = undefined; - } - if (trigger === 'selected' && this.abstractSelectedResult) { - this.map.searchResultsOverlay.removeFeature(this.abstractSelectedResult); - this.abstractSelectedResult = undefined; - } - if (trigger === 'shown') { - this.shownResultsEmphasisGeometries.map((shownResult) => - this.map.searchResultsOverlay.removeFeature(shownResult) - ); - this.shownResultsEmphasisGeometries = []; - } - } - - private buildResultEmphasis( - result: SearchResult, - trigger: 'selected' | 'focused' | 'shown' | undefined - ) { - if (trigger !== 'shown') { - this.clearFeatureEmphasis(trigger); - } - if (!result || !result.data.geometry) { - return; - } - const myOlFeature = featureToOl(result.data, this.map.projection); - const olGeometry = myOlFeature.getGeometry(); - if ( - featuresAreTooDeepInView( - this.map.viewController, - olGeometry.getExtent() as [number, number, number, number], - 0.0025 - ) - ) { - const extent = olGeometry.getExtent(); - const x = extent[0] + (extent[2] - extent[0]) / 2; - const y = extent[1] + (extent[3] - extent[1]) / 2; - const feature1 = new olFeature({ - name: `${trigger}AbstractResult'`, - geometry: new olPoint([x, y]) - }); - const abstractResult = featureFromOl(feature1, this.map.projection); - - let computedStyle; - let zIndexOffset = 0; - - switch (trigger) { - case 'focused': - computedStyle = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyleFocus, - result.style?.focus ? result.style.focus : {} - ) - ); - zIndexOffset = 2; - break; - case 'shown': - computedStyle = getCommonVectorStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyle, - result.style?.base ? result.style.base : {} - ) - ); - break; - case 'selected': - computedStyle = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyleSelection, - result.style?.selection ? result.style.selection : {} - ) - ); - zIndexOffset = 1; - break; - } - abstractResult.meta.style = computedStyle; - abstractResult.meta.style.setZIndex(2000 + zIndexOffset); - this.map.searchResultsOverlay.addFeature( - abstractResult, - FeatureMotion.None - ); - if (trigger === 'focused') { - this.abstractFocusedResult = abstractResult; - } - if (trigger === 'selected') { - this.abstractSelectedResult = abstractResult; - } - if (trigger === 'shown') { - this.shownResultsEmphasisGeometries.push(abstractResult); - } - } else { - this.clearFeatureEmphasis(trigger); - } - } -} diff --git a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.html b/src/app/pages/portal/panels/feature/feature-info/feature-info.component.html deleted file mode 100644 index 21fce4fe..00000000 --- a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.html +++ /dev/null @@ -1,28 +0,0 @@ -@if (store.entities$ | async) { -
-

{{ title }}

- -
- @if (resultSelected$.value && !customFeatureDetails) { - - - } - @if (resultSelected$.value && customFeatureDetails) { - - - } -} diff --git a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.scss b/src/app/pages/portal/panels/feature/feature-info/feature-info.component.scss deleted file mode 100644 index 10f1310a..00000000 --- a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import '../../../portal.variables'; - -:host { - @include mobile { - width: 100%; - overflow-x: clip; - } -} - -.title { - display: flex; - align-items: center; - justify-content: space-between; -} - -h4 { - margin-top: 8px !important; -} - -igo-feature-details { - ::ng-deep table { - margin: 2em auto; - border-spacing: 0px; - border-collapse: collapse; - tbody { - tr:nth-child(odd) { - background-color: #fff !important; - } - tr { - border-bottom: 1px solid #dae6f0; - td { - font: - normal 16px/24px 'Open Sans', - sans-serif; - color: #223654; - margin-top: 4px; - margin-bottom: 4px; - text-align: left; - width: 62% !important; - padding: 0.5em !important; - } - td:first-child { - font-weight: bold; - margin-bottom: 8px; - width: 38% !important; - } - } - tr:hover { - cursor: pointer; - background-color: #dae6f0 !important; - } - } - } -} diff --git a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.ts b/src/app/pages/portal/panels/feature/feature-info/feature-info.component.ts deleted file mode 100644 index 6cbf649c..00000000 --- a/src/app/pages/portal/panels/feature/feature-info/feature-info.component.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - HostBinding, - HostListener, - Input, - OnDestroy, - OnInit, - Output -} from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; - -import { EntityStore, StopPropagationDirective } from '@igo2/common'; -import { ConfigService } from '@igo2/core/config'; -import { LanguageService } from '@igo2/core/language'; -import { MediaService } from '@igo2/core/media'; -import { StorageService } from '@igo2/core/storage'; -import { - Feature, - FeatureDetailsComponent, - FeatureMotion, - IgoMap, - SearchResult, - computeOlFeaturesExtent, - featureToOl, - featuresAreOutOfView, - getCommonVectorSelectedStyle, - getCommonVectorStyle -} from '@igo2/geo'; -import { QueryState, SearchState, StorageState } from '@igo2/integration'; - -import { TranslateModule } from '@ngx-translate/core'; -import { BehaviorSubject, Subscription, combineLatest } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; - -import { FeatureCustomDetailsComponent } from '../feature-custom-details/feature-custom-details.component'; - -@Component({ - selector: 'app-feature-info', - templateUrl: './feature-info.component.html', - styleUrls: ['./feature-info.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - MatIconButton, - StopPropagationDirective, - MatTooltip, - MatIcon, - FeatureDetailsComponent, - FeatureCustomDetailsComponent, - AsyncPipe, - TranslateModule - ] -}) -export class FeatureInfoComponent implements OnInit, OnDestroy { - get storageService(): StorageService { - return this.storageState.storageService; - } - - @Input() - get map(): IgoMap { - return this._map; - } - set map(value: IgoMap) { - this._map = value; - } - private _map: IgoMap; - - @Input() - get store(): EntityStore> { - return this._store; - } - set store(value: EntityStore>) { - this._store = value; - } - private _store: EntityStore>; - - @Output() closeQuery = new EventEmitter(); - - @Input() mapQueryClick: boolean; - - @Output() mapQuery = new EventEmitter(); - - @Input() panelOpenState: boolean; - - @Input() mobile: boolean; - - @Output() panelOpened = new EventEmitter(); - - private isResultSelected$ = new BehaviorSubject(false); - public isSelectedResultOutOfView$ = new BehaviorSubject(false); - private isSelectedResultOutOfView$$: Subscription; - private initialized = true; - public featureTitle: string; - public title: string; - public customFeatureTitle: boolean; - public customFeatureDetails: boolean; - - @Input() searchState: SearchState; - @Input() - get feature(): Feature { - return this._feature; - } - set feature(value: Feature) { - this._feature = value; - } - private _feature: Feature; - - private resultOrResolution$$: Subscription; - - resultSelected$ = new BehaviorSubject>(undefined); - - @HostBinding('style.visibility') - get displayStyle() { - if (this.results.length) { - if (this.results.length === 1 && this.initialized) { - this.selectResult(this.results[0]); - } - return 'visible'; - } - return 'hidden'; - } - - @HostListener('document:keydown.escape', ['$event']) onEscapeHandler( - event: KeyboardEvent - ) { - this.clearButton(); - } - - get results(): SearchResult[] { - return this.store.all(); - } - - get searchStore(): EntityStore { - return this.searchState.store; - } - - @Input() - get mapQueryInit(): boolean { - return this._mapQueryInit; - } - set mapQueryInit(mapQueryInit: boolean) { - this._mapQueryInit = mapQueryInit; - } - private _mapQueryInit = false; - - constructor( - public mediaService: MediaService, - public languageService: LanguageService, - private storageState: StorageState, - private queryState: QueryState, - private configService: ConfigService - ) { - this.customFeatureTitle = this.configService.getConfig( - 'customFeatureTitle', - false - ); - this.customFeatureDetails = this.configService.getConfig( - 'customFeatureDetails', - false - ); - } - - private monitorResultOutOfView() { - this.isSelectedResultOutOfView$$ = combineLatest([ - this.map.viewController.state$, - this.resultSelected$ - ]) - .pipe(debounceTime(100)) - .subscribe((bunch) => { - const selectedResult = bunch[1]; - if (!selectedResult) { - this.isSelectedResultOutOfView$.next(false); - return; - } - const selectedOlFeature = featureToOl( - selectedResult.data, - this.map.projection - ); - const selectedOlFeatureExtent = computeOlFeaturesExtent( - [selectedOlFeature], - this.map.viewProjection - ); - this.isSelectedResultOutOfView$.next( - featuresAreOutOfView(this.map.getExtent(), selectedOlFeatureExtent) - ); - }); - } - - ngOnInit() { - this.store.entities$.subscribe(() => { - this.initialized = true; - }); - this.monitorResultOutOfView(); - } - - ngOnDestroy(): void { - this.clearButton(); - if (this.resultOrResolution$$) { - this.resultOrResolution$$.unsubscribe(); - } - if (this.isSelectedResultOutOfView$$) { - this.isSelectedResultOutOfView$$.unsubscribe(); - } - } - - onTitleClick() { - /// define your own function, ex zoom to feature - this.closeQuery.emit(); - } - - selectResult(result: SearchResult) { - this.store.state.update( - result, - { - focused: true, - selected: true - }, - true - ); - this.resultSelected$.next(result); - - const features = []; - for (const feature of this.store.all()) { - if (feature.meta.id === result.meta.id) { - feature.data.meta.style = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: feature.data }, - this.queryState.queryOverlayStyleSelection - ) - ); - feature.data.meta.style.setZIndex(2000); - } else { - feature.data.meta.style = getCommonVectorStyle( - Object.assign( - {}, - { feature: feature.data }, - this.queryState.queryOverlayStyle - ) - ); - } - features.push(feature.data); - this.featureTitle = feature.meta.title; // will define the feature info title in the panel - this.getTitle(); - } - this.map.queryResultsOverlay.removeFeatures(features); - this.map.queryResultsOverlay.addFeatures(features, FeatureMotion.None); - - this.isResultSelected$.next(true); - this.initialized = false; - } - - getTitle() { - this.title = this.customFeatureTitle - ? this.languageService.translate.instant('feature.title') - : this.featureTitle; - } - - public unselectResult() { - this.resultSelected$.next(undefined); - this.isResultSelected$.next(false); - this.store.state.clear(); - - const features = []; - for (const feature of this.store.all()) { - feature.data.meta.style = getCommonVectorStyle( - Object.assign( - {}, - { feature: feature.data }, - this.queryState.queryOverlayStyle - ) - ); - features.push(feature.data); - } - this.map.queryResultsOverlay.setFeatures( - features, - FeatureMotion.None, - 'map' - ); - } - - public clearButton() { - this.map.queryResultsOverlay.clear(); - this.store.clear(); - this.unselectResult(); - this.mapQuery.emit(false); - this.panelOpened.emit(false); - this.closeQuery.emit(); - } - - mapQueryFromFeatureDetails(event) { - this.mapQuery.emit(event); - } -} diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.component.html b/src/app/pages/portal/panels/panels-handler/panels-handler.component.html new file mode 100644 index 00000000..2448fa80 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.component.html @@ -0,0 +1,99 @@ +@if ((mobileMode$ | async) === false) { +
+ +
+ + + + + + + + +
+
+
+ +
+
+} +@if (mobileMode$ | async) { + + + + + +
+ + + + + + + + +
+
+} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.component.scss b/src/app/pages/portal/panels/panels-handler/panels-handler.component.scss new file mode 100644 index 00000000..1f9cecb7 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.component.scss @@ -0,0 +1,60 @@ +@use 'variables'; +@import '../../portal.variables'; + +:host { + background-color: rgb(255, 255, 255); + + mat-sidenav { + ::ng-deep .mat-drawer-inner-container { + top: $search-bar-height; + position: relative; + height: calc(100% - $search-bar-height); + } + + @extend %box-shadowed-bottom-right; + + height: $app-sidenav-height; + width: $app-sidenav-width; + + overflow: visible; + + .app-sidenav-content { + padding: 0 16px 0 16px !important; + } + } + + .sidenav-button-container { + position: absolute; + top: 50%; + left: calc($app-sidenav-width + 1px); + z-index: 1; + padding: 0; + transition: left 300ms; + + button { + border-radius: 0; + &:hover { + background-color: #156bb2; + } + } + } + + .sidenav-closed { + left: 0 !important; + position: absolute; + } + + mat-expansion-panel { + position: fixed; + display: block; + bottom: 0; + z-index: 9999; + width: 100%; + + ::ng-deep .mat-expansion-panel-content { + overflow-y: scroll !important; + overflow-x: clip; + height: 264px; + } + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.component.ts b/src/app/pages/portal/panels/panels-handler/panels-handler.component.ts new file mode 100644 index 00000000..ceaa1ed6 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.component.ts @@ -0,0 +1,113 @@ +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; +import { + AsyncPipe, + NgClass, + NgSwitch, + NgSwitchCase, + NgTemplateOutlet +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, + TemplateRef +} from '@angular/core'; +import { MatIconButton, MatMiniFabButton } from '@angular/material/button'; +import { + MatExpansionPanel, + MatExpansionPanelHeader +} from '@angular/material/expansion'; +import { MatIcon } from '@angular/material/icon'; +import { MatSidenav } from '@angular/material/sidenav'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { SearchBarComponent } from '@igo2/geo'; + +import { TranslateModule } from '@ngx-translate/core'; +import { Observable, concatMap, distinctUntilChanged, of } from 'rxjs'; + +import { ShownComponent } from './panels-handler.enum'; +import { PanelsHandlerState } from './panels-handler.state'; +import { FilterPanelComponent } from './panels/filter/filter-panel.component'; +import { LegendPanelComponent } from './panels/legend/legend-panel.component'; +import { MapQueryResultsPanelComponent } from './panels/map-query-results/map-query-results-panel.component'; +import { SearchResultPanelComponent } from './panels/search-results/search-results-panel.component'; + +@Component({ + selector: 'app-panels-handler', + templateUrl: './panels-handler.component.html', + styleUrls: ['./panels-handler.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatSidenav, + MatExpansionPanel, + MatExpansionPanelHeader, + MatTooltip, + MatIconButton, + MatIcon, + AsyncPipe, + TranslateModule, + NgClass, + MatMiniFabButton, + NgTemplateOutlet, + SearchBarComponent, + NgSwitch, + NgSwitchCase, + LegendPanelComponent, + SearchResultPanelComponent, + MapQueryResultsPanelComponent, + FilterPanelComponent + ] +}) +export class PanelsHandlerComponent implements OnInit { + public mobileMode$: Observable; + public openedPanel: boolean = false; + + @Input() searchBar: TemplateRef; + + constructor( + private breakpointObserver: BreakpointObserver, + public panelsHandlerState: PanelsHandlerState + ) { + this.mobileMode$ = this.breakpointObserver + .observe('(min-width: 768px)') + .pipe( + distinctUntilChanged(), + concatMap((breakpointState: BreakpointState) => { + return of(!breakpointState.matches); + }) + ); + } + + ngOnInit() { + this.panelsHandlerState.opened$.subscribe((opened) => + this.handlePanels(opened) + ); + this.panelsHandlerState.searchState.store.empty$.subscribe((e) => { + if (!e) { + this.panelsHandlerState.setShownComponent(ShownComponent.Search); + this.panelsHandlerState.setOpenedState(true); + } + }); + this.panelsHandlerState.queryState.store.empty$.subscribe((e) => { + if (!e) { + this.panelsHandlerState.setShownComponent(ShownComponent.Query); + this.panelsHandlerState.setOpenedState(true); + } + }); + } + + handlePanels(value: boolean) { + this.openedPanel = value; + } + + openWithinPanel() { + this.handlePanels(true); + } + + closeWithinPanel() { + this.handlePanels(false); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.enum.ts b/src/app/pages/portal/panels/panels-handler/panels-handler.enum.ts new file mode 100644 index 00000000..bb64c644 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.enum.ts @@ -0,0 +1,12 @@ +export enum ShownComponent { + Query = 'query', + Search = 'search', + Legend = 'legend', + Filter = 'filter' +} + +export enum SearchResultAction { + Focus = 'focus', + Select = 'select', + Unfocus = 'unfocus' +} diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.interface.ts b/src/app/pages/portal/panels/panels-handler/panels-handler.interface.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.state.spec.ts b/src/app/pages/portal/panels/panels-handler/panels-handler.state.spec.ts new file mode 100644 index 00000000..821f69b2 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.state.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PanelsHandlerState } from './panels-handler.state'; + +describe('PanelsHandlerState', () => { + let service: PanelsHandlerState; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PanelsHandlerState); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/pages/portal/panels/panels-handler/panels-handler.state.ts b/src/app/pages/portal/panels/panels-handler/panels-handler.state.ts new file mode 100644 index 00000000..1857277f --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels-handler.state.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; + +import { IgoMap } from '@igo2/geo'; +import { QueryState, SearchState } from '@igo2/integration'; + +import { BehaviorSubject } from 'rxjs'; + +import { ShownComponent } from './panels-handler.enum'; + +@Injectable({ + providedIn: 'root' +}) +export class PanelsHandlerState { + readonly opened$: BehaviorSubject = new BehaviorSubject(false); + readonly shownComponent$: BehaviorSubject = + new BehaviorSubject(undefined); + map: IgoMap; + queryState: QueryState; + searchState: SearchState; + private devaultShownComponent: ShownComponent; + private showComponentHistory: ShownComponent[] = []; + + constructor() { + this.devaultShownComponent = ShownComponent.Search; + this.shownComponent$.next(this.devaultShownComponent); + this.shownComponent$.subscribe((sc) => { + this.showComponentHistory = this.showComponentHistory.filter( + (sch) => sch !== sc + ); + this.showComponentHistory.push(sc); + }); + } + + setOpenedState(value: boolean) { + this.opened$.next(value); + } + + togglePanels() { + const current = this.opened$.getValue(); + this.setOpenedState(!current); + } + + setShownComponent(value: ShownComponent) { + this.shownComponent$.next(value); + } + componentToClose(component: ShownComponent) { + this.showComponentHistory = this.showComponentHistory.filter( + (sch) => sch !== component + ); + const previousComponent = this.showComponentHistory.at(-1); + this.shownComponent$.next(previousComponent ?? this.devaultShownComponent); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.html b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.html new file mode 100644 index 00000000..78db85cc --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.html @@ -0,0 +1,24 @@ +
+

+ {{ 'filter.title' | translate }} +

+ +
+@for ( + layer of panelsHandlerState.map.layers$ | async | filterableDataSource: 'ogc'; + track layer +) { + + +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.scss b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.scss new file mode 100644 index 00000000..6b1a218a --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.scss @@ -0,0 +1,7 @@ +:host { + .close-button { + display: flex; + justify-content: space-between; + align-items: center !important; + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.ts b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.ts new file mode 100644 index 00000000..bf09ecdf --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/filter/filter-panel.component.ts @@ -0,0 +1,39 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { + FilterableDataSourcePipe, + OgcFilterableItemComponent +} from '@igo2/geo'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { ShownComponent } from '../../panels-handler.enum'; +import { PanelsHandlerState } from '../../panels-handler.state'; + +@Component({ + selector: 'app-filter-panel', + templateUrl: './filter-panel.component.html', + styleUrls: ['./filter-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatTooltip, + MatIconButton, + MatIcon, + TranslateModule, + AsyncPipe, + FilterableDataSourcePipe, + OgcFilterableItemComponent + ] +}) +export class FilterPanelComponent { + constructor(public panelsHandlerState: PanelsHandlerState) {} + + clear() { + this.panelsHandlerState.componentToClose(ShownComponent.Filter); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.html b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.html new file mode 100644 index 00000000..0705ac0b --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.html @@ -0,0 +1,21 @@ +
+

+ {{ 'legend.title' | translate }} +

+ +
+ + diff --git a/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.scss b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.scss new file mode 100644 index 00000000..6b1a218a --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.scss @@ -0,0 +1,7 @@ +:host { + .close-button { + display: flex; + justify-content: space-between; + align-items: center !important; + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.ts b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.ts new file mode 100644 index 00000000..b244a56b --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/legend/legend-panel.component.ts @@ -0,0 +1,54 @@ +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit +} from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { Layer, LayerLegendListComponent } from '@igo2/geo'; + +import { TranslateModule } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; + +import { ShownComponent } from '../../panels-handler.enum'; +import { PanelsHandlerState } from '../../panels-handler.state'; + +@Component({ + selector: 'app-legend-panel', + templateUrl: './legend-panel.component.html', + styleUrls: ['./legend-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatTooltip, + MatIconButton, + MatIcon, + TranslateModule, + LayerLegendListComponent + ] +}) +export class LegendPanelComponent implements OnInit, OnDestroy { + public mapLayersShownInLegend: Layer[]; + private layers$$: Subscription; + + constructor(public panelsHandlerState: PanelsHandlerState) {} + + ngOnInit() { + this.layers$$ = this.panelsHandlerState.map.layers$.subscribe((layers) => { + this.mapLayersShownInLegend = layers.filter( + (layer) => layer.showInLayerList !== false + ); + }); + } + + ngOnDestroy() { + this.layers$$.unsubscribe(); + } + + clear() { + this.panelsHandlerState.componentToClose(ShownComponent.Legend); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.html b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.html new file mode 100644 index 00000000..133eadc3 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.html @@ -0,0 +1,23 @@ +@if (selectedFeature$ | async) { +
+

+ {{ title$ | async | translate }} +

+ +
+ @if (selectedFeature$ | async; as selectedFeature) { + @if (!customFeatureDetails) { + + } @else { + + + } + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.scss b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.scss new file mode 100644 index 00000000..98b3df6b --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.scss @@ -0,0 +1,45 @@ +@use 'variables'; +@import '../../../../portal.variables'; + +:host { + .close-button { + display: flex; + justify-content: space-between; + align-items: center !important; + } + igo-feature-details { + ::ng-deep table { + margin: 2em auto; + border-spacing: 0px; + border-collapse: collapse; + tbody { + tr:nth-child(odd) { + background-color: #fff !important; + } + tr { + border-bottom: 1px solid #dae6f0; + td { + font: + normal 16px/24px 'Open Sans', + sans-serif; + color: #223654; + margin-top: 4px; + margin-bottom: 4px; + text-align: left; + width: 62% !important; + padding: 0.5em !important; + } + td:first-child { + font-weight: bold; + margin-bottom: 8px; + width: 38% !important; + } + } + tr:hover { + cursor: pointer; + background-color: #dae6f0 !important; + } + } + } + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.ts b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.ts new file mode 100644 index 00000000..ef46f855 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.component.ts @@ -0,0 +1,92 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { ConfigService } from '@igo2/core/config'; +import { LanguageService } from '@igo2/core/language'; +import { Feature, FeatureDetailsComponent, SearchResult } from '@igo2/geo'; + +import { TranslateModule } from '@ngx-translate/core'; +import { BehaviorSubject, Observable, of, switchMap } from 'rxjs'; + +import { FeatureCustomDetailsComponent } from '../../../feature/feature-custom-details/feature-custom-details.component'; +import { ShownComponent } from '../../panels-handler.enum'; +import { PanelsHandlerState } from '../../panels-handler.state'; +import { onResultSelect } from './map-query-results-panel.utils'; + +@Component({ + selector: 'app-query-results-panel', + templateUrl: './map-query-results-panel.component.html', + styleUrls: ['./map-query-results-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatTooltip, + MatIconButton, + MatIcon, + TranslateModule, + FeatureDetailsComponent, + FeatureCustomDetailsComponent, + AsyncPipe + ] +}) +export class MapQueryResultsPanelComponent implements OnInit { + public title$: BehaviorSubject = new BehaviorSubject(undefined); + public customFeatureTitle: boolean; + public customFeatureDetails: boolean; + public selectedFeature$ = new Observable(undefined); + + constructor( + private configService: ConfigService, + public languageService: LanguageService, + public panelsHandlerState: PanelsHandlerState + ) { + this.customFeatureTitle = this.configService.getConfig( + 'customFeatureTitle', + false + ); + this.customFeatureDetails = this.configService.getConfig( + 'customFeatureDetails', + false + ); + } + + ngOnInit() { + this.selectedFeature$ = + this.panelsHandlerState.queryState.store.entities$.pipe( + switchMap((e: SearchResult[]) => { + this.panelsHandlerState.map.queryResultsOverlay.clear(); + if (!e || !e.length) { + return of(); + } else { + const firstResult = e[0]; + this.panelsHandlerState.queryState.store.state.update( + firstResult, + { + focused: true, + selected: true + }, + true + ); + const feature = firstResult.data; + onResultSelect( + firstResult, + this.panelsHandlerState.map, + this.panelsHandlerState.queryState + ); + this.title$.next( + this.customFeatureTitle ? 'feature.title' : feature.meta.title + ); + return of(feature); + } + }) + ); + } + + clear() { + this.panelsHandlerState.componentToClose(ShownComponent.Query); + this.panelsHandlerState.map.queryResultsOverlay.clear(); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.utils.ts b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.utils.ts new file mode 100644 index 00000000..7cabe20c --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/map-query-results/map-query-results-panel.utils.ts @@ -0,0 +1,41 @@ +import { + FEATURE, + Feature, + FeatureMotion, + IgoMap, + SearchResult, + getCommonVectorSelectedStyle +} from '@igo2/geo'; +import { QueryState } from '@igo2/integration'; + +import olFeature from 'ol/Feature'; +import type { default as OlGeometry } from 'ol/geom/Geometry'; + +export function onResultSelect( + result: SearchResult, + map: IgoMap, + queryState: QueryState +) { + if (result.meta.dataType === FEATURE && result.data.geometry) { + result.data.meta.style = getCommonVectorSelectedStyle( + Object.assign( + {}, + { feature: result.data as Feature | olFeature }, + queryState.queryOverlayStyleSelection, + result.style?.selection ? result.style.selection : {} + ) + ); + + const feature = map.searchResultsOverlay.dataSource.ol.getFeatureById( + result.meta.id + ); + if (feature) { + feature.setStyle(result.data.meta.style); + return; + } + map.queryResultsOverlay.addFeature( + result.data as Feature, + FeatureMotion.None + ); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.html b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.html new file mode 100644 index 00000000..2270019a --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.html @@ -0,0 +1,33 @@ +@if (panelsHandlerState.searchState.store.empty$ | async) { +
+
+
+ {{ 'igo.integration.searchResultsTool.noResults' | translate }} +
+
+ {{ 'igo.integration.searchResultsTool.doSearch' | translate }} +
+

+
+
+} + +@if ((panelsHandlerState.searchState.store.empty$ | async) === false) { + + +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.scss b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.scss new file mode 100644 index 00000000..fb2df80c --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.scss @@ -0,0 +1,5 @@ +:host { + .no-results { + margin: 20px; + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.ts b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.ts new file mode 100644 index 00000000..e67d82b2 --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.component.ts @@ -0,0 +1,90 @@ +import { AsyncPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; + +import { + FeatureMotion, + Research, + SearchResult, + SearchResultsComponent +} from '@igo2/geo'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { SearchResultAction } from '../../panels-handler.enum'; +import { PanelsHandlerState } from '../../panels-handler.state'; +import { + onResultFocus, + onResultSelect, + onResultUnfocus +} from './search-results-panel.utils'; + +@Component({ + selector: 'app-search-results-panel', + templateUrl: './search-results-panel.component.html', + styleUrls: ['./search-results-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + MatTooltip, + MatIconButton, + MatIcon, + TranslateModule, + SearchResultsComponent, + AsyncPipe + ] +}) +export class SearchResultPanelComponent { + public searchResultActions = SearchResultAction; + + constructor(public panelsHandlerState: PanelsHandlerState) {} + + onSearchTermChange(term: string) { + this.panelsHandlerState.searchState.setSearchTerm(term); + } + + onResult(searchResultAction: SearchResultAction, searchResult: SearchResult) { + switch (searchResultAction) { + case SearchResultAction.Focus: + onResultFocus( + searchResult, + this.panelsHandlerState.map, + this.panelsHandlerState.searchState, + { + featureMotion: FeatureMotion.None + } + ); + break; + case SearchResultAction.Select: + this, this.panelsHandlerState.map.searchResultsOverlay.clear(); + onResultSelect( + searchResult, + this.panelsHandlerState.map, + this.panelsHandlerState.searchState + ); + this.close(); + break; + case SearchResultAction.Unfocus: + onResultUnfocus(searchResult, this.panelsHandlerState.map); + break; + } + } + onMoreResults(event: { research: Research; results: SearchResult[] }) { + const results = event.results; + this.panelsHandlerState.searchState.store.state.updateAll({ + focused: false, + selected: false + }); + const newResults = this.panelsHandlerState.searchState.store.entities$.value + .filter((result: SearchResult) => result.source !== event.research.source) + .concat(results); + this.panelsHandlerState.searchState.store.updateMany(newResults); + // todo scroll into view + } + + close() { + this.panelsHandlerState.setOpenedState(false); + } +} diff --git a/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.utils.ts b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.utils.ts new file mode 100644 index 00000000..f8c9629d --- /dev/null +++ b/src/app/pages/portal/panels/panels-handler/panels/search-results/search-results-panel.utils.ts @@ -0,0 +1,77 @@ +import { + CommonVectorStyleOptions, + FEATURE, + Feature, + FeatureMotion, + IgoMap, + SearchResult, + getCommonVectorSelectedStyle +} from '@igo2/geo'; +import { SearchState } from '@igo2/integration'; + +export function onResultFocus( + result: SearchResult, + map: IgoMap, + searchState: SearchState, + options?: { featureMotion?: FeatureMotion } +) { + onResultSelectOrFocus(result, map, searchState, 'focus', options); +} + +export function onResultSelect( + result: SearchResult, + map: IgoMap, + searchState: SearchState, + options?: { featureMotion?: FeatureMotion } +) { + onResultSelectOrFocus(result, map, searchState, 'select', options); +} + +function onResultSelectOrFocus( + result: SearchResult, + map: IgoMap, + searchState: SearchState, + type: 'select' | 'focus', + options?: { featureMotion?: FeatureMotion } +) { + if (result.meta.dataType !== FEATURE) { + return undefined; + } + const feature = (result as SearchResult).data; + + // Somethimes features have no geometry. It happens with some GetFeatureInfo + if (!feature.geometry) { + return; + } + + let searchOverlayStyle: CommonVectorStyleOptions = + searchState.searchOverlayStyle; + let resultStyle: CommonVectorStyleOptions = result.style?.base + ? result.style.base + : {}; + switch (type) { + case 'focus': + searchOverlayStyle = searchState.searchOverlayStyleFocus; + resultStyle = result.style?.focus ? result.style.focus : {}; + break; + case 'select': + searchOverlayStyle = searchState.searchOverlayStyleSelection; + resultStyle = result.style?.selection ? result.style.selection : {}; + break; + } + + feature.meta.style = getCommonVectorSelectedStyle( + Object.assign({}, { feature }, searchOverlayStyle, resultStyle) + ); + + map.searchResultsOverlay.addFeature(feature, options?.featureMotion); +} + +export function onResultUnfocus(result: SearchResult, map: IgoMap) { + const feature = map.searchResultsOverlay.dataSource.ol.getFeatureById( + result.meta.id + ); + if (feature) { + map.searchResultsOverlay.removeFeature(result.data as Feature); + } +} diff --git a/src/app/pages/portal/panels/panels.module.ts b/src/app/pages/portal/panels/panels.module.ts deleted file mode 100644 index f5e09fe6..00000000 --- a/src/app/pages/portal/panels/panels.module.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatIconModule } from '@angular/material/icon'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatTooltipModule } from '@angular/material/tooltip'; - -import { - IgoActionbarModule, - IgoContextMenuModule, - IgoFlexibleModule, - IgoPanelModule, - IgoToolModule -} from '@igo2/common'; -import { IgoContextManagerModule } from '@igo2/context'; -// mobile -import { IgoLanguageModule } from '@igo2/core/language'; -import { IgoMessageModule } from '@igo2/core/message'; -import { - IgoLayerModule, - IgoMapModule, - IgoSearchModule, - IgoSearchResultsModule -} from '@igo2/geo'; -import { IgoAppSearchModule } from '@igo2/integration'; - -import { BottomPanelComponent } from './bottompanel/bottompanel.component'; -import { SidePanelComponent } from './sidepanel/sidepanel.component'; - -@NgModule({ - imports: [ - CommonModule, - MatIconModule, - MatButtonModule, - MatSidenavModule, - MatTooltipModule, - IgoLanguageModule, - IgoPanelModule, - IgoFlexibleModule, - IgoContextManagerModule, - IgoToolModule, - //SEARCH - MatCardModule, - IgoMessageModule, - IgoMapModule, - IgoSearchModule, - IgoActionbarModule, - IgoContextMenuModule, - IgoAppSearchModule, - IgoSearchModule.forRoot(), - MatExpansionModule, - IgoLayerModule, - IgoSearchResultsModule, - SidePanelComponent, - BottomPanelComponent - ], - exports: [SidePanelComponent, BottomPanelComponent] -}) -export class AppPanelsModule {} diff --git a/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.html b/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.html deleted file mode 100644 index c4442110..00000000 --- a/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.html +++ /dev/null @@ -1,36 +0,0 @@ -@if (store && (store.stateView.empty$ | async) === false) { - -
- - - - - - -
-
-} diff --git a/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts b/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts deleted file mode 100644 index eafc7f56..00000000 --- a/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts +++ /dev/null @@ -1,700 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - ElementRef, - EventEmitter, - HostListener, - Input, - OnDestroy, - OnInit, - Output -} from '@angular/core'; - -import { - EntityState, - EntityStore, - FlexibleComponent, - ToolComponent, - getEntityTitle -} from '@igo2/common'; -import { ConfigService } from '@igo2/core/config'; -import { - FEATURE, - Feature, - FeatureMotion, - IgoMap, - Research, - SearchResult, - SearchResultAddButtonComponent, - SearchResultsComponent, - computeOlFeaturesExtent, - featureFromOl, - featureToOl, - featuresAreOutOfView, - featuresAreTooDeepInView, - getCommonVectorSelectedStyle, - getCommonVectorStyle, - moveToOlFeatures, - roundCoordTo -} from '@igo2/geo'; -import { - DirectionState, - MapState, - QueryState, - SearchState, - ToolState -} from '@igo2/integration'; - -import olFeature from 'ol/Feature'; -import olFormatGeoJSON from 'ol/format/GeoJSON'; -import type { default as OlGeometry } from 'ol/geom/Geometry'; -import olPoint from 'ol/geom/Point'; -import * as olProj from 'ol/proj'; - -import pointOnFeature from '@turf/point-on-feature'; -import { BehaviorSubject, Observable, Subscription, combineLatest } from 'rxjs'; -import { debounceTime, map } from 'rxjs/operators'; - -/** - * Tool to browse the search results - */ -@ToolComponent({ - name: 'searchResults', - title: 'igo.integration.tools.searchResults', - icon: 'magnify' -}) -@Component({ - selector: 'app-search-results-tool', - templateUrl: './search-results-tool.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FlexibleComponent, - SearchResultsComponent, - SearchResultAddButtonComponent, - AsyncPipe - ] -}) -export class SearchResultsToolComponent implements OnInit, OnDestroy { - /** - * to show hide results icons - */ - @Input() showIcons: boolean = true; - @Input() searchState: SearchState; - - private hasFeatureEmphasisOnSelection: boolean; - private showResultsGeometries$$: Subscription; - private getRoute$$: Subscription; - private shownResultsGeometries: Feature[] = []; - private shownResultsEmphasisGeometries: Feature[] = []; - private focusedResult$: BehaviorSubject = new BehaviorSubject( - undefined - ); - public isSelectedResultOutOfView$ = new BehaviorSubject(false); - private isSelectedResultOutOfView$$: Subscription; - private abstractFocusedResult: Feature; - private abstractSelectedResult: Feature; - - public debouncedEmpty$: BehaviorSubject = new BehaviorSubject(true); - private debouncedEmpty$$: Subscription; - @Output() featureSelected = new EventEmitter(); - public addFeaturetoLayer: boolean; // in the result features list, display an icon "add this feature to a layer" - - /** - * Store holding the search results - * @internal - */ - get store(): EntityStore { - return this.searchState.store; - } - - /** - * Map to display the results on - * @internal - */ - get map(): IgoMap { - return this.mapState.map; - } - - get featureTitle(): string { - return this.feature ? getEntityTitle(this.feature) : undefined; - } - - get feature$(): Observable { - return this.store.stateView - .firstBy$((e) => e.state.focused) - .pipe( - map( - (element) => - (this.feature = element - ? (element.entity.data as Feature) - : undefined) - ) - ); - } - public feature: Feature; - - public term = ''; - private searchTerm$$: Subscription; - - get termSplitter(): string { - return this.searchState.searchTermSplitter$.value; - } - - private format = new olFormatGeoJSON(); - - get searchStore(): EntityStore { - return this.searchState.store; - } - - public initialized: boolean = undefined; - - @Output() searchEvent = new EventEmitter(); - - @Input() - get mapQueryClick(): boolean { - return this._mapQueryClick; - } - set mapQueryClick(value: boolean) { - this._mapQueryClick = value; - } - private _mapQueryClick: boolean; - - @Input() - get searchInit(): boolean { - return this._searchInit; - } - set searchInit(value: boolean) { - this._searchInit = value; - } - private _searchInit: boolean; - - @Input() - get legendPanelOpened(): boolean { - return this._legendPanelOpened; - } - set legendPanelOpened(value: boolean) { - this._legendPanelOpened = value; - } - private _legendPanelOpened: boolean; - - get queryStore(): EntityStore { - return this.queryState.store; - } - - constructor( - private mapState: MapState, - private elRef: ElementRef, - public toolState: ToolState, - private directionState: DirectionState, - configService: ConfigService, - private queryState: QueryState - ) { - this.hasFeatureEmphasisOnSelection = configService.getConfig( - 'hasFeatureEmphasisOnSelection' - ); - this.addFeaturetoLayer = configService.getConfig('addFeaturetoLayer'); - } - - ngOnInit() { - this.initialized = true; - this.searchTerm$$ = this.searchState.searchTerm$.subscribe( - (searchTerm: string) => { - if (searchTerm !== undefined && searchTerm !== null) { - this.term = searchTerm; - } - } - ); - - if (this.hasFeatureEmphasisOnSelection) { - if (!this.searchState.focusedOrResolution$$) { - this.searchState.focusedOrResolution$$ = combineLatest([ - this.focusedResult$, - this.map.viewController.resolution$ - ]).subscribe((bunch: [SearchResult, number]) => - this.buildResultEmphasis(bunch[0], 'focused') - ); - } - - if (!this.searchState.selectedOrResolution$$) { - this.searchState.selectedOrResolution$$ = combineLatest([ - this.searchState.selectedResult$, - this.map.viewController.resolution$ - ]).subscribe((bunch: [SearchResult, number]) => - this.buildResultEmphasis(bunch[0], 'selected') - ); - } - } - this.monitorResultOutOfView(); - - this.showResultsGeometries$$ = combineLatest([ - this.searchState.searchResultsGeometryEnabled$, - this.store.stateView.all$(), - this.focusedResult$, - this.searchState.selectedResult$, - this.searchState.searchTerm$, - this.map.viewController.resolution$ - ]).subscribe( - ( - bunch: [ - boolean, - { entity: SearchResult; state: EntityState }[], - SearchResult, - SearchResult, - string, - number - ] - ) => { - const searchResultsGeometryEnabled = bunch[0]; - const searchResults = bunch[1]; - - if (this.hasFeatureEmphasisOnSelection) { - this.clearFeatureEmphasis('shown'); - } - this.shownResultsGeometries.map((result) => - this.map.queryResultsOverlay.removeFeature(result) - ); - const featureToHandleGeom = searchResults.filter( - (result) => - result.entity.meta.dataType === FEATURE && - result.entity.data.geometry && - !result.state.selected && - !result.state.focused - ); - - featureToHandleGeom.map((result) => { - if (searchResultsGeometryEnabled) { - result.entity.data.meta.style = getCommonVectorStyle( - Object.assign( - {}, - { - feature: result.entity.data as Feature | olFeature - }, - this.searchState.searchOverlayStyle, - result.entity.style?.base ? result.entity.style.base : {} - ) - ); - this.shownResultsGeometries.push(result.entity.data as Feature); - this.map.queryResultsOverlay.addFeature( - result.entity.data as Feature, - FeatureMotion.None - ); - if (this.hasFeatureEmphasisOnSelection) { - this.buildResultEmphasis( - result.entity as SearchResult, - 'shown' - ); - } - } - }); - } - ); - - this.debouncedEmpty$$ = this.store.stateView.empty$ - .pipe(debounceTime(1500)) - .subscribe((empty) => this.debouncedEmpty$.next(empty)); - } - - private monitorResultOutOfView() { - this.isSelectedResultOutOfView$$ = combineLatest([ - this.map.viewController.state$, - this.searchState.selectedResult$ - ]) - .pipe(debounceTime(100)) - .subscribe((bunch) => { - const selectedResult = bunch[1] as SearchResult; - if (!selectedResult) { - this.isSelectedResultOutOfView$.next(false); - return; - } - if (selectedResult.data.geometry) { - const selectedOlFeature = featureToOl( - selectedResult.data, - this.map.projection - ); - const selectedOlFeatureExtent = computeOlFeaturesExtent( - [selectedOlFeature], - this.map.viewProjection - ); - this.isSelectedResultOutOfView$.next( - featuresAreOutOfView(this.map.getExtent(), selectedOlFeatureExtent) - ); - } - }); - } - - private buildResultEmphasis( - result: SearchResult, - trigger: 'selected' | 'focused' | 'shown' | undefined - ) { - if (trigger !== 'shown') { - this.clearFeatureEmphasis(trigger); - } - if (!result || !result.data.geometry) { - return; - } - const myOlFeature = featureToOl(result.data, this.map.projection); - const olGeometry = myOlFeature.getGeometry(); - if ( - featuresAreTooDeepInView( - this.map.viewController, - olGeometry.getExtent() as [number, number, number, number], - 0.0025 - ) - ) { - const extent = olGeometry.getExtent(); - const x = extent[0] + (extent[2] - extent[0]) / 2; - const y = extent[1] + (extent[3] - extent[1]) / 2; - const feature1 = new olFeature({ - name: `${trigger}AbstractResult'`, - geometry: new olPoint([x, y]) - }); - const abstractResult = featureFromOl(feature1, this.map.projection); - - let computedStyle; - let zIndexOffset = 0; - - switch (trigger) { - case 'focused': - computedStyle = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyleFocus, - result.style?.focus ? result.style.focus : {} - ) - ); - zIndexOffset = 2; - break; - case 'shown': - computedStyle = getCommonVectorStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyle, - result.style?.base ? result.style.base : {} - ) - ); - break; - case 'selected': - computedStyle = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: abstractResult }, - this.searchState.searchOverlayStyleSelection, - result.style?.selection ? result.style.selection : {} - ) - ); - zIndexOffset = 1; - break; - } - abstractResult.meta.style = computedStyle; - abstractResult.meta.style.setZIndex(2000 + zIndexOffset); - this.map.searchResultsOverlay.addFeature( - abstractResult, - FeatureMotion.None - ); - if (trigger === 'focused') { - this.abstractFocusedResult = abstractResult; - } - if (trigger === 'selected') { - this.abstractSelectedResult = abstractResult; - } - if (trigger === 'shown') { - this.shownResultsEmphasisGeometries.push(abstractResult); - } - } else { - this.clearFeatureEmphasis(trigger); - } - } - - private clearFeatureEmphasis(trigger: 'selected' | 'focused' | 'shown') { - if (trigger === 'focused' && this.abstractFocusedResult) { - this.map.searchResultsOverlay.removeFeature(this.abstractFocusedResult); - this.abstractFocusedResult = undefined; - } - if (trigger === 'selected' && this.abstractSelectedResult) { - this.map.searchResultsOverlay.removeFeature(this.abstractSelectedResult); - this.abstractSelectedResult = undefined; - } - if (trigger === 'shown') { - this.shownResultsEmphasisGeometries.map((shownResult) => - this.map.searchResultsOverlay.removeFeature(shownResult) - ); - this.shownResultsEmphasisGeometries = []; - } - } - - @HostListener('change') - ngOnDestroy() { - this.searchTerm$$.unsubscribe(); - if (this.isSelectedResultOutOfView$$) { - this.isSelectedResultOutOfView$$.unsubscribe(); - } - if (this.showResultsGeometries$$) { - this.showResultsGeometries$$.unsubscribe(); - } - if (this.getRoute$$) { - this.getRoute$$.unsubscribe(); - } - if (this.debouncedEmpty$$) { - this.debouncedEmpty$$.unsubscribe(); - } - } - - /** - * Try to add a feature to the map when it's being focused - * @internal - * @param result A search result that could be a feature - */ - onResultFocus(result: SearchResult) { - this.focusedResult$.next(result); - if (result.meta.dataType === FEATURE && result.data.geometry) { - result.data.meta.style = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: result.data as Feature | olFeature }, - this.searchState.searchOverlayStyleFocus, - result.style?.focus ? result.style.focus : {} - ) - ); - - const feature = - this.map.searchResultsOverlay.dataSource.ol.getFeatureById( - result.meta.id - ); - if (feature) { - feature.setStyle(result.data.meta.style); - return; - } - this.map.searchResultsOverlay.addFeature( - result.data as Feature, - FeatureMotion.None - ); - this.featureSelected.emit(); - } - } - - onResultUnfocus(result: SearchResult) { - this.focusedResult$.next(undefined); - if (result.meta.dataType !== FEATURE) { - return; - } - - if (this.store.state.get(result).selected) { - this.featureSelected.emit(); - const feature = - this.map.searchResultsOverlay.dataSource.ol.getFeatureById( - result.meta.id - ); - if (feature) { - const style = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature: result.data as Feature | olFeature }, - this.searchState.searchOverlayStyleFocus, - result.style?.focus ? result.style.focus : {} - ) - ); - feature.setStyle(style); - } - return; - } - this.map.searchResultsOverlay.removeFeature(result.data as Feature); - } - - /** - * Try to add a feature to the map when it's being selected - * @internal - * @param result A search result that could be a feature or some layer options - */ - onResultSelect(result: SearchResult) { - this.map.searchResultsOverlay.dataSource.ol.clear(); - this.tryAddFeatureToMap(result); - this.searchState.setSelectedResult(result); - } - - onSearch(event: { research: Research; results: SearchResult[] }) { - if ((this.mapQueryClick = true)) { - // to clear the mapQuery if a search is initialized - this.queryState.store.softClear(); - this.map.queryResultsOverlay.clear(); - this.mapQueryClick = false; - } - this.store.clear(); - this.searchInit = true; - this.legendPanelOpened = false; - const results = event.results; - this.searchStore.state.updateAll({ focused: false, selected: false }); - const newResults = this.searchStore.entities$.value - .filter((result: SearchResult) => result.source !== event.research.source) - .concat(results); - this.searchStore.updateMany(newResults); - - setTimeout(() => { - const igoList = this.elRef.nativeElement.querySelector('igo-list'); - let moreResults; - event.research.request.subscribe((source) => { - if (!source[0] || !source[0].source) { - moreResults = null; - } else if (source[0].source.getId() === 'icherche') { - moreResults = igoList.querySelector('.icherche .moreResults'); - } else if (source[0].source.getId() === 'ilayer') { - moreResults = igoList.querySelector('.ilayer .moreResults'); - } else if (source[0].source.getId() === 'nominatim') { - moreResults = igoList.querySelector('.nominatim .moreResults'); - } else { - moreResults = igoList.querySelector( - '.' + source[0].source.getId() + ' .moreResults' - ); - } - if ( - moreResults !== null && - !this.isScrolledIntoView(igoList, moreResults) - ) { - igoList.scrollTop = - moreResults.offsetTop + - moreResults.offsetHeight - - igoList.clientHeight; - } - }); - }, 250); - } - - computeElementRef() { - const items = document.getElementsByTagName('igo-search-results-item'); - const igoList = - this.elRef.nativeElement.getElementsByTagName('igo-list')[0]; - let selectedItem; - // eslint-disable-next-line - for (let i = 0; i < items.length; i++) { - if (items[i].className.includes('igo-list-item-selected')) { - selectedItem = items[i]; - } - } - return [igoList, selectedItem]; - } - - adjustTopPanel(elemSource, elem) { - if (!this.isScrolledIntoView(elemSource, elem)) { - elemSource.scrollTop = - elem.offsetTop + - elem.children[0].offsetHeight - - elemSource.clientHeight; - } - } - - zoomToFeatureExtent() { - if (this.feature.geometry) { - const localOlFeature = this.format.readFeature(this.feature, { - dataProjection: this.feature.projection, - featureProjection: this.map.projection - }); - moveToOlFeatures( - this.map.viewController, - [localOlFeature], - FeatureMotion.Zoom - ); - } - } - - /** - * Try to add a feature to the map overlay - * @param result A search result that could be a feature - */ - private tryAddFeatureToMap(result: SearchResult) { - if (result.meta.dataType !== FEATURE) { - return undefined; - } - const feature = (result as SearchResult).data; - - // Somethimes features have no geometry. It happens with some GetFeatureInfo - if (!feature.geometry) { - return; - } - - feature.meta.style = getCommonVectorSelectedStyle( - Object.assign( - {}, - { feature }, - this.searchState.searchOverlayStyleSelection, - result.style?.selection ? result.style.selection : {} - ) - ); - - this.map.searchResultsOverlay.addFeature(feature); - } - - isScrolledIntoView(elemSource, elem) { - const padding = 6; - const docViewTop = elemSource.scrollTop; - const docViewBottom = docViewTop + elemSource.clientHeight; - - const elemTop = elem.offsetTop; - const elemBottom = elemTop + elem.clientHeight + padding; - return elemBottom <= docViewBottom && elemTop >= docViewTop; - } - - getRoute() { - //this.toolState.toolbox.activateTool('directions'); - this.directionState.stopsStore.clearStops(); - setTimeout(() => { - let routingCoordLoaded = false; - if (this.getRoute$$) { - this.getRoute$$.unsubscribe(); - } - this.getRoute$$ = - this.directionState.stopsStore.storeInitialized$.subscribe( - (init: boolean) => { - if ( - this.directionState.stopsStore.storeInitialized$.value && - !routingCoordLoaded - ) { - routingCoordLoaded = true; - const stop = this.directionState.stopsStore - .all() - .find((e) => e.position === 1); - let coord; - if (this.feature.geometry) { - if (this.feature.geometry.type === 'Point') { - coord = [ - this.feature.geometry.coordinates[0], - this.feature.geometry.coordinates[1] - ]; - } else { - const point = pointOnFeature(this.feature.geometry); - coord = [ - point.geometry.coordinates[0], - point.geometry.coordinates[1] - ]; - } - } - stop.text = this.featureTitle; - stop.coordinates = coord; - this.directionState.stopsStore.update(stop); - if (this.map.geolocationController.position$.value) { - const currentPos = - this.map.geolocationController.position$.value; - const stop = this.directionState.stopsStore - .all() - .find((e) => e.position === 0); - const currentCoord = olProj.transform( - currentPos.position, - currentPos.projection, - 'EPSG:4326' - ); - const coord: [number, number] = roundCoordTo( - [currentCoord[0], currentCoord[1]], - 6 - ); - stop.text = coord.join(','); - stop.coordinates = coord; - this.directionState.stopsStore.update(stop); - } - } - } - ); - }, 250); - } -} diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.html b/src/app/pages/portal/panels/sidepanel/sidepanel.component.html deleted file mode 100644 index 40c70162..00000000 --- a/src/app/pages/portal/panels/sidepanel/sidepanel.component.html +++ /dev/null @@ -1,111 +0,0 @@ -
- -
- @if (legendPanelOpened) { -
-
-

- {{ 'legend.title' | translate }} -

- -
- - -
- } - - @if (!searchInit && (queryStore.empty$ | async) && !legendPanelOpened) { -
-
-
- {{ 'igo.integration.searchResultsTool.noResults' | translate }} -
-
- {{ 'igo.integration.searchResultsTool.doSearch' | translate }} -
-

-
-
- } - - @if (mapQueryClick) { -
- - -
- } - - @if (searchInit) { -
- - -
- } -
-
-
- -
-
diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss b/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss deleted file mode 100644 index dc4123c1..00000000 --- a/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss +++ /dev/null @@ -1,124 +0,0 @@ -@use 'variables'; -@import '../../portal.variables'; - -:host { - background-color: rgb(255, 255, 255); - - mat-sidenav { - @extend %box-shadowed-bottom-right; - - height: $app-sidenav-height; - width: $app-sidenav-width; - - overflow: visible; - } - - .app-sidenav-content { - padding: 0 16px 0 16px !important; - } - - .app-content, - igo-panel { - height: 100%; - } - - igo-search-results { - position: relative; - left: $portal-left !important; - width: 100%; - display: contents; - height: 100%; - } - - igo-search-results-tool { - display: block; - position: relative; - margin: 0 calc(4 * variables.$igo-margin); - top: calc(2 * variables.$igo-margin); - height: 100%; - } - - .sidepanel-button-container { - position: absolute; - top: 50%; - left: calc($app-sidenav-width + 1px); - z-index: 1; - padding: 0; - transition: left 300ms; - - button { - border-radius: 0; - &:hover { - background-color: #156bb2; - } - } - } - - .sidepanel-closed { - left: 0 !important; - position: absolute; - } - - ::ng-deep .icon svg { - fill: #ffffff; - } - - @keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - app-search-results-tool, - app-feature-info { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - padding: 16px 16px 0 16px; - } - - ::ng-deep app-search-results-tool > div > section > h4 > strong { - font-size: 21px !important; - } - - app-feature-info { - position: relative; - } - - ::ng-deep .mat-drawer-inner-container { - top: $search-bar-height; - position: relative; - height: calc(100% - $search-bar-height); - } - - .legend-items img { - width: 18px; - vertical-align: middle; - padding: 0 5px 0 0; - } - - // info-bulle arrows from panels - ::ng-deep .tooltip-chart::after { - content: '' !important; - position: absolute !important; - top: 18% !important; - right: 100% !important; - margin-top: -5px !important; - border-width: 10px !important; - border-style: solid !important; - border-color: transparent transparent transparent white !important; - transform: rotate(180deg); - } - - .legend-container { - padding: 16px; - } - - .title { - margin-top: 8px !important; - } -} diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts b/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts deleted file mode 100644 index 83b13c2e..00000000 --- a/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { AsyncPipe, NgClass } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - EventEmitter, - HostListener, - Input, - OnDestroy, - OnInit, - Output -} from '@angular/core'; -import { MatIconButton, MatMiniFabButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatSidenav } from '@angular/material/sidenav'; -import { MatTooltip } from '@angular/material/tooltip'; - -import { ActionStore, EntityStore } from '@igo2/common'; -import { ConfigService } from '@igo2/core/config'; -import { - FEATURE, - Feature, - FeatureMotion, - IgoMap, - Layer, - LayerLegendListComponent, - SearchResult -} from '@igo2/geo'; -import { QueryState, SearchState } from '@igo2/integration'; - -import { TranslateModule } from '@ngx-translate/core'; -import { BehaviorSubject } from 'rxjs'; - -import { FeatureInfoComponent } from '../feature/feature-info/feature-info.component'; -import { SearchResultsToolComponent } from '../search-results-tool/search-results-tool.component'; - -@Component({ - selector: 'app-sidepanel', - templateUrl: './sidepanel.component.html', - styleUrls: ['./sidepanel.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - MatSidenav, - MatIconButton, - MatTooltip, - MatIcon, - LayerLegendListComponent, - FeatureInfoComponent, - SearchResultsToolComponent, - NgClass, - MatMiniFabButton, - AsyncPipe, - TranslateModule - ] -}) -export class SidePanelComponent implements OnInit, OnDestroy { - title$: BehaviorSubject = new BehaviorSubject(undefined); - @Input() searchState: SearchState; - - @Input() - get map(): IgoMap { - return this._map; - } - set map(value: IgoMap) { - this._map = value; - } - private _map: IgoMap; - - @Input() - get opened(): boolean { - return this._opened; - } - set opened(value: boolean) { - if (value === this._opened) { - return; - } - - this._opened = value; - this.openedChange.emit(this._opened); - } - private _opened: boolean; - - @Output() openedChange = new EventEmitter(); - - @Input() mobile: boolean; // for tooltipPosition in featureDetails - - @Input() mapQueryClick: boolean; - - @Output() mapQuery = new EventEmitter(); - - get queryStore(): EntityStore { - return this.queryState.store; - } - - @Output() selectFeature = new EventEmitter(); - - @Input() - get feature(): Feature { - return this._feature; - } - set feature(value: Feature) { - this._feature = value; - this.cdRef.detectChanges(); - this.selectFeature.emit(); - } - private _feature: Feature; - - public selectedFeature: Feature; - public hasFeatureEmphasisOnSelection: Boolean = true; - - @Input() featureTitle: string; - - @Input() - get searchInit(): boolean { - return this._searchInit; - } - set searchInit(value: boolean) { - this._searchInit = value; - } - private _searchInit: boolean; - - public store = new ActionStore([]); - - public lonlat; - public mapProjection: string; - - public settingsChange$ = new BehaviorSubject(undefined); - - get searchStore(): EntityStore { - return this.searchState.store; - } - - @Input() - get layers(): Layer[] { - return this._layers; - } - set layers(value: Layer[]) { - this._layers = value; - } - private _layers: Layer[]; - - @Input() - get legendPanelOpened(): boolean { - return this._legendPanelOpened; - } - set legendPanelOpened(value: boolean) { - this._legendPanelOpened = value; - } - private _legendPanelOpened: boolean; - - @Input() panelOpenState: boolean; - - @Output() closeLegend = new EventEmitter(); - @Output() closeQuery = new EventEmitter(); - @Output() panelOpened = new EventEmitter(); - - public mapLayersShownInLegend: Layer[]; - - constructor( - private configService: ConfigService, - private queryState: QueryState, - private cdRef: ChangeDetectorRef - ) {} - - ngOnInit() { - this.queryStore.entities$ // clear the search when a mapQuery is initialised - .subscribe((entities) => { - if (entities.length > 0) { - this.mapQuery.emit(true); - this.legendPanelOpened = false; - this.panelOpened.emit(true); - this.clearSearch(); - } else { - this.closePanelOnCloseQuery(); - } - }); - } - - @HostListener('change') - ngOnDestroy() { - this.store.destroy(); - this.store.entities$.unsubscribe(); - this.legendPanelOpened = false; - this.clearSearch(); - this.clearQuery(); - this.map.propertyChange$.unsubscribe; - } - - isScrolledIntoView(elemSource, elem) { - const padding = 6; - const docViewTop = elemSource.scrollTop; - const docViewBottom = docViewTop + elemSource.clientHeight; - const elemTop = elem.offsetTop; - const elemBottom = elemTop + elem.clientHeight + padding; - return elemBottom <= docViewBottom && elemTop >= docViewTop; - } - - /** - * Try to add a feature to the map when it's being focused - * @internal - * @param result A search result that could be a feature - */ - onResultFocus(result: SearchResult) { - this.tryAddFeatureToMap(result); - this.selectedFeature = (result as SearchResult).data; - } - - /** - * Try to add a feature to the map overlay - * @param layer A search result that could be a feature - */ - private tryAddFeatureToMap(layer: SearchResult) { - if (layer.meta.dataType !== FEATURE) { - return undefined; - } - - // Somethimes features have no geometry. It happens with some GetFeatureInfo - if (layer.data.geometry === undefined) { - return; - } - - this.map.searchResultsOverlay.setFeatures( - [layer.data] as Feature[], - FeatureMotion.Default - ); - - this.hasFeatureEmphasisOnSelection = this.configService.getConfig( - 'hasFeatureEmphasisOnSelection' - ); - } - - /* - * Remove a feature to the map overlay - */ - removeFeatureFromMap() { - this.map.searchResultsOverlay.clear(); - } - - closePanelOnCloseQuery() { - this.closeQuery.emit(); - this.mapQuery.emit(false); - if (!this.searchInit && !this.legendPanelOpened) { - this.panelOpened.emit(false); - } - if (this.searchInit || this.legendPanelOpened) { - this.panelOpened.emit(true); - } - } - - clearSearch() { - this.map.searchResultsOverlay.clear(); - this.searchStore.clear(); - this.searchState.setSelectedResult(undefined); - this.searchState.deactivateCustomFilterTermStrategy(); - this.searchInit = false; - this.searchState.setSearchTerm(''); - } - - clearQuery(): void { - this.queryState.store.softClear(); - this.queryState.store.clear(); - this.mapQuery.emit(false); - this.removeFeatureFromMap(); - } - - closePanelLegend() { - /* this flushes the legend whenever a user closes the panel. if not, - the user has to click twice on the legend button to open the legend with the button - */ - this.legendPanelOpened = false; - this.closeLegend.emit(); - this.map.propertyChange$.unsubscribe; - } - - panelOpenedFromFeature(event) { - this.panelOpened.emit(event); - } - - mapQueryFromFeature(event) { - this.mapQuery.emit(event); - } -} diff --git a/src/app/pages/portal/portal.animation.ts b/src/app/pages/portal/portal.animation.ts index cddbe4e8..4ab5bc53 100644 --- a/src/app/pages/portal/portal.animation.ts +++ b/src/app/pages/portal/portal.animation.ts @@ -27,34 +27,6 @@ export function controlSlideX(): AnimationTriggerMetadata { ]); } -export function controlSlideY(): AnimationTriggerMetadata { - return trigger('controlStateY', [ - state('close', style({})), - state( - 'firstRowFromBottom', - style({ - bottom: '2px', - 'margin-left': '0px' - }) - ), - state( - 'firstRowFromBottom-expanded', - style({ - bottom: '285px', - 'margin-left': '-55px' - }) - ), - state( - 'firstRowFromBottom-expanded-maximized', - style({ - bottom: '500px', // workspace full size - 'margin-left': '-55px' - }) - ), - transition('* => *', animate('200ms')) - ]); -} - export function controlsAnimations(): AnimationTriggerMetadata[] { return [ trigger('controlsOffsetY', [ @@ -65,46 +37,10 @@ export function controlsAnimations(): AnimationTriggerMetadata[] { bottom: '5px' }) ), - state( - 'firstRowFromBottom-expanded', - style({ - bottom: '5px' - }) - ), - state( - 'firstRowFromBottom-expanded-maximized', - style({ - bottom: '500px' - }) - ), state( 'secondRowFromBottom', style({ - bottom: '47px' - }) - ), - state( - 'thirdRowFromBottom', - style({ - bottom: '104px' - }) - ), - state( - '', - style({ - bottom: 'calc(285px)' - }) - ), - state( - 'secondRowFromBottom-expanded', - style({ - bottom: 'calc(285px + 52px)' - }) - ), - state( - 'thirdRowFromBottom-expanded', - style({ - bottom: 'calc(285px + 104px)' + bottom: '35px' }) ), transition('* => *', animate('200ms')) diff --git a/src/app/pages/portal/portal.component.html b/src/app/pages/portal/portal.component.html index 5d3a295b..d3884fda 100644 --- a/src/app/pages/portal/portal.component.html +++ b/src/app/pages/portal/portal.component.html @@ -1,48 +1,15 @@ + + @if (!mobile) {
@if (showSearchBar) { - - + } @if (mobile) {
} - -
} @@ -66,33 +33,20 @@ @if (mobile) {
} - - - + + +
@if (hasGeolocateButton) { + @if ( + (panelsHandlerState.map.layers$ | async | filterableDataSource: 'ogc') + .length > 0 + ) { + + } @if (appConfig.hasLegendButton) { }
- - @if (mobile) { - - - } @if (hasFooter && !mobile) { @@ -141,7 +87,8 @@
+ + + + + diff --git a/src/app/pages/portal/portal.component.scss b/src/app/pages/portal/portal.component.scss index ac3d7f4b..64e94e16 100644 --- a/src/app/pages/portal/portal.component.scss +++ b/src/app/pages/portal/portal.component.scss @@ -1,3 +1,6 @@ +@use '@angular/material' as mat; +@use '../../../style/breakpoints'; + @import './portal.variables'; /*** Main ***/ @@ -9,40 +12,6 @@ mat-icon.disabled { color: rgba(0, 0, 0, 0.38); } - - /*** Layer toggle ***/ - .layer-toggle { - display: flex; - flex-flow: column; - position: absolute; - z-index: 1002 !important; - background-color: rgba(255, 255, 255, 0.95); - } - - .layer-toggle-desktop { - top: calc(4 * $igo-margin); - right: 8px; - } - - .layer-toggle-mobile { - top: calc(2 * $igo-margin); - right: 50%; - transform: translate(50%); - ::ng-deep .layer-toggles-title { - font-size: 14px; - } - ::ng-deep .layer-toggle-title { - font-size: 14px; - } - ::ng-deep .button-group { - transform: scale(0.9); - } - - ::ng-deep .mat-expansion-panel-body { - padding: 0 8px 8px; - } - } - /*** Sidenav ***/ mat-sidenav-container { height: 100%; @@ -57,12 +26,6 @@ z-index: auto; } } - - /*** Expansion Panel ***/ - .spacer { - flex: 1 1 auto; - } - /*** Search bar ***/ igo-search-bar { background-color: $app-background-color; @@ -72,14 +35,12 @@ z-index: 4; height: $igo-icon-size; margin: 0 $igo-margin; - } - igo-search-bar { width: $search-bar-width; @include mobile { width: $search-bar-width-mobile; - max-width: 360px; + top: 15%; } } @@ -100,53 +61,12 @@ // Mobile pushed elements - ::ng-deep .baselayers-pushed { - left: calc($app-sidenav-width + 4px) !important; - animation-duration: 3s; - visibility: visible; - opacity: 1; - opacity: 0; - animation: fadeIn 0.1s; - animation-delay: 0s; - animation-fill-mode: forwards; - } - - .igo-baselayers-switcher-mobile { - left: 4px; - bottom: 36px; - z-index: 1; - } - - .footer-mobile-offset { - bottom: 62px; - } - - .igo-baselayers-switcher { - z-index: 1; - } - - ::ng-deep igo-search-bar { - height: 100%; - } - igo-search-bar svg { color: white; height: 40px; width: 40px; } - ::ng-deep .igo-search-bar-container > mat-form-field { - padding: 0 !important; - } - - /** search bar mobile **/ - app-bottompanel { - position: fixed; - display: block; - bottom: 0; - z-index: 9999; - } - /*** Map browser ***/ igo-map-browser { width: 100%; @@ -188,6 +108,54 @@ } } + .map-actions { + position: absolute; + display: flex; + transition: + bottom 250ms, + left 250ms; + + .spinner-spacer { + height: 40px; + } + + &.--vertical { + flex-direction: column; + + > :not(:first-child) { + margin-top: 8px; + } + } + + &.--horizontal { + > :not(:first-child) { + margin-right: 8px; + } + } + + &.--bottom { + bottom: 4px; + } + + &.--right { + right: 4px; + align-items: flex-end; // todo ajouter a igo + } + + &.--top { + top: 4px; + } + + &.--horizontal.--bottom.--right { + right: calc(40px + 4px * 2); + } + + &.--left { + left: 4px; + align-items: flex-start; // todo ajouter a igo + } + } + igo-map-browser ::ng-deep .ol-overlaycontainer-stopevent { position: absolute; width: 100%; @@ -206,44 +174,12 @@ } } - .map-buttons { - display: flex; - flex-direction: column; - z-index: 10; - max-height: 50%; - position: absolute; - bottom: 6px; - right: 16px; - } - - .map-buttons-mobile { - display: flex; - flex-direction: column; - z-index: 10; - position: absolute; - bottom: 36px; - max-height: 50%; - right: 4px; - } - - .igo-zoom-button-container, - button, - app-legend-button, - igo-rotation-button, - igo-zoom-button, - igo-offline-button, - igo-geolocate-button, - igo-home-extent-button { - margin-top: 4px !important; - position: unset !important; - bottom: unset !important; - margin-left: auto; - align-self: flex-end; - margin-top: auto; - } - igo-zoom-button { - @include tablet { + ::ng-deep button { + @include mat.elevation(2); + } + + @include breakpoints.media-breakpoint-down('mobile') { display: none; } } diff --git a/src/app/pages/portal/portal.component.ts b/src/app/pages/portal/portal.component.ts index 5551092a..213d9991 100644 --- a/src/app/pages/portal/portal.component.ts +++ b/src/app/pages/portal/portal.component.ts @@ -3,15 +3,9 @@ import { BreakpointState, Breakpoints } from '@angular/cdk/layout'; -import { AsyncPipe, NgClass } from '@angular/common'; +import { AsyncPipe, NgClass, NgTemplateOutlet } from '@angular/common'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { - AfterContentInit, - Component, - OnDestroy, - OnInit, - ViewChild -} from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSidenavContainer, @@ -20,7 +14,7 @@ import { import { MatTooltip } from '@angular/material/tooltip'; import { ActivatedRoute, Params } from '@angular/router'; -import { EntityRecord, EntityStore } from '@igo2/common'; +import { EntityRecord } from '@igo2/common'; import { DetailedContext, LayerContextDirective, @@ -29,7 +23,7 @@ import { import { AnalyticsService } from '@igo2/core/analytics'; import { ConfigService } from '@igo2/core/config'; import { LanguageService } from '@igo2/core/language'; -import { Media, MediaService } from '@igo2/core/media'; +import { MediaService } from '@igo2/core/media'; import { MessageService } from '@igo2/core/message'; import { BaseLayersSwitcherComponent, @@ -38,11 +32,11 @@ import { DropGeoFileDirective, FEATURE, Feature, + FilterableDataSourcePipe, GeolocateButtonComponent, HoverFeatureDirective, IgoMap, ImportService, - Layer, LayerService, MapBrowserComponent, MapOfflineDirective, @@ -87,28 +81,25 @@ import { import { EnvironmentOptions } from 'src/environments/environnement.interface'; import { FooterComponent } from '../footer/footer.component'; +import { FilterButtonComponent } from './filter-button/filter-button.component'; import { LegendButtonComponent } from './legend-button/legend-button.component'; import { MapOverlayComponent } from './map-overlay/map-overlay.component'; -import { BottomPanelComponent } from './panels/bottompanel/bottompanel.component'; -import { SidePanelComponent } from './panels/sidepanel/sidepanel.component'; -import { - controlSlideX, - controlSlideY, - controlsAnimations -} from './portal.animation'; +import { PanelsHandlerComponent } from './panels/panels-handler/panels-handler.component'; +import { ShownComponent } from './panels/panels-handler/panels-handler.enum'; +import { PanelsHandlerState } from './panels/panels-handler/panels-handler.state'; +import { controlSlideX, controlsAnimations } from './portal.animation'; @Component({ selector: 'app-portal', templateUrl: './portal.component.html', styleUrls: ['./portal.component.scss'], - animations: [controlsAnimations(), controlSlideX(), controlSlideY()], + animations: [controlsAnimations(), controlSlideX()], standalone: true, imports: [ MatSidenavContainer, MatSidenavContent, SearchBarComponent, MatTooltip, - SidePanelComponent, MapBrowserComponent, MapOfflineDirective, MapContextDirective, @@ -122,25 +113,30 @@ import { ZoomButtonComponent, RotationButtonComponent, LegendButtonComponent, - BottomPanelComponent, + FilterButtonComponent, FooterComponent, MapOverlayComponent, AsyncPipe, - TranslateModule + TranslateModule, + PanelsHandlerComponent, + NgTemplateOutlet, + FilterableDataSourcePipe ] }) -export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { +export class PortalComponent implements OnInit, OnDestroy { + readonly breakpoint$: Observable; + public Breakpoints = Breakpoints; + + public shownComponents = ShownComponent; public appConfig: EnvironmentOptions; public hasFooter: boolean; public hasGeolocateButton: boolean; public showSearchBar: boolean; - public legendPanelOpened = false; + public legendDialogOpened = false; - public searchBarTerm = ''; public termDefinedInUrl = false; - public termSplitter = '|'; public termDefinedInUrlTriggered = false; private addedLayers$$: Subscription[] = []; private layers$$: Subscription; @@ -150,44 +146,20 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { private searchTerm$$: Subscription; private routeParams: Params; + // renommer mobileMode plutot que mobile? public mobile: boolean; @ViewChild('searchbar') searchBar: SearchBarComponent; public dialogOpened: MatDialogRef; - get map(): IgoMap { - return this.mapState.map; - } - - isMobile(): boolean { - return this.mediaService.getMedia() === Media.Mobile; - } - public mobileBreakPoint: string = '(min-width: 768px)'; - public Breakpoints = Breakpoints; - public currentBreakpoint: string = ''; - - get searchStore(): EntityStore { - return this.searchState.store; - } - - get searchResultsGeometryEnabled(): boolean { - return this.searchState.searchResultsGeometryEnabled$.value; - } - - get queryStore(): EntityStore { - //FeatureInfo - return this.queryState.store; - } - public panelOpenState = false; - public mapQueryClick = false; - public searchInit = false; - - public mapLayersShownInLegend: Layer[]; + public currentBreakpoint: string = ''; public legendButtonTooltip: unknown; - readonly breakpoint$: Observable; + get map(): IgoMap { + return this.mapState.map; + } constructor( private route: ActivatedRoute, @@ -198,7 +170,7 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { private contextState: ContextState, private mapState: MapState, public searchState: SearchState, - private queryState: QueryState, + public queryState: QueryState, private searchSourceService: SearchSourceService, private configService: ConfigService, private importService: ImportService, @@ -209,7 +181,8 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { public dialog: MatDialog, public queryService: QueryService, private breakpointObserver: BreakpointObserver, - private analyticsService: AnalyticsService + private analyticsService: AnalyticsService, + public panelsHandlerState: PanelsHandlerState ) { this.handleAppConfigs(); this.dialogOpened = this.dialog.getDialogById( @@ -223,18 +196,17 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { } ngOnInit() { + this.initPanelsHandlerState(); this.queryService.defaultFeatureCount = 1; window['IGO'] = this; this.map.ol.once('rendercomplete', () => { this.readQueryParams(); - if (this.appConfig.geolocate?.activateDefault) { + if (this.appConfig.geolocate?.activateDefault !== undefined) { this.map.geolocationController.tracking = this.appConfig.geolocate?.activateDefault; } }); - this.searchState.searchTermSplitter$.next(this.termSplitter); - this.route.queryParams.subscribe((params) => { this.readLanguageParam(params); }); @@ -243,12 +215,6 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { (context: DetailedContext) => this.onChangeContext(context) ); - this.searchState.selectedResult$.subscribe((result) => { - if (result && this.isMobile()) { - this.closePanels(); - } - }); - this.searchTerm$$ = this.searchState.searchTerm$ .pipe(skip(1)) .subscribe((searchTerm: string) => { @@ -262,92 +228,46 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { this.queryService.defaultFeatureCount = 1; - this.queryStore.entities$.subscribe((entities) => { - if (entities.length > 0) { - this.openPanelonQuery(); - } - }); - this.breakpoint$.subscribe(() => this.breakpointChanged()); } private handleAppConfigs() { this.appConfig = this.configService.getConfigs(); - this.hasGeolocateButton = this.configService.getConfig( 'geolocate.button.visible', true ); - this.showSearchBar = this.configService.getConfig( 'searchBar.showSearchBar', true ); - this.hasFooter = this.configService.getConfig('hasFooter', true); - this.mobileBreakPoint = this.configService.getConfig( 'mobileBreakPoint', - "'(min-width: 768px)'" + '(min-width: 768px)' ); } - ngAfterContentInit(): void { - this.map.viewController.setInitialState(); + private initPanelsHandlerState() { + this.panelsHandlerState.map = this.map; + this.panelsHandlerState.queryState = this.queryState; + this.panelsHandlerState.searchState = this.searchState; } - toggleLegend() { - if (this.appConfig.legendInPanel || this.mobile) { - if (!this.legendPanelOpened) { - this.legendButtonTooltip = - this.languageService.translate.instant('legend.close'); - this.openPanelLegend(); - if (this.searchInit) { - this.clearSearch(); - this.openPanels(); - } - if (this.mapQueryClick) { - this.onClearQuery(); - this.mapQueryClick = false; - this.openPanels(); - } - } else { - this.legendButtonTooltip = - this.languageService.translate.instant('legend.open'); - this.closePanelLegend(); - } + togglePanelComponent(component: ShownComponent) { + const currentComponent = this.panelsHandlerState.shownComponent$.getValue(); + const opened = this.panelsHandlerState.opened$.getValue(); + + if (component !== currentComponent && opened) { + this.panelsHandlerState.setShownComponent(component); } else { - if (!this.legendDialogOpened) { - this.legendDialogOpened = true; - } + this.panelsHandlerState.setShownComponent(component); + this.panelsHandlerState.togglePanels(); } } - panelOpened(event) { - this.panelOpenState = event; - } - - mapQuery(event) { - this.mapQueryClick = event; - } - - closePanelLegend() { - this.legendPanelOpened = false; - this.closePanels(); - this.map.propertyChange$.unsubscribe; - } - - openPanelLegend() { - this.legendPanelOpened = true; - this.openPanels(); - this.map.propertyChange$.subscribe(() => { - this.mapLayersShownInLegend = this.map.layers.filter( - (layer) => layer.showInLayerList !== false - ); - }); - } - public breakpointChanged() { + // todo service distinct? renommer mobileMode plutot que mobile? if (this.breakpointObserver.isMatched('(min-width: 768px)')) { this.currentBreakpoint = this.mobileBreakPoint; this.mobile = false; @@ -362,6 +282,13 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { this.searchTerm$$.unsubscribe(); } + public onClearSearch() { + this.map.searchResultsOverlay.clear(); + this.searchState.store.clear(); + this.searchState.setSelectedResult(undefined); + this.searchState.deactivateCustomFilterTermStrategy(); + } + private getQuerySearchSource(): SearchSource { return this.searchSourceService .getSources() @@ -372,58 +299,29 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { } onMapQuery(event: { features: Feature[]; event: MapBrowserEvent }) { - if (this.appConfig.queryOnlyOne) { - event.features = [event.features[0]]; - this.map.queryResultsOverlay.clear(); // to avoid double-selection in the map - } const baseQuerySearchSource = this.getQuerySearchSource(); const querySearchSourceArray: QuerySearchSource[] = []; - if (event.features.length) { - if (this.searchInit) { - this.clearSearch(); - } - this.clearSearchbarterm(''); - if (this.mapQueryClick) { - this.onClearQuery(); - } - this.openPanelonQuery(); - const results = event.features.map((feature: Feature) => { - let querySearchSource = querySearchSourceArray.find( - (s) => s.title === feature.meta.sourceTitle - ); - if (querySearchSource) { - this.onClearQuery(); - this.openPanelonQuery(); - this.mapQueryClick = true; - } - if (!querySearchSource) { - querySearchSource = new QuerySearchSource({ - title: feature.meta.sourceTitle - }); - querySearchSourceArray.push(querySearchSource); - } - return featureToSearchResult(feature, querySearchSource); - }); - const filteredResults = results.filter((x) => x !== undefined); - const research = { - request: of(filteredResults), - reverse: false, - source: baseQuerySearchSource - }; - research.request.subscribe((queryResults: SearchResult[]) => { - this.queryStore.load(queryResults); - }); - } else { - this.mapQueryClick = false; - if (!this.searchInit && !this.legendPanelOpened && !this.mobile) { - // in desktop keep legend opened if user clicks on the map - this.panelOpenState = false; - } - if (!this.searchInit && this.mobile) { - // mobile mode, close legend when user click on the map - this.panelOpenState = false; + const results = event.features.map((feature: Feature) => { + let querySearchSource = querySearchSourceArray.find( + (s) => s.title === feature.meta.sourceTitle + ); + if (!querySearchSource) { + querySearchSource = new QuerySearchSource({ + title: feature.meta.sourceTitle + }); + querySearchSourceArray.push(querySearchSource); } - } + return featureToSearchResult(feature, querySearchSource); + }); + const filteredResults = results.filter((x) => x !== undefined); + const research = { + request: of(filteredResults), + reverse: false, + source: baseQuerySearchSource + }; + research.request.subscribe((queryResults: SearchResult[]) => { + this.queryState.store.load(queryResults); + }); } /** @@ -435,40 +333,19 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { } onSearchTermChange(term?: string) { - if (this.mobile) { - this.panelOpenState = true; - } if (this.routeParams?.search && term !== this.routeParams.search) { this.searchState.deactivateCustomFilterTermStrategy(); } - + this.searchState.searchTerm$.next(term); this.searchState.setSearchTerm(term); const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); if (termWithoutHashtag.length < 2) { - if (this.mobile) { - this.panelOpenState = true; - } - this.clearSearch(); + this.onClearSearch(); return; } - this.onBeforeSearch(); - } - - clearSearchbarterm(event) { - if (!this.mobile) { - this.searchBar.setTerm(''); - } } onSearch(event: { research: Research; results: SearchResult[] }) { - this.searchInit = true; - this.legendPanelOpened = false; - this.panelOpenState = true; - if (this.mapQueryClick) { - this.onClearQuery(); - this.mapQueryClick = false; - this.panelOpenState = true; - } const results = event.results; const isReverseSearch = !sourceCanSearch(event.research.source); @@ -484,24 +361,14 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { .filter(sourceCanSearch); } - const newResults = this.searchStore.entities$.value + const newResults = this.searchState.store.entities$.value .filter( (result: SearchResult) => result.source !== event.research.source && enabledSources.includes(result.source) ) .concat(results); - this.searchStore.updateMany(newResults); - } - - private closePanels() { - if (!this.mapQueryClick && !this.searchInit && !this.legendPanelOpened) { - this.panelOpenState = false; - } - } - - private openPanels() { - this.panelOpenState = true; + this.searchState.store.updateMany(newResults); } private onChangeContext(context: DetailedContext) { @@ -522,38 +389,16 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { this.contextLoaded = true; } - private onBeforeSearch() { - this.openPanels(); - } - clearSearch() { this.map.searchResultsOverlay.clear(); - this.searchStore.clear(); + this.searchState.store.clear(); this.searchState.setSelectedResult(undefined); this.searchState.deactivateCustomFilterTermStrategy(); - this.searchInit = false; - this.searchBarTerm = ''; // the searchbarterm doesn't clear up this.searchState.setSearchTerm(''); } - closePanelOnCloseQuery() { - this.mapQueryClick = false; - if (this.searchInit || this.legendPanelOpened) { - this.openPanels(); // to prevent the panel to close when click searchbar after query - } - } - - openPanelonQuery() { - this.mapQueryClick = true; - this.openPanels; - this.legendPanelOpened = false; - this.clearSearch(); - } - - onClearQuery() { - this.queryState.store.clear(); // clears the info panel - this.queryState.store.softClear(); // clears the info panel - this.map.queryResultsOverlay.clear(); // to avoid double-selection in the map + getControlsOffsetY() { + return this.mobile ? 'secondRowFromBottom' : 'firstRowFromBottom'; } private readQueryParams() { @@ -575,9 +420,9 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { if (this.routeParams['zoomExtent']) { const extentParams = this.routeParams['zoomExtent'].split(','); const olExtent = olProj.transformExtent( - extentParams, + extentParams.map((str) => Number(str.trim())), 'EPSG:4326', - this.map.projection + this.map.projectionCode ); this.map.viewController.zoomToExtent( olExtent as [number, number, number, number] @@ -598,7 +443,7 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { private readFocusFirst() { if (this.routeParams['sf'] === '1' && this.termDefinedInUrl) { - const entities$$ = this.searchStore.stateView + const entities$$ = this.searchState.store.stateView .all$() .pipe( skipWhile((entities) => entities.length === 0), @@ -626,7 +471,7 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { !this.routeParams['zoom'] && this.routeParams['sf'] !== '1' ) { - const entities$$ = this.searchStore.stateView + const entities$$ = this.searchState.store.stateView .all$() .pipe( skipWhile((entities) => entities.length === 0), @@ -650,7 +495,7 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { this.map.viewController.zoomToExtent(totalExtent); }); } - this.searchBarTerm = this.routeParams['search']; + this.searchState.searchTerm$.next(this.routeParams['search']); } if (this.routeParams['searchGeom'] === '1') { this.searchState.searchResultsGeometryEnabled$.next(true); @@ -728,6 +573,9 @@ export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { .replace('VERSION=' + version, '') .replace('version=' + version, ''); } + if (url.endsWith('?')) { + url = url.substring(0, url.length - 1); + } const currentLayersByService = this.extractLayersByService( layersByService[cnt] diff --git a/src/app/pages/portal/portal.module.ts b/src/app/pages/portal/portal.module.ts index 1d0d7a5f..11316d6c 100644 --- a/src/app/pages/portal/portal.module.ts +++ b/src/app/pages/portal/portal.module.ts @@ -34,14 +34,12 @@ import { } from '@igo2/geo'; import { IgoAppSearchBarModule, IgoIntegrationModule } from '@igo2/integration'; -import { AppPanelsModule } from './panels/panels.module'; import { PortalComponent } from './portal.component'; @NgModule({ imports: [ IgoLayerModule, IgoAppSearchBarModule, - AppPanelsModule, CommonModule, MatTooltipModule, MatButtonModule, diff --git a/src/contexts/_default.json b/src/contexts/_default.json index 7051847c..f2282c3a 100644 --- a/src/contexts/_default.json +++ b/src/contexts/_default.json @@ -3,22 +3,107 @@ "base": "_base", "layers": [ { - "id": "mtq", - "title": "Établissements MTQ", - "showInLayerList": true, - "visible": true, - "baseLayer": false, + "title": "Photo radar", "sourceOptions": { "type": "wms", "url": "https://ws.mapserver.transports.gouv.qc.ca/swtq", + "queryable": true, "params": { - "layers": "etablissement_mtq", - "version": "1.3.0" + "layers": "radars_photos" }, - "queryable": true, - "queryLayerFeatures": true, - "queryTitle": "Établissements MTQ" + "sourceFields": [ + { + "order": 1, + "alias": "Type d'appareil", + "name": "typeAppareil" + }, + { + "alias": "Début", + "name": "dateDebutService" + }, + { + "alias": "Fin", + "name": "dateFinService" + }, + { + "alias": "Description", + "name": "description" + }, + { + "alias": "Arrondissement", + "name": "arrondissement" + }, + { + "alias": "Municipalité", + "name": "municipalite" + }, + { + "alias": "Région", + "name": "region" + }, + { + "alias": "Fiche technique", + "name": "urlImage" + } + ], + "ogcFilters": { + "enabled": true, + "editable": false, + "checkboxes": { + "order": 1, + "groups": [ + { + "title": "Group 1 Title", + "name": "1", + "ids": [ + "id1" + ] + } + ], + "bundles": [ + { + "id": "id1", + "logical": "Or", + "title": "Type de radar photo", + "selectors": [ + { + "title": "Radar photo fixe", + "filters": { + "operator": "PropertyIsEqualTo", + "propertyName": "typeAppareil", + "expression": "Radar photo fixe" + } + }, + { + "title": "Radar photo mobile", + "filters": { + "operator": "PropertyIsEqualTo", + "propertyName": "typeAppareil", + "expression": "Radar photo mobile" + } + }, + { + "title": "Radar photo fixe + feu rouge", + "filters": { + "operator": "PropertyIsEqualTo", + "propertyName": "typeAppareil", + "expression": "Radar photo fixe et surveillance au feu rouge" + } + }, + { + "title": "Radar feu rouge", + "filters": { + "operator": "PropertyIsEqualTo", + "propertyName": "typeAppareil", + "expression": "Appareil de surveillance au feu rouge" + } + } + ] + } + ] + } + } } } ] -} +} \ No newline at end of file diff --git a/src/locale/en.json b/src/locale/en.json index 120a530d..bf65a917 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -39,6 +39,12 @@ "close": "Close legend", "open": "Open legend" }, + "filter": { + "title": "Filters", + "button": "Filters", + "close": "Close filter panel", + "open": "Open filter panel" + }, "pwa": { "new-version-title": "New version available. ", "new-version": "Do you want to reload the app?", @@ -62,4 +68,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/locale/fr.json b/src/locale/fr.json index 7920317d..989e31d9 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -29,6 +29,12 @@ "close": "Fermer la légende", "open": "Afficher la légende" }, + "filter": { + "title": "Filtre", + "button": "Filtre", + "close": "Fermer le(s) filtre(s)", + "open": "Afficher le(s) filtre(s)" + }, "feature": { "title": "Objet", "close": "Fermer le panneau", @@ -62,4 +68,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/style/breakpoints.scss b/src/style/breakpoints.scss new file mode 100644 index 00000000..d99ff452 --- /dev/null +++ b/src/style/breakpoints.scss @@ -0,0 +1,57 @@ +@use 'sass:map'; + +// There is no standard for breakpoints for the Quebec.ca theme +// This is the Material breakpoint design by devices +$device-breakpoints: ( + mobile: ( + max: 599px + ), + tablet: ( + min: 600px, + max: 1239px + ), + laptop: ( + min: 1240px, + max: 1439px + ), + desktop: ( + min: 1440px + ) +); + +// Source: https://github.com/twbs/bootstrap/blob/main/scss/mixins/_breakpoints.scss +@function breakpoint-min($name, $breakpoints: $device-breakpoints) { + $min: map.get($breakpoints, $name, min); + @return if($min != 0, $min, null); +} + +@function breakpoint-max($name, $breakpoints: $device-breakpoints) { + $max: map.get($breakpoints, $name, max); + @return if($max and $max > 0, $max, null); +} + +// Media of at least the minimum breakpoint width. No query for the smallest breakpoint. +// Makes the @content apply to the given breakpoint and wider. +@mixin media-breakpoint-up($name, $breakpoints: $device-breakpoints) { + $min: breakpoint-min($name, $breakpoints); + @if $min { + @media (min-width: $min) { + @content; + } + } @else { + @content; + } +} + +// Media of at most the maximum breakpoint width. No query for the largest breakpoint. +// Makes the @content apply to the given breakpoint and narrower. +@mixin media-breakpoint-down($name, $breakpoints: $device-breakpoints) { + $max: breakpoint-max($name, $breakpoints); + @if $max { + @media (max-width: $max) { + @content; + } + } @else { + @content; + } +}