diff --git a/angular.json b/angular.json index 947b97b0..1f55a1ce 100644 --- a/angular.json +++ b/angular.json @@ -30,8 +30,7 @@ "src/assets", "src/config", "src/contexts", - "src/locale", - "src/particular/locale", + "src/locale" { "glob": "**/*", "input": "./node_modules/@igo2/common/assets/", @@ -253,6 +252,7 @@ "cache": { "enabled": false, "environment": "all" - } + }, + "analytics": false } } diff --git a/package.json b/package.json index 752dddd4..3a2605d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "igo2-quebec", - "version": "1.0.2-beta", + "version": "1.1.0-beta", "description": "Infrastructure géomatique ouverte - Québec.ca", "author": "Communauté IGO", "repository": { diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 7cfe66b3..9e4cfe68 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -93,12 +93,10 @@ $footer-height: 29px; } -//* Spinner *// - igo-spinner { position: absolute; - top: calc(#{$header-height-mobile} + 4px) !important; - right: 4px; + top: 50% !important; + right: 50% ; z-index: 100; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 562568ee..eb2322dd 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -23,7 +23,15 @@ import { provideILayerSearchSource, provideOptionsApi, provideStyleListOptions, - provideWorkspaceSearchSource + provideWorkspaceSearchSource, + SearchService, + provideOsrmDirectionsSource, + provideNominatimSearchSource, + CoordinatesSearchResultFormatter, + provideDefaultCoordinatesSearchResultFormatter, + provideDefaultIChercheSearchResultFormatter, + provideSearchSourceService, + IChercheSearchSource } from '@igo2/geo'; @@ -86,13 +94,24 @@ export const defaultTooltipOptions: MatTooltipDefaultOptions = { // path: './assets/config/config.json' path: './config/config.json' }), - RouteService, + provideCoordinatesReverseSearchSource(), provideIChercheSearchSource(), - provideWorkspaceSearchSource(), + provideNominatimSearchSource(), provideIChercheReverseSearchSource(), - provideCoordinatesReverseSearchSource(), provideILayerSearchSource(), + provideOsrmDirectionsSource(), provideOptionsApi(), + CoordinatesSearchResultFormatter, + provideDefaultCoordinatesSearchResultFormatter(), + provideDefaultIChercheSearchResultFormatter(), + provideSearchSourceService(), + SearchService, + IChercheSearchSource, + provideStyleListOptions({ + path: './assets/list-style.json' + }), + RouteService, + provideWorkspaceSearchSource(), { provide: APP_INITIALIZER, useFactory: appInitializerFactory, diff --git a/src/app/pages/header/header.component.scss b/src/app/pages/header/header.component.scss index 8b913615..eebcc62b 100644 --- a/src/app/pages/header/header.component.scss +++ b/src/app/pages/header/header.component.scss @@ -67,7 +67,7 @@ display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; height: 100%; -width: 98%; +width: calc(100% - 32px); align-content: center; margin: auto; } 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 99e2fbab..3d5d2b45 100644 --- a/src/app/pages/portal/legend-button/legend-button.component.html +++ b/src/app/pages/portal/legend-button/legend-button.component.html @@ -1 +1,9 @@ - + diff --git a/src/app/pages/portal/legend-button/legend-button.component.scss b/src/app/pages/portal/legend-button/legend-button.component.scss index 20852e8b..2f15950d 100644 --- a/src/app/pages/portal/legend-button/legend-button.component.scss +++ b/src/app/pages/portal/legend-button/legend-button.component.scss @@ -1,127 +1,15 @@ -@import '../portal.variables.scss'; - - // button - -app-legend-button, ::ng-deep .mat-raised-button { - font-size: 0.9rem !important; - font-weight: 500 !important; - margin: 0; - min-width: 8px; - border: 0px; - border-radius: 0 !important; -} - -::ng-deep .qcca-theme app-legend-button button, ::ng-deep .qcca-theme .mat-raised-button { - color: #095797 !important; - border-color: #095797 !important; -} - -// DIALOG - -// FLEX - -::ng-deep #legend-button-dialog-container { - width: fit-content !important; - z-index: 10; - position: fixed; - right: 0.5%; - max-width: 99% !important; - padding: 16px !important; - max-height: calc(100% - #{$header-height-mobile} - #{$footer-height} - 100px) !important; - overflow: hidden; - display: flex; - flex-direction: column; - height: 100%; - - igo-layer-legend-list { - max-height: inherit; - .layer-legend-list-container { - overflow-y: auto; - height: inherit; - max-height: inherit; - } - } - - igo-layer-legend-item { - flex-wrap: wrap; - width: min-content; - display: flex; - flex-direction: column; - margin-bottom: 16px; - } - - igo-layer-legend-item:last-child { - margin-bottom: 0; - } - - .igo-layer-legend-container { - padding-left: 0 !important; - width: max-content !important; - } - - @media (min-width: 768px){ - max-height: calc(100% - #{$header-height} - #{$footer-height} - 180px) !important; //TODO : Inject the dialog in the portal then add config hasHeader, hasMenu and hasFooter - } - -} - -::ng-deep app-legend-button-dialog { - - max-height: calc(100% - 24px) !important; - height: inherit; - - .mat-list-base .mat-list-item .mat-list-item-content { - padding: 0 !important; - } - - // TITLES - - .mat-dialog-title { - margin: 0; - display: block; - } - - .mat-dialog-title:after { - content: ""; - display: block; - width: 2.8rem; - border-bottom: 4px solid #f09686; - margin-bottom: 0.8rem; - } - - .mat-list-item:after { - content: ""; - display: block; - width: 1.8rem; - border-bottom: 3px solid #DAE6F0; - margin-bottom: 0.4rem; - padding-top: 2px; - } - - .igo-layer-list-item { - height: fit-content !important; - } -} - -// CLOSE BUTTON - -::ng-deep app-legend-button-dialog { - .mat-icon, mat-button-wrapper, .mat-icon-button .mat-icon, .mat-icon-button { - line-height: 26px; - height: 24px; - width: 24px; - } - .mat-icon svg { - height: 20px; - width: 20px; - } - div.mat-dialog-content.legend > img { - max-height: 100%; - max-width: 100%; - } - div.mat-dialog-actions { - float: right; - padding: 0; - min-height: auto; - } -} + // Legend button + + #legend-button, ::ng-deep .mat-raised-button { + font-size: 0.9rem !important; + font-weight: 500 !important; + margin: 0; + min-width: 8px; + border: 0px; + border-radius: 0 !important; +} + +::ng-deep .qcca-theme #legend-button { // if Qc desing theme is applied + color: #095797 !important; + border-color: #095797 !important; +} \ No newline at end of file diff --git a/src/app/pages/portal/legend-button/legend-button.component.ts b/src/app/pages/portal/legend-button/legend-button.component.ts index 21af636f..dc4d29b1 100644 --- a/src/app/pages/portal/legend-button/legend-button.component.ts +++ b/src/app/pages/portal/legend-button/legend-button.component.ts @@ -1,60 +1,44 @@ -import { Component, OnInit } from '@angular/core'; -import { MatDialog, MatDialogState } from '@angular/material/dialog'; -import { IgoMap, Layer } from '@igo2/geo'; -import { MapState } from '@igo2/integration'; -import { Observable } from 'rxjs'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { LanguageService } from '@igo2/core'; +import { LegendDialogComponent } from '../legend-dialog/legend-dialog.component'; @Component({ selector: 'app-legend-button', templateUrl: './legend-button.component.html', styleUrls: ['./legend-button.component.scss'] }) - export class LegendButtonComponent { public dialogRef = null; - constructor(public dialog: MatDialog) {} + public legendButtonTooltip = this.languageService.translate.instant('legend.open'); + + @Output() toggleLegend = new EventEmitter(); + + @Input() tooltipDisabled: boolean; + + @Input() legendInPanel: boolean; + + @Input() mobile: boolean; - toggleDialog() { - const dialogOpened = this.dialog.getDialogById('legend-button-dialog-container'); + constructor(public dialog: MatDialog, protected languageService: LanguageService) { } + + toggleLegendButton(): void { + if (!this.legendInPanel && !this.mobile){ + const dialogOpened = this.dialog.getDialogById('legend-dialog-container'); if (!dialogOpened) { - this.dialogRef = this.dialog.open(LegendButtonDialogComponent, { - id: 'legend-button-dialog-container', + this.legendButtonTooltip = this.languageService.translate.instant('legend.close'); + this.dialogRef = this.dialog.open(LegendDialogComponent, { + id: 'legend-dialog-container', hasBackdrop: false, closeOnNavigation: true }); } else { + this.legendButtonTooltip = this.languageService.translate.instant('legend.open'); this.dialogRef.close(); } - } -} - -@Component({ - selector: 'app-legend-button-dialog', - templateUrl: 'legend-button-dialog.component.html' -}) -export class LegendButtonDialogComponent implements OnInit { - - public getState: MatDialogState; - - get map(): IgoMap { - return this.mapState.map; - } - - get layers$(): Observable { - return this.map.layers$; - } - - public mapLayersShownInLegend: Layer[]; - - constructor( - private mapState: MapState - ) {} - - ngOnInit() { - this.mapLayersShownInLegend = this.map.layers.filter(layer => ( - layer.showInLayerList !== false - )); + } + this.toggleLegend.emit(); } } diff --git a/src/app/pages/portal/legend-button/legend-button.module.ts b/src/app/pages/portal/legend-button/legend-button.module.ts index f4a3f8b5..a973a90d 100644 --- a/src/app/pages/portal/legend-button/legend-button.module.ts +++ b/src/app/pages/portal/legend-button/legend-button.module.ts @@ -1,34 +1,26 @@ -import { LegendButtonComponent, LegendButtonDialogComponent } from './legend-button.component'; +import { LegendButtonComponent } from './legend-button.component'; import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LegendDialogModule } from '../legend-dialog/legend-dialog.module'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { IgoMapModule, IgoLayerModule } from '@igo2/geo'; -import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { IgoLanguageModule } from '@igo2/core'; +import { MatDialogModule } from '@angular/material/dialog'; @NgModule({ - declarations: [LegendButtonComponent, LegendButtonDialogComponent], + declarations: [LegendButtonComponent], imports: [ + CommonModule, MatIconModule, + MatButtonModule, + IgoMapModule, IgoLayerModule, + MatTooltipModule, + IgoLanguageModule, MatDialogModule, - IgoLayerModule, - IgoMapModule, - MatButtonModule + LegendDialogModule ], - providers: - [{ - provide: 'app-legend-button-dialog', - useValue: LegendButtonDialogComponent, -}, - {provide:MatDialogRef , useValue:{} }, - - { provide: MAT_DIALOG_DATA, useValue: {} } - -], - exports: [ - LegendButtonComponent, - LegendButtonDialogComponent, - MatIconModule - ], - bootstrap: [LegendButtonComponent], + exports: [LegendButtonComponent] }) export class LegendButtonModule { } diff --git a/src/app/pages/portal/legend-button/legend-button-dialog.component.html b/src/app/pages/portal/legend-dialog/legend-dialog.component.html similarity index 80% rename from src/app/pages/portal/legend-button/legend-button-dialog.component.html rename to src/app/pages/portal/legend-dialog/legend-dialog.component.html index 0ba0ffe0..05dee5a3 100644 --- a/src/app/pages/portal/legend-button/legend-button-dialog.component.html +++ b/src/app/pages/portal/legend-dialog/legend-dialog.component.html @@ -1,5 +1,5 @@
-

Légende

+

{{"legend.title" | translate}}

Légende [allowShowAllLegends]="false" [updateLegendOnResolutionChange]="true" [showAllLegendsValue]="false"> - + \ No newline at end of file diff --git a/src/app/pages/portal/legend-dialog/legend-dialog.component.scss b/src/app/pages/portal/legend-dialog/legend-dialog.component.scss new file mode 100644 index 00000000..cf11882e --- /dev/null +++ b/src/app/pages/portal/legend-dialog/legend-dialog.component.scss @@ -0,0 +1,112 @@ +@import '../portal.variables.scss'; + +// FLEX + +::ng-deep #legend-dialog-container { + width: fit-content !important; + z-index: 10; + position: fixed; + right: 0.5%; + max-width: 99% !important; + padding: 16px !important; + max-height: calc(100% - #{$header-height-mobile} - #{$footer-height} - 100px) !important; + overflow: hidden; + display: flex; + flex-direction: column; + height: 100%; + border-radius: 0; + height: fit-content; + + igo-layer-legend-list { + max-height: inherit; + .layer-legend-list-container { + overflow-y: auto; + height: inherit; + max-height: inherit; + } + } + + igo-layer-legend-item { + flex-wrap: wrap; + width: min-content; + display: flex; + flex-direction: column; + margin-bottom: 16px; + } + + igo-layer-legend-item:last-child { + margin-bottom: 0; + } + + .igo-layer-legend-container { + padding-left: 0 !important; + width: max-content !important; + } + + @media (min-width: 768px){ + max-height: calc(100% - #{$header-height} - #{$footer-height} - 180px) !important; //TODO : Inject the dialog in the portal then add config hasHeader, hasMenu and hasFooter + height: fit-content; + } + +} + +::ng-deep app-legend-dialog { + + max-height: calc(100% - 24px) !important; + height: inherit; + + .mat-list-base .mat-list-item .mat-list-item-content { + padding: 0 !important; + } + + // TITLES + + .mat-dialog-title { + margin: 0; + display: block; + } + + .mat-dialog-title:after { + content: ""; + display: block; + width: 2.8rem; + border-bottom: 4px solid #f09686; + margin-bottom: 0.8rem; + } + + .mat-list-item:after { + content: ""; + display: block; + width: 1.8rem; + border-bottom: 3px solid #DAE6F0; + margin-bottom: 0.4rem; + padding-top: 2px; + } + + .igo-layer-list-item { + height: fit-content !important; + } +} + +// CLOSE BUTTON + +::ng-deep app-legend-dialog { + .mat-icon, mat-button-wrapper, .mat-icon-button .mat-icon, .mat-icon-button { + line-height: 26px; + height: 24px; + width: 24px; + } + .mat-icon svg { + height: 20px; + width: 20px; + } + div.mat-dialog-content.legend > img { + max-height: 100%; + max-width: 100%; + } + div.mat-dialog-actions { + float: right; + padding: 0; + min-height: auto; + } +} \ No newline at end of file diff --git a/src/app/pages/portal/legend-dialog/legend-dialog.component.ts b/src/app/pages/portal/legend-dialog/legend-dialog.component.ts new file mode 100644 index 00000000..30f98235 --- /dev/null +++ b/src/app/pages/portal/legend-dialog/legend-dialog.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogState } from '@angular/material/dialog'; +import { IgoMap, Layer } from '@igo2/geo'; +import { MapState } from '@igo2/integration'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-legend-dialog', + templateUrl: 'legend-dialog.component.html', + styleUrls: ['./legend-dialog.component.scss'] +}) +export class LegendDialogComponent implements OnInit { + + public getState: MatDialogState; + + get map(): IgoMap { + return this.mapState.map; + } + + get layers$(): Observable { + return this.map.layers$; + } + + public mapLayersShownInLegend: Layer[]; + + constructor( + private mapState: MapState + ) {} + + ngOnInit() { + this.mapLayersShownInLegend = this.map.layers.filter(layer => ( + layer.showInLayerList !== false + )); + } +} diff --git a/src/app/pages/portal/legend-dialog/legend-dialog.module.ts b/src/app/pages/portal/legend-dialog/legend-dialog.module.ts new file mode 100644 index 00000000..827ca4a8 --- /dev/null +++ b/src/app/pages/portal/legend-dialog/legend-dialog.module.ts @@ -0,0 +1,25 @@ +import { LegendDialogComponent } from './legend-dialog.component'; +import { NgModule } from '@angular/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { IgoMapModule, IgoLayerModule } from '@igo2/geo'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { IgoLanguageModule } from '@igo2/core'; +import { MatDialogModule } from '@angular/material/dialog'; + +@NgModule({ + declarations: [LegendDialogComponent], + imports: [ + MatIconModule, + MatDialogModule, + IgoLayerModule, + IgoMapModule, + MatButtonModule, + MatTooltipModule, + IgoLanguageModule + ], + exports: [ + LegendDialogComponent + ] +}) +export class LegendDialogModule { } diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.html b/src/app/pages/portal/panels/bottompanel/bottompanel.component.html new file mode 100644 index 00000000..41ab2f23 --- /dev/null +++ b/src/app/pages/portal/panels/bottompanel/bottompanel.component.html @@ -0,0 +1,102 @@ +
+ + + + + + + +
+ +

{{ 'legend.title' | translate }}

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

+
+
+ +
+ + + + +
+ +
+ + + + +
+ +
+
\ No newline at end of file diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss b/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss new file mode 100644 index 00000000..72789213 --- /dev/null +++ b/src/app/pages/portal/panels/bottompanel/bottompanel.component.scss @@ -0,0 +1,144 @@ +@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%; + scrollbar-color: #095797 #fff; //firefox + scrollbar-width: thin; //firefox +} + +::ng-deep app-bottompanel .mat-expansion-panel-content::-webkit-scrollbar { + width: 6px; +} + +::ng-deep app-bottompanel .mat-expansion-panel-content::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px #DAE6F0; + border-radius: 10px; +} + +::ng-deep app-bottompanel .mat-expansion-panel-content::-webkit-scrollbar-thumb { + background: #095797; + border-radius: 10px; +} + +::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-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; +} \ No newline at end of file diff --git a/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts b/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts new file mode 100644 index 00000000..d3cec9eb --- /dev/null +++ b/src/app/pages/portal/panels/bottompanel/bottompanel.component.ts @@ -0,0 +1,558 @@ +import { + Component, + Input, + OnInit, + Output, + OnDestroy, + EventEmitter, + ChangeDetectionStrategy, + ChangeDetectorRef, + ElementRef +} from '@angular/core'; + +import olFeature from 'ol/Feature'; +import olPoint from 'ol/geom/Point'; + +import { StorageService } from '@igo2/core'; +import { EntityStore, ActionStore } from '@igo2/common'; +import { BehaviorSubject, Subscription, combineLatest, tap} from 'rxjs'; + +import { + IgoMap, + FEATURE, + Feature, + FeatureMotion, + MapService, + Research, + SearchResult, + SearchService, + Layer, + featureToOl, + featuresAreTooDeepInView, + featureFromOl, + getCommonVectorSelectedStyle, + getCommonVectorStyle +} from '@igo2/geo'; +import { MapState, QueryState, StorageState } from '@igo2/integration'; +import { SearchState } from '../search-results-tool/search.state'; +import { ConfigService } from '@igo2/core'; + +import type { default as OlGeometry } from 'ol/geom/Geometry'; +@Component({ + selector: 'app-bottompanel', + templateUrl: './bottompanel.component.html', + styleUrls: ['./bottompanel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +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; + + @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; + public igoSearchPointerSummaryEnabled: boolean = false; + 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 searchState: SearchState, + 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('showSearchBar') === undefined ? true : + this.configService.getConfig('showSearchBar'); + 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(); + } + + onPointerSummaryStatusChange(value) { + this.igoSearchPointerSummaryEnabled = value; + } + + onSearchTermChange(term = '') { + this.term = term; + this.clearedSearchbar = false; + const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); + + 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, 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-details/feature-details-custom.component.html b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.html new file mode 100644 index 00000000..c317a82d --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.html @@ -0,0 +1,60 @@ + +
+ Custom feature details template + + + +
+ +
+
+ +
+ + + +
+ {{property.key}} +
+ +
+
+ +
+ {{ getEmbeddedLinkText(property.value) }} +
+ +
+ {{ 'igo.geo.targetHtmlUrl' | translate }} + +
+ +
+ + + +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.scss b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.scss new file mode 100644 index 00000000..de57d594 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.scss @@ -0,0 +1,87 @@ +@import '@igo2/core/style/partial/core.variables'; + +div { + box-sizing: border-box; +} + +.table-container { + display: block; + margin: 2em auto; + width: 100%; +} + +.table-row { + display: flex; + flex-flow: row nowrap; + border-bottom: 1px solid #DAE6F0; +} + +.table-row:hover { + cursor: pointer; + background-color: #DAE6F0; +} + +.table-row .key { + width: 38%; + font: bold 16px/24px 'Open Sans', sans-serif; + color: #223654; + margin-top: 4px; + margin-bottom: 4px; + text-align: left; +} + +.table-row .value { + width: 62%; + font: normal 16px/24px 'Open Sans', sans-serif; + color: #223654; + text-align: left; + margin-top: 4px; + margin-bottom: 8px; +} + +.flex-row { + text-align: center; + padding: 0.5em; +} + +.info { + vertical-align: bottom; + transform: scale(0.9); +} + +.link:hover { + color: #3374CC; +} + +::ng-deep .tooltip-right, ::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-right::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); +} + +::ng-deep .tooltip-above::after { + display: none; +} \ No newline at end of file diff --git a/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.ts b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.ts new file mode 100644 index 00000000..e053b9a7 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-details/feature-details-custom.component.ts @@ -0,0 +1,237 @@ +import { + Component, + Input, + ChangeDetectionStrategy, + ChangeDetectorRef, + Output, + EventEmitter, + OnDestroy, + OnInit, +} from '@angular/core'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { NetworkService, ConnectionState, LanguageService, MessageService } from '@igo2/core'; +import { ConfigService } from '@igo2/core'; +import { SearchSource, IgoMap, Feature } from '@igo2/geo'; +import { HttpClient } from '@angular/common/http'; +import { TooltipPosition } from '@angular/material/tooltip'; +import { getEntityTitle } from '@igo2/common'; + +@Component({ + selector: 'app-feature-details-custom', + templateUrl: './feature-details-custom.component.html', + styleUrls: ['./feature-details-custom.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class FeatureDetailsCustomComponent implements OnDestroy, OnInit { + private state: ConnectionState; + private unsubscribe$ = new Subject(); + + @Input() + get source(): SearchSource { + return this._source; + } + set source(value: SearchSource) { + this._source = value; + this.cdRef.detectChanges(); + } + + @Input() map: IgoMap; + + @Input() + get feature(): Feature { + return this._feature; + } + set feature(value: Feature) { + this._feature = value; + this.cdRef.detectChanges(); + this.selectFeature.emit(); + } + + @Input() + get mobile(): boolean { + return this._mobile; + } + set mobile(value: boolean) { + this._mobile = value; + } + private _mobile: boolean; + + @Input() + get mapQueryClick(): boolean { + return this._mapQueryClick; + } + set mapQueryClick(value: boolean) { + this._mapQueryClick = value; + } + private _mapQueryClick: boolean; + + private _feature: Feature; + private _source: SearchSource; + public featureTitle: string; + + @Output() selectFeature = new EventEmitter(); + + @Input() + matTooltipPosition: TooltipPosition; + + public ready : boolean; + + constructor( + private cdRef: ChangeDetectorRef, + private networkService: NetworkService, + private languageService: LanguageService, + private configService: ConfigService, + private http: HttpClient, + private messageService: MessageService, + ) { + this.networkService.currentState().pipe(takeUntil(this.unsubscribe$)).subscribe((state: ConnectionState) => { + this.state = state; + }); + } + + ngOnInit() { + this.ready = true; + } + + ngOnDestroy() { + this.mapQueryClick = false; + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + + formatReading(reading: number): string { + return reading.toString().replace(".", ","); + } + + tooltipPosition(){ + if (this.mobile) { + this.matTooltipPosition = 'above'; + } else { + this.matTooltipPosition = 'right'; + } + } + + /** + * @internal + */ + get title(): string { + return getEntityTitle(this.feature); + } + + isObject(value) { + return typeof value === 'object'; + } + + isDoc(value) { + if (typeof value === 'string') { + if (this.isUrl(value)) { + const regex = /(pdf|docx?|xlsx?)$/; + return regex.test(value.toLowerCase()); + } else { + return false; + } + } + } + + isUrl(value) { + if (typeof value === 'string') { + const regex = /^https?:\/\//; + return regex.test(value); + } + } + + isImg(value) { + if (typeof value ==='string') { + if (this.isUrl(value)) { + const regex = /(jpe?g|png|gif)$/; + return regex.test(value.toLowerCase()); + } else { + return false; + } + } + } + + isEmbeddedLink(value) { + if (typeof value === 'string') { + const matchRegex = / + + +
+ +
+
+ +
+ +
+ +
+ {{property.key}} +
+ +
+
+ +
+ {{ getEmbeddedLinkText(property.value) }} +
+ +
+ {{ 'igo.geo.targetHtmlUrl' | translate }} + +
+ +
+ + + +
+ +
+
+
+ +
+ \ No newline at end of file diff --git a/src/app/pages/portal/panels/feature/feature-details/feature-details.component.scss b/src/app/pages/portal/panels/feature/feature-details/feature-details.component.scss new file mode 100644 index 00000000..de57d594 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-details/feature-details.component.scss @@ -0,0 +1,87 @@ +@import '@igo2/core/style/partial/core.variables'; + +div { + box-sizing: border-box; +} + +.table-container { + display: block; + margin: 2em auto; + width: 100%; +} + +.table-row { + display: flex; + flex-flow: row nowrap; + border-bottom: 1px solid #DAE6F0; +} + +.table-row:hover { + cursor: pointer; + background-color: #DAE6F0; +} + +.table-row .key { + width: 38%; + font: bold 16px/24px 'Open Sans', sans-serif; + color: #223654; + margin-top: 4px; + margin-bottom: 4px; + text-align: left; +} + +.table-row .value { + width: 62%; + font: normal 16px/24px 'Open Sans', sans-serif; + color: #223654; + text-align: left; + margin-top: 4px; + margin-bottom: 8px; +} + +.flex-row { + text-align: center; + padding: 0.5em; +} + +.info { + vertical-align: bottom; + transform: scale(0.9); +} + +.link:hover { + color: #3374CC; +} + +::ng-deep .tooltip-right, ::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-right::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); +} + +::ng-deep .tooltip-above::after { + display: none; +} \ No newline at end of file diff --git a/src/app/pages/portal/panels/feature/feature-details/feature-details.component.ts b/src/app/pages/portal/panels/feature/feature-details/feature-details.component.ts new file mode 100644 index 00000000..9676da64 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-details/feature-details.component.ts @@ -0,0 +1,243 @@ +import { + Component, + Input, + ChangeDetectionStrategy, + ChangeDetectorRef, + Output, + EventEmitter, + OnDestroy, + OnInit, +} from '@angular/core'; + +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { NetworkService, ConnectionState, MessageService } from '@igo2/core'; +import { ConfigService } from '@igo2/core'; +import { SearchSource, IgoMap, Feature } from '@igo2/geo'; +import { HttpClient } from '@angular/common/http'; +import { Clipboard } from '@igo2/utils'; + +@Component({ + selector: 'app-feature-details', + templateUrl: './feature-details.component.html', + styleUrls: ['./feature-details.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) + +export class FeatureDetailsComponent implements OnDestroy, OnInit { + private state: ConnectionState; + private unsubscribe$ = new Subject(); + + @Input() + get source(): SearchSource { + return this._source; + } + set source(value: SearchSource) { + this._source = value; + this.cdRef.detectChanges(); + } + + @Input() map: IgoMap; + + @Input() + get feature(): Feature { + return this._feature; + } + set feature(value: Feature) { + this._feature = value; + this.cdRef.detectChanges(); + this.selectFeature.emit(); + } + + @Input() mobile: boolean; + @Input() mapQueryClick: boolean; + @Output() mapQuery = new EventEmitter(); + + private _feature: Feature; + private _source: SearchSource; + public featureTitle: string; + + @Output() selectFeature = new EventEmitter(); + + public title: string; + + public ready : boolean; + + constructor( + private cdRef: ChangeDetectorRef, + private networkService: NetworkService, + private configService: ConfigService, + private http: HttpClient, + private messageService: MessageService, + ) { + this.networkService.currentState().pipe(takeUntil(this.unsubscribe$)).subscribe((state: ConnectionState) => { + this.state = state; + }); + } + + ngOnInit() { + this.ready = true; + this.title = this.feature.properties.value; + } + + ngOnDestroy() { + this.mapQuery.emit(false); + this.unsubscribe$.next(); + this.unsubscribe$.complete(); + } + + formatReading(reading: number): string { + return reading.toString().replace(".", ","); + } + + /** + * @internal + */ + + isObject(value) { + return typeof value === 'object'; + } + + isDoc(value) { + if (typeof value === 'string') { + if (this.isUrl(value)) { + const regex = /(pdf|docx?|xlsx?)$/; + return regex.test(value.toLowerCase()); + } else { + return false; + } + } + } + + isUrl(value) { + if (typeof value === 'string') { + const regex = /^https?:\/\//; + return regex.test(value); + } + } + + isImg(value) { + if (typeof value ==='string') { + if (this.isUrl(value)) { + const regex = /(jpe?g|png|gif)$/; + return regex.test(value.toLowerCase()); + } else { + return false; + } + } + } + + isEmbeddedLink(value) { + if (typeof value === 'string') { + const matchRegex = / + + +

{{title}}

+ + + +
+ + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 00000000..0885252e --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-info/feature-info.component.scss @@ -0,0 +1,34 @@ +@import '../../../portal.variables'; + +:host { + + @include mobile { + width: 100%; + overflow-x: clip; + } +} + +.title { + display: flex; + flex-direction: row; + align-items: center; +} + +.title h4{ + display: block; + text-align: center; +} + +.title button { + float: right; + width: 24px; + height: 24px; + margin-left: auto; +} + +app-feature-details { + float: left; + width: 100%; + overflow-x: clip; + margin-right: 8px; +} \ No newline at end of file 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 new file mode 100644 index 00000000..88343932 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-info/feature-info.component.ts @@ -0,0 +1,256 @@ +import { + Component, + Input, + Output, + EventEmitter, + HostBinding, + HostListener, + ChangeDetectionStrategy, + OnInit, + OnDestroy +} from '@angular/core'; +import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { + EntityStore +} from '@igo2/common'; +import { + Feature, + SearchResult, + IgoMap, + FeatureMotion, + getCommonVectorStyle, + getCommonVectorSelectedStyle, + featuresAreOutOfView, + computeOlFeaturesExtent, + featureToOl +} from '@igo2/geo'; +import { + MediaService, + LanguageService, + StorageService, + ConfigService +} from '@igo2/core'; +import { QueryState, StorageState } from '@igo2/integration'; +import { SearchState } from '../../search-results-tool/search.state'; + +@Component({ + selector: 'app-feature-info', + templateUrl: './feature-info.component.html', + styleUrls: ['./feature-info.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +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() + 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, + private searchState: SearchState + ) { + this.customFeatureTitle = this.configService.getConfig('customFeatureTitle') === undefined ? false : + this.configService.getConfig('customFeatureTitle'); + this.customFeatureDetails = this.configService.getConfig('customFeatureDetails') === undefined ? false : + this.configService.getConfig('customFeatureDetails'); + } + + 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(this.map, [selectedOlFeature]); + this.isSelectedResultOutOfView$.next(featuresAreOutOfView(this.map, 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/feature/feature-info/feature-info.module.ts b/src/app/pages/portal/panels/feature/feature-info/feature-info.module.ts new file mode 100644 index 00000000..611c6840 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature-info/feature-info.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { IgoStopPropagationModule } from '@igo2/common'; +import { IgoLanguageModule } from '@igo2/core'; +import { IgoSearchResultsModule } from '@igo2/geo'; +import { IgoFeatureModule } from '../feature.module'; +import { FeatureInfoComponent } from './feature-info.component'; + +@NgModule({ + imports: [ + CommonModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + IgoLanguageModule, + IgoStopPropagationModule, + IgoFeatureModule, + IgoSearchResultsModule + ], + exports: [FeatureInfoComponent], + declarations: [FeatureInfoComponent] +}) +export class AppFeatureInfoModule {} diff --git a/src/app/pages/portal/panels/feature/feature.module.ts b/src/app/pages/portal/panels/feature/feature.module.ts new file mode 100644 index 00000000..e202f683 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { IgoFeatureDetailsModule } from './feature-details/feature-details.module'; +import { IgoFeatureFormModule } from '@igo2/geo'; + +@NgModule({ + imports: [ + CommonModule + ], + exports: [ + IgoFeatureDetailsModule, + IgoFeatureFormModule + ], + declarations: [], + providers: [] +}) +export class IgoFeatureModule {} diff --git a/src/app/pages/portal/panels/feature/feature.theming.scss b/src/app/pages/portal/panels/feature/feature.theming.scss new file mode 100644 index 00000000..fbd67067 --- /dev/null +++ b/src/app/pages/portal/panels/feature/feature.theming.scss @@ -0,0 +1,3 @@ +@mixin igo-feature-theming($theme) { + @include igo-feature-details-theming($theme); +} diff --git a/src/app/pages/portal/sideresult/sideresult.module.ts b/src/app/pages/portal/panels/panels.module.ts similarity index 62% rename from src/app/pages/portal/sideresult/sideresult.module.ts rename to src/app/pages/portal/panels/panels.module.ts index 172c9d6e..f037b48d 100644 --- a/src/app/pages/portal/sideresult/sideresult.module.ts +++ b/src/app/pages/portal/panels/panels.module.ts @@ -8,8 +8,12 @@ import { MatIconModule } from '@angular/material/icon'; import { MatSidenavModule } from '@angular/material/sidenav'; import { MatTooltipModule } from '@angular/material/tooltip'; import {MatExpansionModule} from '@angular/material/expansion'; // mobile +import { IgoLanguageModule } from '@igo2/core'; +import { IgoMessageModule } from '@igo2/core'; +import { AppFeatureInfoModule} from './feature/feature-info/feature-info.module'; +import { IgoFeatureModule } from './feature/feature.module'; +import { IgoFeatureDetailsModule} from './feature/feature-details/feature-details.module'; -import { IgoLanguageModule, IgoMessageModule } from '@igo2/core'; import { IgoPanelModule, IgoFlexibleModule, @@ -18,13 +22,14 @@ import { IgoContextMenuModule } from '@igo2/common'; import { - IgoFeatureModule, IgoMapModule, - IgoSearchModule + IgoSearchModule, + IgoLayerModule, + IgoSearchResultsModule } from '@igo2/geo'; import { IgoContextManagerModule } from '@igo2/context'; -import { SideResultComponent } from './sideresult.component'; -import { BottomResultComponent } from './bottomresult.component'; +import { SidePanelComponent } from './sidepanel/sidepanel.component'; +import { BottomPanelComponent } from './bottompanel/bottompanel.component'; @NgModule({ imports: [ @@ -38,7 +43,6 @@ import { BottomResultComponent } from './bottomresult.component'; IgoFlexibleModule, IgoContextManagerModule, IgoToolModule, - IgoFeatureModule, //SEARCH MatCardModule, IgoMessageModule, @@ -49,10 +53,15 @@ import { BottomResultComponent } from './bottomresult.component'; IgoAppSearchModule, IgoSearchModule.forRoot(), IgoAppSearchResultsToolModule, - MatExpansionModule + MatExpansionModule, + AppFeatureInfoModule, + IgoFeatureModule, + IgoFeatureDetailsModule, + IgoLayerModule, + IgoSearchResultsModule ], - exports: [SideResultComponent, BottomResultComponent ], + exports: [SidePanelComponent, BottomPanelComponent], //SEARCH - declarations: [SideResultComponent, BottomResultComponent] + declarations: [SidePanelComponent, BottomPanelComponent] }) -export class AppSideResultModule {} +export class AppPanelsModule {} diff --git a/src/app/pages/portal/sideresult/sideresult.theming.scss b/src/app/pages/portal/panels/panels.theming.scss similarity index 100% rename from src/app/pages/portal/sideresult/sideresult.theming.scss rename to src/app/pages/portal/panels/panels.theming.scss 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 new file mode 100644 index 00000000..10ee7964 --- /dev/null +++ b/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.html @@ -0,0 +1,36 @@ + + +
+ + + + + + +
+ +
diff --git a/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.ts b/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts similarity index 87% rename from src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.ts rename to src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts index c0043f85..8c355272 100644 --- a/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.ts +++ b/src/app/pages/portal/panels/search-results-tool/search-results-tool.component.ts @@ -4,7 +4,9 @@ import { Input, OnInit, ElementRef, - OnDestroy + OnDestroy, + HostListener, + Output, EventEmitter } from '@angular/core'; import { Observable, BehaviorSubject, Subscription, combineLatest } from 'rxjs'; import { debounceTime, map } from 'rxjs/operators'; @@ -20,9 +22,7 @@ import { ConfigService } from '@igo2/core'; import { EntityStore, ToolComponent, - FlexibleState, getEntityTitle, - FlexibleComponent, EntityState } from '@igo2/common'; @@ -44,7 +44,8 @@ import { roundCoordTo } from '@igo2/geo'; -import { MapState, SearchState, ToolState, DirectionState } from '@igo2/integration'; +import { MapState, ToolState, DirectionState, QueryState } from '@igo2/integration'; +import { SearchState } from './search.state'; /** * Tool to browse the search results @@ -65,13 +66,7 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { */ @Input() showIcons: boolean = true; - /** - * Determine the top panel default state - */ - @Input() topPanelStateDefault: string = 'expanded'; - - private hasFeatureEmphasisOnSelection: boolean = false; - + private hasFeatureEmphasisOnSelection: boolean; private showResultsGeometries$$: Subscription; private getRoute$$: Subscription; private shownResultsGeometries: Feature[] = []; @@ -86,6 +81,8 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { 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 @@ -120,25 +117,12 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { ) ); } - public feature: Feature; public term = ''; private searchTerm$$: Subscription; - public settingsChange$ = new BehaviorSubject(undefined); - public topPanelState$ = new BehaviorSubject('initial'); - private topPanelState$$: Subscription; - - @Input() - set topPanelState(value: FlexibleState) { - this.topPanelState$.next(value); - } - get topPanelState(): FlexibleState { - return this.topPanelState$.value; - } - get termSplitter(): string { return this.searchState.searchTermSplitter$.value; } @@ -149,20 +133,60 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { 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 searchState: SearchState, private elRef: ElementRef, public toolState: ToolState, private directionState: DirectionState, - configService: ConfigService + 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) { @@ -171,29 +195,6 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } ); - for (const res of this.store.stateView.all$().value) { - if (this.store.state.get(res.entity).selected === true) { - this.topPanelState = 'expanded'; - } - } - - this.searchState.searchSettingsChange$.subscribe(() => { - this.settingsChange$.next(true); - }); - - this.topPanelState$$ = this.topPanelState$.subscribe(() => { - const igoList = this.computeElementRef()[0]; - const selected = this.computeElementRef()[1]; - if (selected) { - setTimeout(() => { - // To be sure the flexible component has been displayed yet - if (!this.isScrolledIntoView(igoList, selected)) { - this.adjustTopPanel(igoList, selected); - } - }, FlexibleComponent.transitionTime + 50); - } - }); - if (this.hasFeatureEmphasisOnSelection) { if (!this.searchState.focusedOrResolution$$) { this.searchState.focusedOrResolution$$ = combineLatest([ @@ -368,8 +369,8 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } } + @HostListener('change') ngOnDestroy() { - this.topPanelState$$.unsubscribe(); this.searchTerm$$.unsubscribe(); if (this.isSelectedResultOutOfView$$) { this.isSelectedResultOutOfView$$.unsubscribe(); @@ -405,6 +406,7 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { return; } this.map.searchResultsOverlay.addFeature(result.data as Feature, FeatureMotion.None); + this.featureSelected.emit(); } } @@ -415,6 +417,7 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } if (this.store.state.get(result).selected === true) { + this.featureSelected.emit(); const feature = this.map.searchResultsOverlay.dataSource.ol.getFeatureById(result.meta.id); if (feature) { const style = getCommonVectorSelectedStyle( @@ -437,41 +440,24 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { onResultSelect(result: SearchResult) { this.map.searchResultsOverlay.dataSource.ol.clear(); this.tryAddFeatureToMap(result); - //this.searchState.setSelectedResult(result); - /* - if (this.topPanelState === 'initial') { - if (this.topPanelStateDefault !== 'collapsed') { - this.topPanelState = 'expanded'; - } else { - this.topPanelState = 'collapsed'; - } - }*/ - /* - if (this.topPanelState === 'expanded') { - const igoList = this.computeElementRef()[0]; - const selected = this.computeElementRef()[1]; - if (!this.isScrolledIntoView(igoList, selected)) { - this.adjustTopPanel(igoList, selected); - } - }*/ + 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; - const newResults = this.store.entities$.value + 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.store.load(newResults); - - for (const res of this.store.all()) { - if ( - this.store.state.get(res).focused === true && - this.store.state.get(res).selected !== true - ) { - this.store.state.update(res, { focused: false }, true); - } - } + this.searchStore.updateMany(newResults); setTimeout(() => { const igoList = this.elRef.nativeElement.querySelector('igo-list'); @@ -488,7 +474,6 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } else { moreResults = igoList.querySelector('.' + source[0].source.getId() + ' .moreResults'); } - if ( moreResults !== null && !this.isScrolledIntoView(igoList, moreResults) @@ -526,14 +511,6 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } } - toggleTopPanel() { - if (this.topPanelState === 'expanded') { - this.topPanelState = 'collapsed'; - } else { - this.topPanelState = 'expanded'; - } - } - zoomToFeatureExtent() { // console.log("zoomToFeatureExtent") if (this.feature.geometry) { @@ -580,7 +557,7 @@ export class SearchResultsToolComponent implements OnInit, OnDestroy { } getRoute() { - this.toolState.toolbox.activateTool('directions'); + //this.toolState.toolbox.activateTool('directions'); this.directionState.stopsStore.clearStops(); setTimeout(() => { let routingCoordLoaded = false; diff --git a/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.module.ts b/src/app/pages/portal/panels/search-results-tool/search-results-tool.module.ts similarity index 100% rename from src/app/pages/portal/sideresult/search-results-tool/search-results-tool.module.ts rename to src/app/pages/portal/panels/search-results-tool/search-results-tool.module.ts diff --git a/src/app/pages/portal/panels/search-results-tool/search.state.ts b/src/app/pages/portal/panels/search-results-tool/search.state.ts new file mode 100644 index 00000000..39f71d51 --- /dev/null +++ b/src/app/pages/portal/panels/search-results-tool/search.state.ts @@ -0,0 +1,144 @@ +import { Injectable } from '@angular/core'; + +import { EntityRecord, EntityStore, EntityStoreFilterCustomFuncStrategy, EntityStoreStrategyFuncOptions } from '@igo2/common'; +import { ConfigService, StorageService } from '@igo2/core'; +import { SearchResult, SearchSourceService, SearchSource, CommonVectorStyleOptions } from '@igo2/geo'; +import { BehaviorSubject, Subscription } from 'rxjs'; + +/** + * Service that holds the state of the search module + */ +@Injectable({ + providedIn: 'root' +}) +export class SearchState { + public searchOverlayStyle: CommonVectorStyleOptions = {}; + public searchOverlayStyleSelection: CommonVectorStyleOptions = {}; + public searchOverlayStyleFocus: CommonVectorStyleOptions = {}; + + public focusedOrResolution$$: Subscription; + public selectedOrResolution$$: Subscription; + + readonly searchTermSplitter$: BehaviorSubject = new BehaviorSubject('|'); + + readonly searchTerm$: BehaviorSubject = new BehaviorSubject(undefined); + + readonly searchType$: BehaviorSubject = new BehaviorSubject(undefined); + + readonly searchDisabled$: BehaviorSubject = new BehaviorSubject(false); + + readonly searchResultsGeometryEnabled$: BehaviorSubject = new BehaviorSubject(false); + + readonly selectedResult$: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * Store that holds the search results + */ + readonly store: EntityStore = new EntityStore([]); + + /** + * Search types currently enabled in the search source service + */ + get searchTypes(): string[] { + return this.searchSourceService + .getEnabledSources() + .map((source: SearchSource) => (source.constructor as any).type); + } + + constructor( + private searchSourceService: SearchSourceService, + private storageService: StorageService, + private configService: ConfigService) { + this.searchOverlayStyle = { + markerColor: '#45D7E6', // marker fill + markerOpacity: 0.8, // marker opacity not applied if a rgba markerColor is provided + markerOutlineColor: '#ffffff', // marker contour + fillColor: 'transparent', // poly + fillOpacity: 0, // poly fill opacity not applied if a rgba fillColor is provided + strokeColor: '#45D7E6', // line and poly + strokeOpacity: 0.7, // line and poly not applied if a rgba strokeColor is provided + strokeWidth: 4 // line and poly + }; + this.searchOverlayStyleSelection = { + markerColor: '#45D7E6', // marker fill + markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided + markerOutlineColor: '#ffffff', // marker contour + fillColor: 'transparent', // poly + fillOpacity: 0, // poly fill opacity not applied if a rgba fillColor is provided + strokeColor: '#45D7E6', // line and poly + strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided + strokeWidth: 4 // line and poly + }; + this.searchOverlayStyleFocus = { + markerColor: '#45D7E6', // marker fill + markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided + markerOutlineColor: '#ffffff', // marker contour + fillColor: 'transparent', // poly + fillOpacity: 0, // poly fill opacity not applied if a rgba fillColor is provided + strokeColor: '#45D7E6', // line and poly + strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided + strokeWidth: 4 // line and poly + }; + + const searchResultsGeometryEnabled = this.storageService.get('searchResultsGeometryEnabled') as boolean; + if (searchResultsGeometryEnabled) { + this.searchResultsGeometryEnabled$.next(searchResultsGeometryEnabled); + } + this.store.addStrategy(this.createCustomFilterTermStrategy(), false); + } + + private createCustomFilterTermStrategy(): EntityStoreFilterCustomFuncStrategy { + const filterClauseFunc = (record: EntityRecord) => { + return record.entity.meta.score === 100; + }; + return new EntityStoreFilterCustomFuncStrategy({filterClauseFunc} as EntityStoreStrategyFuncOptions); + } + + /** + * Activate custom strategy + * + */ + activateCustomFilterTermStrategy() { + const strategy = this.store.getStrategyOfType(EntityStoreFilterCustomFuncStrategy); + if (strategy !== undefined) { + strategy.activate(); + } + } + + /** + * Deactivate custom strategy + * + */ + deactivateCustomFilterTermStrategy() { + const strategy = this.store.getStrategyOfType(EntityStoreFilterCustomFuncStrategy); + if (strategy !== undefined) { + strategy.deactivate(); + } + } + + enableSearch() { + this.searchDisabled$.next(false); + } + + disableSearch() { + this.searchDisabled$.next(true); + } + + setSearchTerm(searchTerm: string) { + this.searchTerm$.next(searchTerm); + } + + setSearchType(searchType: string) { + this.searchSourceService.enableSourcesByType(searchType); + this.searchType$.next(searchType); + } + + setSelectedResult(result: SearchResult) { + this.selectedResult$.next(result); + } + + setSearchResultsGeometryStatus(value) { + this.storageService.set('searchResultsGeometryEnabled', value); + this.searchResultsGeometryEnabled$.next(value); + } +} diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.html b/src/app/pages/portal/panels/sidepanel/sidepanel.component.html new file mode 100644 index 00000000..0a53300c --- /dev/null +++ b/src/app/pages/portal/panels/sidepanel/sidepanel.component.html @@ -0,0 +1,86 @@ +
+ + +
+ +
+ +

{{ 'legend.title' | translate }}

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

+
+
+ +
+ + + + +
+ +
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss b/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss new file mode 100644 index 00000000..05bd7707 --- /dev/null +++ b/src/app/pages/portal/panels/sidepanel/sidepanel.component.scss @@ -0,0 +1,161 @@ +@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; + + @include mobile { + width: $app-sidenav-width-mobile; + max-width: 400px; + } + overflow: visible; +} + +.app-sidenav-content { + padding: 0 16px 0 16px!important; +} + +.app-content, igo-panel { + height: 100%; +} + +/* +.app-sidepanel-content { // no reference to it, to delete ? + height: 100%; + display: block; + position: relative; + overflow: hidden; +}*/ + +app-sidepanel { + width: $app-sidenav-width; +} + +::ng-deep 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 * $igo-margin); + top: calc(2 * $igo-margin); + height: 100%; +} + +#sidepanel-button { + position: absolute; + top: 50%; + left: calc($app-sidenav-width + 1px); + z-index: 1; + padding: 0; + min-width: 12px!important; + width: 24px; + border-radius: 0; +} + +#sidepanel-button: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; } +} + +/*mat-sidenav::backdrop { + z-index: 999; +}*/ + +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); +} + +:host { + scrollbar-color: #095797 #fff; //firefox + scrollbar-width: thin; //firefox +} + +::ng-deep app-sidepanel .mat-drawer-inner-container::-webkit-scrollbar { + width: 6px; +} + +::ng-deep app-sidepanel .mat-drawer-inner-container::-webkit-scrollbar-track { + box-shadow: inset 0 0 5px #DAE6F0; + border-radius: 10px; +} + +::ng-deep app-sidepanel .mat-drawer-inner-container::-webkit-scrollbar-thumb { + background: #095797; + border-radius: 10px; + cursor: pointer; // I wish it worked out +} + +// Legend + +.mat-icon, mat-button-wrapper, .mat-icon-button .mat-icon, .mat-icon-button { + line-height: 26px; + height: 24px; + width: 24px; + float: right; +} +.mat-icon svg { + height: 20px; + width: 20px; +} + +.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); +} \ No newline at end of file diff --git a/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts b/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts new file mode 100644 index 00000000..4a279d18 --- /dev/null +++ b/src/app/pages/portal/panels/sidepanel/sidepanel.component.ts @@ -0,0 +1,260 @@ +import { + Component, + Input, + Output, + OnInit, + OnDestroy, + EventEmitter, + ChangeDetectionStrategy, + HostListener, + ChangeDetectorRef +} from '@angular/core'; + +import { EntityStore, ActionStore } from '@igo2/common'; + +import { BehaviorSubject } from 'rxjs'; + +import { + IgoMap, + FEATURE, + Feature, + FeatureMotion, + SearchResult, + Layer +} from '@igo2/geo'; +import { QueryState } from '@igo2/integration'; +import { ConfigService } from '@igo2/core'; +import { SearchState } from '../search-results-tool/search.state'; + +@Component({ + selector: 'app-sidepanel', + templateUrl: './sidepanel.component.html', + styleUrls: ['./sidepanel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SidePanelComponent implements OnInit, OnDestroy { + title$: BehaviorSubject = new BehaviorSubject(undefined); + + @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 searchState: SearchState, + 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.component.html b/src/app/pages/portal/portal.component.html index 49cb6fd7..29a6734a 100644 --- a/src/app/pages/portal/portal.component.html +++ b/src/app/pages/portal/portal.component.html @@ -1,101 +1,114 @@ - + - -
- +
- - + (clearFeature)="clearSearch()"> -
+
- - + [mobile]="mobile" + [(opened)]="panelOpenState" + [searchInit]="searchInit" + [store]="queryStore" + [legendPanelOpened]="legendPanelOpened" + [mapQueryClick]="mapQueryClick" + [panelOpenState]="panelOpenState" + (panelOpened)="panelOpened($event)" + (toggleLegend)="togglePanelLegend()" + (closeLegend)="closePanelLegend()" + (closeQuery)="closePanelOnCloseQuery()" + (openLegend)="openPanelLegend()" + [layers]="mapLayersShownInLegend"> +
- + - - - -
- - - - -
+
+ + + + +
+ + + + +
- + + panelOpenState ? 'right' : 'left'">
-
\ No newline at end of file diff --git a/src/app/pages/portal/portal.component.scss b/src/app/pages/portal/portal.component.scss index bca3fe43..6b7f0678 100644 --- a/src/app/pages/portal/portal.component.scss +++ b/src/app/pages/portal/portal.component.scss @@ -79,6 +79,42 @@ 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 .qcca-theme mat-button-toggle.mat-button-toggle-checked .mat-button-toggle-label-content { + font-size: 12px; + } + ::ng-deep .mat-expansion-panel-body { + padding: 0 8px 8px; + } +} + /*** Sidenav ***/ mat-sidenav-container { height: 100%; @@ -105,7 +141,7 @@ igo-search-bar { background-color: $app-background-color; font-size: 16px; position: absolute; - top: calc(4 * #{$igo-margin}); + top: calc(4 * $igo-margin); z-index: 4; height: $igo-icon-size; margin: 0 $igo-margin; @@ -124,8 +160,14 @@ igo-search-bar, igo-search-results igo-list, .baselayers { left: $portal-left!important; } +::ng-deep .igo-search-bar-container .mat-icon { + fill: #FFFFFF; +} + +// Mobile pushed elements + ::ng-deep .baselayers-pushed { - left: calc(#{$app-sidenav-width} + 4px)!important; + left: calc($app-sidenav-width + 4px)!important; animation-duration: 3s; visibility: visible; opacity: 1; @@ -135,13 +177,21 @@ igo-search-bar, igo-search-results igo-list, .baselayers { animation-fill-mode: forwards; } -.igo-baselayers-switcher-mobile { +.igo-baselayers-switcher-mobile, ::ng-deep .ol-scale-line { bottom: 48px!important; - left: 4px; + z-index: 1; } .igo-baselayers-switcher-mobile { - bottom: 48px!important; + left: 4px; +} + +::ng-deep .ol-scale-line { + left: 88px!important; // baselayers-switcher is 80px width +} + +.igo-baselayers-switcher { + z-index: 1; } ::ng-deep igo-search-bar { @@ -220,11 +270,11 @@ igo-search-bar svg { } /** search bar mobile **/ -app-bottomresult { +app-bottompanel { position: fixed; display: block; bottom: 0; - z-index: 100; + z-index: 9999; } @@ -237,6 +287,7 @@ igo-map-browser { } igo-map-browser ::ng-deep .ol-attribution { + display: none; left: 50px; bottom: $igo-margin; text-align: left; @@ -289,6 +340,7 @@ igo-map-browser.toast-offset-attribution ::ng-deep .ol-attribution { @include mobile { + display: none; bottom: 50px; } } @@ -305,9 +357,6 @@ igo-map-browser.toast-offset-attribution ::ng-deep div.ol-scale-line { position: absolute!important; display: block; - top: 20px!important; - bottom: unset; - left: 10px!important; background: unset!important; color: black; z-index: 5!important; @@ -352,7 +401,6 @@ igo-map-browser.toast-offset-attribution .map-buttons-mobile { display: flex; flex-direction: column; - margin-bottom: 5px; z-index: 10; position: absolute; bottom: 48px!important; @@ -360,18 +408,18 @@ igo-map-browser.toast-offset-attribution right: 4px; } -.igo-zoom-button-container, button, app-legend-button, igo-zoom-button, igo-offline-button, igo-geolocate-button, igo-home-extent-button, #app-legend-button-dialog, mat-dialog-container app-legend-button-dialog, app-legend-button-dialog { +.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; + box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); } -// all buttons become raised -::ng-deep igo-zoom-button { - box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); +::ng-deep app-legend-button button .mat-button-focus-overlay { // trick to prevent the button to stay focused when legend closes form sidepanel + opacity: 0 !important; } igo-zoom-button { @@ -380,15 +428,33 @@ igo-zoom-button { } } +// info-bulle arrows from panels +::ng-deep .sidepanel .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); + } + +::ng-deep .sidepanel { + position: absolute!important; + height: 100%; + } + //** ADJUSTMENTS TO FOOTER AND HEADER **// ::ng-deep .map-hasfooter { - height: calc( 100% - #{$footer-height} )!important; + height: calc( 100% - $footer-height)!important; } -::ng-deep #legend-button-dialog-container { - top: calc( #{$header-height-mobile} + 4px); - max-height: calc(100% - #{$header-height-mobile} - #{$footer-height} - 100px); +::ng-deep #legend-dialog-container, app-legend-dialog { + top: calc($header-height-mobile + 4px); + max-height: calc(100% - $header-height-mobile - $footer-height - 100px); } .sidenav { @@ -396,12 +462,16 @@ igo-zoom-button { // display: block; } +::ng-deep #legend-dialog-container, app-legend-dialog { + top: calc($header-height + 4px); + max-height: calc(100% - $header-height - $footer-height - 180px); +} + //map height .igo-attribution-offset { min-height: $map-min-height; } - ::ng-deep .igo-map-browser-target .ol-viewport { min-height: $map-min-height !important; } diff --git a/src/app/pages/portal/portal.component.ts b/src/app/pages/portal/portal.component.ts index 89aa4546..dfce456a 100644 --- a/src/app/pages/portal/portal.component.ts +++ b/src/app/pages/portal/portal.component.ts @@ -2,6 +2,7 @@ import { EntitiesAllService } from './../list/listServices/entities-all.service' import { Component, OnInit, + AfterContentInit, OnDestroy, ViewChild, ElementRef, @@ -11,8 +12,8 @@ import { SimpleChanges } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { Subscription, BehaviorSubject, combineLatest, of } from 'rxjs'; -import { debounceTime, take, skipWhile, first, distinctUntilChanged, tap } from 'rxjs/operators'; +import { Subscription, BehaviorSubject, of, skip } from 'rxjs'; +import { debounceTime, take, skipWhile, distinctUntilChanged, tap } from 'rxjs/operators'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import * as olProj from 'ol/proj'; import { MatPaginator } from '@angular/material/paginator'; @@ -21,7 +22,6 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import olFormatGeoJSON from 'ol/format/GeoJSON'; import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { ObjectUtils } from '@igo2/utils'; -import MapBrowserEvent from 'ol/MapBrowserEvent'; import type { default as OlGeometry } from 'ol/geom/Geometry'; import olFeature from 'ol/Feature'; @@ -32,7 +32,8 @@ import { ConfigService, LanguageService, MessageService, - StorageService + StorageService, + AnalyticsService } from '@igo2/core'; import { @@ -41,9 +42,7 @@ import { WorkspaceStore, ActionStore, EntityStore, - Toolbox, - Tool, - Widget, + getEntityTitle, EntityTablePaginatorOptions, EntityRecord } from '@igo2/common'; @@ -65,24 +64,26 @@ import { handleFileImportError, handleFileImportSuccess, WfsWorkspace, - FeatureWorkspace, - EditionWorkspace, generateIdFromSourceOptions, computeOlFeaturesExtent, - addStopToStore, ImageLayer, VectorLayer, MapExtent, IgoMap, DataSourceService, - QuerySearchSource, SearchSource, + QuerySearchSource, featureToSearchResult, QueryService, + Layer, + MapService, + SearchBarComponent, featureFromOl, WMSDataSource, OgcFilterWriter, - IgoOgcFilterObject + IgoOgcFilterObject, + FeatureWorkspace, + EditionWorkspace } from '@igo2/geo'; import { @@ -90,11 +91,11 @@ import { WorkspaceState, QueryState, ContextState, - SearchState, - ToolState, DirectionState } from '@igo2/integration'; +import { SearchState } from './panels/search-results-tool/search.state'; + import { PwaService } from '../../services/pwa.service'; import { @@ -119,10 +120,10 @@ import { ListEntitiesService } from '../list/listServices/list-entities-services ] }) -export class PortalComponent implements OnInit, OnDestroy { +export class PortalComponent implements OnInit, AfterContentInit, OnDestroy { @Input() features = {added: []}; //TODO update the type later when I know what it is.. @Output() workspaceSelected = new EventEmitter(); - @Output() mapQuery = new EventEmitter(); + @Output() mapQueryEvent = new EventEmitter(); @Input() additionalProperties: Map> = new Map(); @Input() entitiesAll: Array; //all entities @Input() entitiesList: Array; //filtered entities @@ -143,7 +144,8 @@ export class PortalComponent implements OnInit, OnDestroy { public hasFooter: boolean = true; public hasLegendButton: boolean = true; public hasGeolocateButton: boolean = true; - public hasExpansionPanel: boolean = false; + public showMenuButton = true; + public hasExpansionPanel: boolean = undefined; public hasToolbox: boolean = false; public workspaceNotAvailableMessage: String = 'workspace.disabled.resolution'; public workspaceEntitySortChange$: BehaviorSubject = new BehaviorSubject(false); @@ -154,20 +156,15 @@ export class PortalComponent implements OnInit, OnDestroy { public selectedWorkspace$: BehaviorSubject = new BehaviorSubject(undefined);; public hasSideSearch = true; public showSearchBar = true; - public showMenuButton = true; - public sidenavOpened$: BehaviorSubject = new BehaviorSubject(false); @ViewChild('mapBrowser', { read: ElementRef, static: true }) mapBrowser: ElementRef; - - public term: string; + public legendPanelOpened = false; + public legendDialogOpened = false; public settingsChange$ = new BehaviorSubject(undefined); getBaseLayersUseStaticIcon(): Boolean { return this.configService.getConfig('useStaticIcon'); } - - public toastPanelOffsetX$: BehaviorSubject = new BehaviorSubject(undefined); - public minSearchTermLength = 2; public hasHomeExtentButton = false; public hasFeatureEmphasisOnSelection: Boolean = false; public workspacePaginator: MatPaginator; @@ -188,6 +185,7 @@ export class PortalComponent implements OnInit, OnDestroy { public termSplitter = '|'; public termDefinedInUrlTriggered = false; private addedLayers$$: Subscription[] = []; + private layers$$: Subscription; public forceCoordsNA = false; public contextMenuStore = new ActionStore([]); @@ -195,14 +193,12 @@ export class PortalComponent implements OnInit, OnDestroy { private contextLoaded = false; private context$$: Subscription; - private openSidenav$$: Subscription; + private openPanels$$: Subscription; private sidenavMediaAndOrientation$$: Subscription; + private searchTerm$$: Subscription; public igoSearchPointerSummaryEnabled: boolean; - public toastPanelForExpansionOpened = true; - private activeWidget$$: Subscription; - public showToastPanelForExpansionToggle = false; private routeParams: Params; public toastPanelHtmlDisplay = false; public mobile: boolean; @@ -212,29 +208,17 @@ export class PortalComponent implements OnInit, OnDestroy { // public additionalProperties: Map> = new Map(); public additionalTypes: Array = []; private layerId: string; - @ViewChild('searchBar', { read: ElementRef, static: true }) - searchBar: ElementRef; - + @ViewChild('searchbar') searchBar: SearchBarComponent; public dialogOpened = this.dialog.getDialogById('legend-button-dialog-container'); get map(): IgoMap { return this.mapState.map; } - get sidenavOpened(): boolean { - return this.sidenavOpened$.value; - } - - set sidenavOpened(value: boolean) { - this.sidenavOpened$.next(value); - } - get auth(): AuthOptions { return this.configService.getConfig('auth') || []; } - // Responsiveness - isMobile(): boolean { return this.mediaService.getMedia() === Media.Mobile; } @@ -258,7 +242,7 @@ export class PortalComponent implements OnInit, OnDestroy { get backdropShown(): boolean { return ( ('(min-width: 768px)' && - this.sidenavOpened + this.panelOpenState )); } @@ -267,25 +251,13 @@ export class PortalComponent implements OnInit, OnDestroy { } set expansionPanelExpanded(value: boolean) { this.workspaceState.workspacePanelExpanded = value; - if (value === true) { + if (value) { this.map.viewController.setPadding({bottom: 280}); } else { this.map.viewController.setPadding({bottom: 0}); } } - get contextUri(): string { - return this.contextState.context$?.getValue() ? this.contextState.context$.getValue().uri : undefined; - } - - get toastPanelShown(): boolean { - return true; - } - - get expansionPanelBackdropShown(): boolean { - return this.expansionPanelExpanded && this.toastPanelForExpansionOpened; - } - get workspace(): Workspace { return this.workspaceState.workspace$.value; } @@ -306,17 +278,20 @@ export class PortalComponent implements OnInit, OnDestroy { return this.searchState.searchResultsGeometryEnabled$.value; } - get toolbox(): Toolbox { - return this.toolState.toolbox; - } - get workspaceStore(): WorkspaceStore { return this.workspaceState.store; } - get queryStore(): EntityStore { + get queryStore(): EntityStore { //FeatureInfo return this.queryState.store; } + public panelOpenState = false; + public mapQueryClick = false; + public searchInit = false; + + public mapLayersShownInLegend: Layer[]; + public legendInPanel: boolean; + public legendButtonTooltip = this.languageService.translate.instant('legend.open'); constructor( // public cdRef: ChangeDetectorRef, @@ -326,7 +301,6 @@ export class PortalComponent implements OnInit, OnDestroy { private additionalTypesService: FiltersAdditionalTypesService, private activeFilterService: FiltersActiveFiltersService, private filteredEntitiesService: FilteredEntitiesService, - private queryService: QueryService, private route: ActivatedRoute, public workspaceState: WorkspaceState, public authService: AuthService, @@ -336,9 +310,9 @@ export class PortalComponent implements OnInit, OnDestroy { public capabilitiesService: CapabilitiesService, private contextState: ContextState, private mapState: MapState, + private mapService: MapService, private searchState: SearchState, private queryState: QueryState, - private toolState: ToolState, private searchSourceService: SearchSourceService, private configService: ConfigService, private importService: ImportService, @@ -349,8 +323,10 @@ export class PortalComponent implements OnInit, OnDestroy { private storageService: StorageService, private directionState: DirectionState, public dialog: MatDialog, + public queryService: QueryService, private breakpointObserver: BreakpointObserver, - private pwaService: PwaService + private pwaService: PwaService, + private analyticsService: AnalyticsService ) { this.useEmbeddedVersion = this.configService.getConfig('embeddedVersion.useEmbeddedVersion') === undefined ? false : this.configService.getConfig('embeddedVersion.useEmbeddedVersion'); this.hasFooter = this.configService.getConfig('hasFooter') === undefined ? false : @@ -384,9 +360,13 @@ export class PortalComponent implements OnInit, OnDestroy { this.mobileBreakPoint = this.configService.getConfig('mobileBreakPoint') === undefined ? "'(min-width: 768px)'" : this.configService.getConfig('mobileBreakPoint'); this.layerId = this.configService.getConfig('embeddedVersion.layerId'); + this.hasHomeExtentButton = this.configService.getConfig('homeExtentButton') === undefined ? false : true; + this.legendInPanel = this.configService.getConfig('legendInPanel') === undefined ? true : + this.configService.getConfig('legendInPanel'); } ngOnInit() { + this.queryService.defaultFeatureCount = 1; this.map.status$.subscribe(value => { if(value === 1 && (this.showSimpleFeatureList || this.showSimpleFilters) && typeof this.layerId === 'string'){ console.log("SETTING WORKSPACE"); @@ -437,27 +417,43 @@ export class PortalComponent implements OnInit, OnDestroy { this.contextMenuStore.load(contextActions); - this.onSettingsChange$.subscribe(() => { - this.searchState.setSearchSettingsChange(); - }); - this.searchState.selectedResult$.subscribe((result) => { if (result && this.isMobile()) { - this.closeSidenav(); + this.closePanels(); + } + }); + + this.searchTerm$$ = this.searchState.searchTerm$.pipe(skip(1)).subscribe((searchTerm: string) => { + if (searchTerm !== undefined && searchTerm !== null) { + this.analyticsService.trackSearch(searchTerm, this.searchState.store.count); + } + }); + + this.queryService.defaultFeatureCount = 1; + + this.queryStore.entities$ + .subscribe( + (entities) => { + if (entities.length > 0) { + this.openPanelonQuery(); } }); this.workspaceState.workspaceEnabled$.next(this.hasExpansionPanel); this.workspaceState.store.empty$.subscribe((workspaceEmpty) => { - if (!this.hasExpansionPanel) { return; } - this.workspaceState.workspaceEnabled$.next(workspaceEmpty ? false : true); - if (workspaceEmpty) { - this.expansionPanelExpanded = false; + }); + + this.map.layers$.subscribe( layerList => { + for(let layer of layerList){ + if(layer.options.id === this.layerId){ + this.workspaceState.setActiveWorkspaceById(this.layerId); + this.expansionPanelExpanded = true; + break; + } } - this.updateMapBrowserClass(); }); this.map.layers$.subscribe( layerList => { @@ -493,43 +489,9 @@ export class PortalComponent implements OnInit, OnDestroy { } }); - this.activeWidget$$ = this.workspaceState.activeWorkspaceWidget$.subscribe( - (widget: Widget) => { - if (widget !== undefined) { - this.openToastPanelForExpansion(); - this.showToastPanelForExpansionToggle = true; - } else { - this.closeToastPanelForExpansion(); - this.showToastPanelForExpansionToggle = false; - } - } - ); - - this.openSidenav$$ = this.toolState.openSidenav$.subscribe( - (openSidenav: boolean) => { - if (openSidenav) { - this.openSidenav(); - this.toolState.openSidenav$.next(false); - } - } - ); - this.activeFilterService.onEvent().subscribe(activeFilters => { this.applyFilters(activeFilters); }); - - - this.sidenavMediaAndOrientation$$ = - combineLatest([ - this.sidenavOpened$, - this.mediaService.media$, - this.mediaService.orientation$] - ).pipe( - debounceTime(50) - ).subscribe(() => { - this.computeToastPanelOffsetX(); - }); - this.additionalPropertiesService.onEvent().subscribe(additionalProperties => this.additionalProperties = additionalProperties); this.additionalTypesService.onEvent().subscribe(additionalTypes => this.additionalTypes = additionalTypes); this.entitiesAllService.onEvent().subscribe(entitiesAll => this.entitiesAll = entitiesAll); @@ -544,8 +506,68 @@ export class PortalComponent implements OnInit, OnDestroy { ); } + ngAfterContentInit(): void { + this.map.viewController.setInitialState(); + } + + toggleDialogLegend(){ + if (!this.legendDialogOpened) { + this.legendDialogOpened = true; + } + } + + toggleLegend(){ + if (this.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(); + } + } + else { + if (!this.legendDialogOpened) { + this.legendDialogOpened = true; + } + } + } + + 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() { - if(this.breakpointObserver.isMatched('(min-width: 768px)')) { // this.mobileBreakPoint is used before its initialization + if(this.breakpointObserver.isMatched('(min-width: 768px)')) { this.currentBreakpoint = this.mobileBreakPoint; this.mobile = false; } else { @@ -560,13 +582,62 @@ export class PortalComponent implements OnInit, OnDestroy { distinctUntilChanged() ); - computeToastPanelOffsetX() { - if (this.isMobile() || !this.isLandscape()) { - Promise.resolve().then(() => this.toastPanelOffsetX$.next(undefined)); - } else { - Promise.resolve().then(() => this.toastPanelOffsetX$.next(this.getToastPanelExtent())); + /* + private initSW() { + const dataDownload = this.configService.getConfig('pwa.dataDownload'); + if ('serviceWorker' in navigator && dataDownload) { + let downloadMessage; + let currentVersion; + const dataLoadSource = this.storageService.get('dataLoadSource'); + navigator.serviceWorker.ready.then((registration) => { + console.log('Service Worker Ready'); + this.http.get('ngsw.json').pipe( + concatMap((ngsw: any) => { + const datas$ = []; + let hasDataInDataDir: boolean = false; + if (ngsw) { + // IF FILE NOT IN THIS LIST... DELETE? + currentVersion = ngsw.appData.version; + const cachedDataVersion = this.storageService.get('cachedDataVersion'); + if (currentVersion !== cachedDataVersion && dataLoadSource === 'pending') { + this.pwaService.updates.checkForUpdate(); + } + if (dataLoadSource === 'newVersion' || !dataLoadSource) { + ((ngsw as any).assetGroups as any).map((assetGroup) => { + if (assetGroup.name === 'contexts') { + const elemToDownload = assetGroup.urls.concat(assetGroup.files).filter(f => f); + elemToDownload.map((url, i) => datas$.push(this.http.get(url).pipe(delay(750)))); + } + }); + if (hasDataInDataDir) { + const message = this.languageService.translate.instant('pwa.data-download-start'); + downloadMessage = this.messageService + .info(message, undefined, { disableTimeOut: true, progressBar: false, closeButton: true, tapToDismiss: false }); + this.storageService.set('cachedDataVersion', currentVersion); + } + return zip(...datas$); + } + + } + return zip(...datas$); + }) + ) + .pipe(delay(1000)) + .subscribe(() => { + if (downloadMessage) { + this.messageService.remove((downloadMessage as any).toastId); + const message = this.languageService.translate.instant('pwa.data-download-completed'); + this.messageService.success(message, undefined, { timeOut: 40000 }); + if (currentVersion) { + this.storageService.set('dataLoadSource', 'pending'); + this.storageService.set('cachedDataVersion', currentVersion); + } + } + }); + + }); } - } + }*/ createFeatureProperties(layer: ImageLayer | VectorLayer) { let properties = {}; @@ -636,82 +707,9 @@ export class PortalComponent implements OnInit, OnDestroy { ngOnDestroy() { this.context$$.unsubscribe(); - this.activeWidget$$.unsubscribe(); - this.openSidenav$$.unsubscribe(); this.workspaceMaximize$$.map(f => f.unsubscribe()); - this.sidenavMediaAndOrientation$$.unsubscribe(); - } - - removeFeatureFromMap() { - this.map.searchResultsOverlay.clear(); - } - - /** - * Cancel ongoing add layer, if any - */ - private cancelOngoingAddLayer() { - this.addedLayers$$.forEach((sub: Subscription) => sub.unsubscribe()); - this.addedLayers$$ = []; - } - - onBackdropClick() { - this.closeSidenav(); - } - - onToggleSidenavClick() { - this.toggleSidenav(); - } - - closeToastPanelForExpansion() { - this.toastPanelForExpansionOpened = false; - } - - openToastPanelForExpansion() { - this.toastPanelForExpansionOpened = true; - } - - onMapQuery(event: { features: Feature[]; event: MapBrowserEvent }) { - // the event.features array contains duplicates, so they must be removed - event.features = event.features.reduce((unique, item) => { - const hasDuplicate = unique.some((existingItem) => { - return JSON.stringify(existingItem["properties"]) === JSON.stringify(item["properties"]); - }); - - if (!hasDuplicate) { - unique.push(item); - } - - return unique; - }, []); - if(event.features.length > 0) this.mapQuery.emit(event.features); - const baseQuerySearchSource = this.getQuerySearchSource(); - const querySearchSourceArray: QuerySearchSource[] = []; - const results = event.features.map((feature: Feature) => { - let querySearchSource = querySearchSourceArray.find( - (s) => s.title === feature.meta.sourceTitle - ); - if (this.getFeatureIsSameActiveWks(feature)) { - if (this.getWksActiveOpenInResolution() && !(this.workspace as WfsWorkspace).getLayerWksOptionMapQuery()) { - return; - } - } - 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); - }); + this.layers$$?.unsubscribe(); + this.searchTerm$$.unsubscribe(); } private getQuerySearchSource(): SearchSource { @@ -746,7 +744,121 @@ export class PortalComponent implements OnInit, OnDestroy { return false; } + onMapQuery(event: { features: Feature[]; event: MapBrowserEvent }) { + if(this.useEmbeddedVersion) { + // the event.features array contains duplicates, so they must be removed + event.features = event.features.reduce((unique, item) => { + const hasDuplicate = unique.some((existingItem) => { + return JSON.stringify(existingItem["properties"]) === JSON.stringify(item["properties"]); + }); + + if (!hasDuplicate) { + unique.push(item); + } + + return unique; + }, []); + if(event.features.length > 0) this.mapQueryEvent.emit(event.features); + const baseQuerySearchSource = this.getQuerySearchSource(); + const querySearchSourceArray: QuerySearchSource[] = []; + const results = event.features.map((feature: Feature) => { + let querySearchSource = querySearchSourceArray.find( + (s) => s.title === feature.meta.sourceTitle + ); + if (this.getFeatureIsSameActiveWks(feature)) { + if (this.getWksActiveOpenInResolution() && !(this.workspace as WfsWorkspace).getLayerWksOptionMapQuery()) { + return; + } + } + 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 { + if(this.configService.getConfig('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 (this.getFeatureIsSameActiveWks(feature)) { + if (this.getWksActiveOpenInResolution() && !(this.workspace as WfsWorkspace).getLayerWksOptionMapQuery()) { + return; + } + } + 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; + } + } + } + } + + /** + * Cancel ongoing add layer, if any + */ + private cancelOngoingAddLayer() { + this.addedLayers$$.forEach((sub: Subscription) => sub.unsubscribe()); + this.addedLayers$$ = []; + } + + onBackdropClick() { + this.closePanels(); + this.mapQueryClick = false; + } + onSearchTermChange(term?: string) { + if(this.mobile) {this.panelOpenState = true;} if (this.routeParams?.search && term !== this.routeParams.search) { this.searchState.deactivateCustomFilterTermStrategy(); } @@ -754,13 +866,26 @@ export class PortalComponent implements OnInit, OnDestroy { this.searchState.setSearchTerm(term); const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); if (termWithoutHashtag.length < 2) { - this.onClearSearch(); + if(this.mobile) {this.panelOpenState = true;} + this.clearSearch(); 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); @@ -786,29 +911,14 @@ export class PortalComponent implements OnInit, OnDestroy { this.searchStore.updateMany(newResults); } - onSearchSettingsChange() { - this.onSettingsChange$.next(true); - } - - private closeSidenav() { - this.sidenavOpened = false; - this.map.viewController.padding[3] = 0; - } - - private openSidenav() { - this.sidenavOpened = true; - this.map.viewController.padding[3] = this.isMobile() ? 0 : 400; - } - - private toggleSidenav() { - this.sidenavOpened ? this.closeSidenav() : this.openSidenav(); - this.computeToastPanelOffsetX(); + private closePanels() { + if (!this.mapQueryClick && !this.searchInit && !this.legendPanelOpened){ + this.panelOpenState = false; + } } - public toolChanged(tool: Tool) { - if (tool && tool.name === 'searchResults' && this.searchBar) { - this.searchBar.nativeElement.getElementsByTagName('input')[0].focus(); - } + private openPanels() { + this.panelOpenState = true; } private computeHomeExtentValues(context: DetailedContext) { @@ -837,53 +947,51 @@ export class PortalComponent implements OnInit, OnDestroy { } this.computeHomeExtentValues(context); - this.route.queryParams.pipe(debounceTime(250)).subscribe((qParams) => { if (!qParams['context'] || qParams['context'] === context.uri) { this.readLayersQueryParams(qParams); } }); - if (this.contextLoaded) { - const contextManager = this.toolbox.getTool('contextManager'); - const contextManagerOptions = contextManager - ? contextManager.options - : {}; - let toolToOpen = contextManagerOptions.toolToOpenOnContextChange; - - if (!toolToOpen) { - const toolOrderToOpen = ['mapTools', 'map', 'mapDetails', 'mapLegend']; - for (const toolName of toolOrderToOpen) { - if (this.toolbox.getTool(toolName)) { - toolToOpen = toolName; - break; - } - } - } - - if (toolToOpen) { - this.toolbox.activateTool(toolToOpen); - } - } - this.contextLoaded = true; } private onBeforeSearch() { - if ( - !this.toolbox.activeTool$.value || - this.toolbox.activeTool$.value.name !== 'searchResults' - ) { - this.toolbox.activateTool('searchResults'); - } - this.openSidenav(); + this.openPanels(); } - public onClearSearch() { + clearSearch() { this.map.searchResultsOverlay.clear(); this.searchStore.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 + } + + getTitle(result: SearchResult) { + return getEntityTitle(result); } onContextMenuOpen(event: { x: number; y: number }) { @@ -920,120 +1028,15 @@ export class PortalComponent implements OnInit, OnDestroy { this.searchBarTerm = coord.map((c) => c.toFixed(6)).join(', '); } - updateMapBrowserClass() { - const header = this.queryState.store.entities$.value.length > 0; - if (this.hasExpansionPanel && this.workspaceState.workspaceEnabled$.value) { - this.mapBrowser.nativeElement.classList.add('has-expansion-panel'); - } else { - this.mapBrowser.nativeElement.classList.remove('has-expansion-panel'); - } - - if (this.hasExpansionPanel && this.expansionPanelExpanded) { - if (this.workspaceMaximize$.value) { - this.mapBrowser.nativeElement.classList.add('expansion-offset-maximized'); - this.mapBrowser.nativeElement.classList.remove('expansion-offset'); - } else { - this.mapBrowser.nativeElement.classList.add('expansion-offset'); - this.mapBrowser.nativeElement.classList.remove('expansion-offset-maximized'); - } - } else { - if (this.workspaceMaximize$.value) { - this.mapBrowser.nativeElement.classList.remove('expansion-offset-maximized'); - } else { - this.mapBrowser.nativeElement.classList.remove('expansion-offset'); - } - } - - if (this.sidenavOpened) { - this.mapBrowser.nativeElement.classList.add('sidenav-offset'); - } else { - this.mapBrowser.nativeElement.classList.remove('sidenav-offset'); - } - - if (this.sidenavOpened && !this.isMobile()) { - this.mapBrowser.nativeElement.classList.add('sidenav-offset-baselayers'); - } else { - this.mapBrowser.nativeElement.classList.remove( - 'sidenav-offset-baselayers' - ); - } - - if ( - header && - (this.isMobile() || this.isTablet() || this.sidenavOpened) && - !this.expansionPanelExpanded - ) { - this.mapBrowser.nativeElement.classList.add('toast-offset-attribution'); - } else { - this.mapBrowser.nativeElement.classList.remove( - 'toast-offset-attribution' - ); - } - } - - getToastPanelExtent() { - if (!this.sidenavOpened) { - if (this.toastPanelHtmlDisplay && this.mediaService.isDesktop()) { - return 'htmlDisplay'; - } - if (this.fullExtent) { - return 'fullStandard'; - } else { - return 'standard'; - } - } else if (this.sidenavOpened) { - if (this.toastPanelHtmlDisplay && this.mediaService.isDesktop()) { - return 'htmlDisplayOffsetX'; - } - if (this.fullExtent) { - return 'fullOffsetX'; - } else { - return 'standardOffsetX'; - } - } - } - onPointerSummaryStatusChange(value) { this.storageService.set('searchPointerSummaryEnabled', value); this.igoSearchPointerSummaryEnabled = value; } - // getToastPanelStatus() { - // if (this.isMobile() === true && this.toastPanelOpened === false) { - // if (this.sidenavOpened === false) { - // if (this.expansionPanelExpanded === false) { - // if (this.queryState.store.entities$.value.length > 0) { - // return 'low'; - // } - // } - // } - // } - // } - - // get toastPanelOpened(): boolean { - // return this._toastPanelOpened; - // } - // set toastPanelOpened(value: boolean) { - // if (value !== !this._toastPanelOpened) { - // return; - // } - // this._toastPanelOpened = value; - // this.cdRef.detectChanges(); - // } - // private _toastPanelOpened = - // (this.storageService.get('toastOpened') as boolean) !== false; - - - getControlsOffsetY() { - return this.expansionPanelExpanded ? - this.workspaceMaximize$.value ? 'firstRowFromBottom-expanded-maximized' : 'firstRowFromBottom-expanded' : - 'firstRowFromBottom'; - } private readQueryParams() { this.route.queryParams.subscribe((params) => { this.routeParams = params; - this.readToolParams(); this.readSearchParams(); this.readFocusFirst(); this.computeZoomToExtent(); @@ -1059,6 +1062,12 @@ export class PortalComponent implements OnInit, OnDestroy { } } + getControlsOffsetY() { + return this.expansionPanelExpanded ? + this.workspaceMaximize$.value ? 'firstRowFromBottom-expanded-maximized' : 'firstRowFromBottom-expanded' : + 'firstRowFromBottom'; + } + private computeFocusFirst() { setTimeout(() => { const resultItem: any = document @@ -1122,73 +1131,6 @@ export class PortalComponent implements OnInit, OnDestroy { } } - private readToolParams() { - if (this.routeParams['tool']) { - this.matDialogRef$.pipe( - skipWhile(r => r !== undefined), - first() - ).subscribe(matDialogOpened => { - if (!matDialogOpened) { - this.toolbox.activateTool(this.routeParams['tool']); - } - }); - } - - if (this.routeParams['sidenav'] === '1') { - setTimeout(() => { - this.openSidenav(); - }, 250); - } - - if (this.routeParams['routing']) { - let routingCoordLoaded = false; - const stopCoords = this.routeParams['routing'].split(';'); - const routingOptions = this.routeParams['routingOptions']; - let resultSelection: number; - if (routingOptions) { - resultSelection = parseInt(routingOptions.split('result:')[1], 10); - } - this.directionState.stopsStore.storeInitialized$ - .pipe(skipWhile(init => !init), first()) - .subscribe((init: boolean) => { - if (init && !routingCoordLoaded) { - routingCoordLoaded = true; - stopCoords.map((coord, i) => { - if (i > 1) { - addStopToStore(this.directionState.stopsStore); - } - }); - setTimeout(() => { - stopCoords.map((coord, i) => { - const stop = this.directionState.stopsStore.all().find(e => e.position === i); - stop.text = coord; - stop.coordinates = coord.split(','); - this.directionState.stopsStore.update(stop); - }); - }, this.directionState.debounceTime * 1.25); // this delay is due to the default component debounce time - } - }); - // zoom to active route - this.directionState.routesFeatureStore.count$ - .pipe(skipWhile(c => c < 1), first()) - .subscribe(c => { - if (c >= 1) { - this.directionState.zoomToActiveRoute$.next(); - } - }); - // select the active route by url controls - this.directionState.routesFeatureStore.count$ - .pipe(skipWhile(c => c < 2), first()) - .subscribe(() => { - if (resultSelection) { - this.directionState.routesFeatureStore.entities$.value.map(d => d.properties.active = false); - this.directionState.routesFeatureStore.entities$.value[resultSelection].properties.active = true; - this.directionState.zoomToActiveRoute$.next(); - } - }); - } - } - private readLayersQueryParams(params: Params) { this.readLayersQueryParamsByType(params, 'wms'); this.readLayersQueryParamsByType(params, 'wmts'); diff --git a/src/app/pages/portal/portal.module.ts b/src/app/pages/portal/portal.module.ts index b0761b47..6059394c 100644 --- a/src/app/pages/portal/portal.module.ts +++ b/src/app/pages/portal/portal.module.ts @@ -1,3 +1,4 @@ +import { LegendButtonModule } from './legend-button/legend-button.module'; import { IgoSimpleFiltersModule } from './../filters/simple-filters.module'; import { IgoSimpleFeatureListModule } from './../list/simple-feature-list.module'; import { NgModule } from '@angular/core'; @@ -5,9 +6,9 @@ import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; -import { MatSidenavModule } from '@angular/material/sidenav'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatDialogModule } from '@angular/material/dialog'; +import { LegendDialogModule} from './legend-dialog/legend-dialog.module'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; @@ -31,6 +32,7 @@ import { IgoImportExportModule, IgoMapModule, IgoQueryModule, + IgoLayerModule, IgoSearchModule } from '@igo2/geo'; import { @@ -38,14 +40,13 @@ import { IgoContextMapButtonModule } from '@igo2/context'; -import { IgoIntegrationModule } from '@igo2/integration'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { IgoAppSearchBarModule, IgoIntegrationModule } from '@igo2/integration'; import { MapOverlayModule } from './map-overlay/map-overlay.module'; -import { AppSidenavModule } from './sidenav/sidenav.module'; import { PortalComponent } from './portal.component'; - -import { LegendButtonModule } from './legend-button/legend-button.module'; -import { AppSideResultModule } from './sideresult/sideresult.module'; +import { FooterModule } from './../footer/footer.module'; +import { AppPanelsModule } from './panels/panels.module'; @NgModule({ imports: [ @@ -61,30 +62,36 @@ import { AppSideResultModule } from './sideresult/sideresult.module'; MatSelectModule, IgoCoreModule, IgoFeatureModule, - IgoImportExportModule, IgoMapModule, - IgoQueryModule.forRoot(), - IgoSearchModule.forRoot(), - IgoActionModule, - IgoWorkspaceModule, IgoEntityModule, - IgoGeoWorkspaceModule, - IgoPanelModule, - IgoToolModule, - IgoContextMenuModule, - IgoBackdropModule, IgoFlexibleModule, IgoIntegrationModule, - AppSidenavModule, - AppSideResultModule, - MapOverlayModule, IgoContextManagerModule, + FooterModule, + IgoLayerModule, + IgoWorkspaceModule, + IgoGeoWorkspaceModule, + IgoQueryModule.forRoot(), + IgoSearchModule.forRoot(), IgoContextMapButtonModule, - LegendButtonModule, + MatDialogModule, + IgoActionModule, + IgoImportExportModule, + MapOverlayModule, + AppPanelsModule, + IgoPanelModule, + IgoBackdropModule, + IgoContextMenuModule, + IgoToolModule, IgoEntityTableModule, IgoEntityTablePaginatorModule, + LegendDialogModule, + MatSidenavModule, + IgoAppSearchBarModule, + IgoSearchModule, + LegendButtonModule ], - exports: [PortalComponent], - declarations: [PortalComponent] + exports: [PortalComponent], + declarations: [PortalComponent] }) export class PortalModule {} diff --git a/src/app/pages/portal/portal.variables.scss b/src/app/pages/portal/portal.variables.scss index 2df06880..cfd54dae 100644 --- a/src/app/pages/portal/portal.variables.scss +++ b/src/app/pages/portal/portal.variables.scss @@ -9,6 +9,7 @@ $menu-height-mobile: 40px; $header-height: 72px; $header-height-mobile: 72px; $footer-height: 29px; +$search-bar-height: 60px; $portal-height-hasHeader-mobile: calc( 100% - #{$header-height-mobile}); $portal-height-hasFooter-hasHeader-mobile: calc( 100% - #{$footer-height} - #{$header-height-mobile} ); @@ -35,7 +36,7 @@ $app-footer-height: 48px; $app-mobile-min-space-right: 40px; $app-sidenav-margin-top: 60px; -$app-sidenav-height: calc(100% - #{$footer-height} - 4px); +$app-sidenav-height: calc(100% - #{$footer-height}); $app-sidenav-content-height: calc(100% - #{$app-sidenav-margin-top} - #{$footer-height}); $app-sidenav-width: 400px; $app-sidenav-width-mobile: calc(100% - #{$app-mobile-min-space-right} - #{$igo-margin}); diff --git a/src/app/pages/portal/sidenav/sidenav.component.html b/src/app/pages/portal/sidenav/sidenav.component.html deleted file mode 100644 index 84fd22f7..00000000 --- a/src/app/pages/portal/sidenav/sidenav.component.html +++ /dev/null @@ -1,37 +0,0 @@ - - disableClose=false - -
- -
- - - - - - - - - - - -
-
-
diff --git a/src/app/pages/portal/sidenav/sidenav.component.scss b/src/app/pages/portal/sidenav/sidenav.component.scss deleted file mode 100644 index 3444532d..00000000 --- a/src/app/pages/portal/sidenav/sidenav.component.scss +++ /dev/null @@ -1,54 +0,0 @@ -@import '../portal.variables'; - -:host { - background-color: rgb(255, 255, 255); -} - -// This is needed because whe using the -// sidenav "side" mode, the z-index is 1 -// and the sidenav appears below our backdrop. -// The "side" mode is required to prevent -// the sidenav from focusing a random button on open. -:host ::ng-deep mat-sidenav { - z-index: 3 !important; -} - -mat-sidenav { - @extend %box-shadowed-bottom-right; - - height: 100%; - width: $app-sidenav-width; - - @include mobile { - width: $app-sidenav-width-mobile; - max-width: 400px; - } -} - -.app-content, igo-panel { - height: 100%; -} - -.app-sidenav-content { - margin-top: $app-sidenav-margin-top; - height: $app-sidenav-height; - @include mobile { - height: calc(100% - #{$app-sidenav-margin-top}); - } -} - -igo-panel ::ng-deep .igo-panel-content { - position: relative; -} - - -div.toolActivated > igo-panel ::ng-deep > div.igo-panel-header { - margin-left: 50px; - position: relative; -} - -igo-toolbox { - @include mobile { - overflow: auto; - } -} diff --git a/src/app/pages/portal/sidenav/sidenav.component.ts b/src/app/pages/portal/sidenav/sidenav.component.ts deleted file mode 100644 index cb04621a..00000000 --- a/src/app/pages/portal/sidenav/sidenav.component.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - Component, - Input, - Output, - OnInit, - OnDestroy, - EventEmitter, - ChangeDetectionStrategy -} from '@angular/core'; - -import { BehaviorSubject, Subscription } from 'rxjs'; - -import { Tool, Toolbox } from '@igo2/common'; -import { IgoMap } from '@igo2/geo'; -import { ToolState, CatalogState } from '@igo2/integration'; -import { ConfigService } from '@igo2/core'; - -@Component({ - selector: 'app-sidenav', - templateUrl: './sidenav.component.html', - styleUrls: ['./sidenav.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SidenavComponent implements OnInit, OnDestroy { - title$: BehaviorSubject = new BehaviorSubject(undefined); - - private activeTool$$: Subscription; - - @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(); - @Output() toolChange = new EventEmitter(); - - get toolbox(): Toolbox { - return this.toolState.toolbox; - } - - constructor( - private toolState: ToolState, - private configService: ConfigService, - private catalogState: CatalogState) {} - - ngOnInit() { - this.activeTool$$ = this.toolbox.activeTool$.subscribe((tool: Tool) => { - const sidenavTitle = this.configService.getConfig('sidenavTitle') || 'IGO'; - if (tool) { - if (tool.name === 'catalogBrowser') { - for (const catalog of this.catalogState.catalogStore.all()) { - if (this.catalogState.catalogStore.state.get(catalog).selected === true) { - this.title$.next(catalog.title); - } - } - } else if (tool.name === 'activeTimeFilter' || tool.name === 'activeOgcFilter') { - for (const layer of this.map.layers) { - if (layer.options.active === true) { - this.title$.next(layer.title); - } - } - } else { - this.title$.next(tool.title); - } - } else { - this.title$.next(sidenavTitle); - } - this.toolChange.emit(tool); - }); - } - - ngOnDestroy() { - this.activeTool$$.unsubscribe(); - } - - onPreviousButtonClick() { - this.toolbox.activatePreviousTool(); - } - - onUnselectButtonClick() { - this.toolbox.deactivateTool(); - } -} diff --git a/src/app/pages/portal/sidenav/sidenav.module.ts b/src/app/pages/portal/sidenav/sidenav.module.ts deleted file mode 100644 index 6d103eaf..00000000 --- a/src/app/pages/portal/sidenav/sidenav.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatTooltipModule } from '@angular/material/tooltip'; - -import { IgoLanguageModule } from '@igo2/core'; -import { - IgoPanelModule, - IgoFlexibleModule, - IgoToolModule, - IgoHomeButtonModule -} from '@igo2/common'; -import { IgoFeatureModule } from '@igo2/geo'; -import { IgoContextManagerModule } from '@igo2/context'; - -import { SidenavComponent } from './sidenav.component'; - -@NgModule({ - imports: [ - CommonModule, - MatIconModule, - MatButtonModule, - MatSidenavModule, - MatTooltipModule, - IgoLanguageModule, - IgoPanelModule, - IgoFlexibleModule, - IgoContextManagerModule, - IgoToolModule, - IgoFeatureModule, - IgoHomeButtonModule - ], - exports: [SidenavComponent], - declarations: [SidenavComponent] -}) -export class AppSidenavModule {} diff --git a/src/app/pages/portal/sidenav/sidenav.theming.scss b/src/app/pages/portal/sidenav/sidenav.theming.scss deleted file mode 100644 index 725b79ac..00000000 --- a/src/app/pages/portal/sidenav/sidenav.theming.scss +++ /dev/null @@ -1,5 +0,0 @@ -@mixin app-sidenav-theming($theme) { - $primary: map-get($theme, primary); - $accent: map-get($theme, accent); - $foreground: map-get($theme, foreground); -} diff --git a/src/app/pages/portal/sideresult/bottomresult.component.html b/src/app/pages/portal/sideresult/bottomresult.component.html deleted file mode 100644 index 54428f5b..00000000 --- a/src/app/pages/portal/sideresult/bottomresult.component.html +++ /dev/null @@ -1,39 +0,0 @@ -
- - - - - - - - - - -
\ No newline at end of file diff --git a/src/app/pages/portal/sideresult/bottomresult.component.scss b/src/app/pages/portal/sideresult/bottomresult.component.scss deleted file mode 100644 index 93cf3d91..00000000 --- a/src/app/pages/portal/sideresult/bottomresult.component.scss +++ /dev/null @@ -1,48 +0,0 @@ -@import '../portal.variables'; - -: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: 1rem; -} - -::ng-deep app-bottomresult .mat-expansion-panel-content { - overflow-y: scroll!important; - height: 250px; - width: 100%; -} - -#bottomResultMobile { - position: relative; - display: block; - bottom: 0; -} diff --git a/src/app/pages/portal/sideresult/bottomresult.component.ts b/src/app/pages/portal/sideresult/bottomresult.component.ts deleted file mode 100644 index ddae8fda..00000000 --- a/src/app/pages/portal/sideresult/bottomresult.component.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { - Component, - Input, - Output, - OnInit, - OnDestroy, - EventEmitter, - ChangeDetectionStrategy, - ElementRef, - ViewChild -} from '@angular/core'; - -import * as proj from 'ol/proj'; -import { LanguageService, MediaService } from '@igo2/core'; -import { EntityStore, ActionStore } from '@igo2/common'; -import { BehaviorSubject } from 'rxjs'; - -import { - IgoMap, - FEATURE, - Feature, - FeatureMotion, - GoogleLinks, - LayerService, - MapService, - Layer, - ProjectionService, - Research, - SearchResult, - SearchService -} from '@igo2/geo'; -import { CatalogState, SearchState } from '@igo2/integration'; -import { ConfigService } from '@igo2/core'; - -@Component({ - selector: 'app-bottomresult', - templateUrl: './bottomresult.component.html', - styleUrls: ['./bottomresult.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class BottomResultComponent implements OnInit, OnDestroy { - title$: BehaviorSubject = new BehaviorSubject(undefined); - @Input() hasSearchQuery: boolean; - - @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(); - - // SEARCH - events: string[] = []; - public showMenuButton: boolean; - public store = new ActionStore([]); - public showSearchBar: boolean; - public igoSearchPointerSummaryEnabled: boolean = false; - public panelOpenState: boolean; - public termSplitter: string = '|'; - - public view = { - center: [-73, 47.2], - zoom: 7 - }; - - public osmLayer: Layer; - - @ViewChild('mapBrowser', { read: ElementRef, static: true }) mapBrowser: ElementRef; - - public lonlat; - public mapProjection: string; - public term: string; - public settingsChange$ = new BehaviorSubject(undefined); - - get searchStore(): EntityStore { - return this.searchState.store; - } - - get isTouchScreen(): boolean { - return this.mediaService.isTouchScreen(); - } - - public selectedFeature: Feature; - - constructor( - private configService: ConfigService, - private catalogState: CatalogState, - //SEARCH - private languageService: LanguageService, - private projectionService: ProjectionService, - private mapService: MapService, - private layerService: LayerService, - private searchState: SearchState, - private searchService: SearchService, - private mediaService: MediaService, - private elRef: ElementRef - ) { - // SEARCH - this.mapService.setMap(this.map); - this.showSearchBar = this.configService.getConfig('showSearchBar') === undefined ? true : - this.configService.getConfig('showSearchBar'); - - this.layerService - .createAsyncLayer({ - title: 'OSM', - sourceOptions: { - type: 'osm' - } - }) - .subscribe(layer => { - this.osmLayer = layer; - //this.map.addLayer(layer); - }); - } - - //SEARCH - - onPointerSummaryStatusChange(value) { - this.igoSearchPointerSummaryEnabled = value; - } - - onSearchTermChange(term = '') { - this.term = term; - const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); - if (termWithoutHashtag.length < 2) { - this.searchStore.clear(); - this.selectedFeature = undefined; - this.panelOpenState = true; - } - } - - onSearch(event: { research: Research; results: SearchResult[] }) { - console.log('onSearch'); - 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); - } - - onSearchSettingsChange() { - this.settingsChange$.next(true); - } - - /** - * 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 - ); - } - - ngOnInit() { - this.store.load([ - { - id: 'coordinates', - title: 'coordinates', - handler: this.onSearchCoordinate.bind(this) - }, - { - id: 'googleMaps', - title: 'googleMap', - handler: this.onOpenGoogleMaps.bind(this), - args: ['1'] - }, - { - id: 'googleStreetView', - title: 'googleStreetView', - handler: this.onOpenGoogleStreetView.bind(this) - } - ]); - } - - ngOnDestroy() { - this.store.destroy(); - } - - //SEARCH - /* - * Remove a feature to the map overlay - */ - removeFeatureFromMap() { - this.map.searchResultsOverlay.clear(); - } - - onContextMenuOpen(event: { x: number; y: number }) { - const position = this.mapPosition(event); - const coord = this.mapService.getMap().ol.getCoordinateFromPixel(position); - this.mapProjection = this.mapService.getMap().projection; - this.lonlat = proj.transform(coord, this.mapProjection, 'EPSG:4326'); - } - - mapPosition(event: { x: number; y: number }) { - const contextmenuPoint = event; - contextmenuPoint.y = - contextmenuPoint.y - - this.mapBrowser.nativeElement.getBoundingClientRect().top + - window.scrollY; - contextmenuPoint.x = - contextmenuPoint.x - - this.mapBrowser.nativeElement.getBoundingClientRect().left + - window.scrollX; - const position = [contextmenuPoint.x, contextmenuPoint.y]; - return position; - } - - onPointerSearch(event) { - this.lonlat = event; - this.onSearchCoordinate(); - } - - 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 }); - }); - } - } - } - - onOpenGoogleMaps() { - window.open(GoogleLinks.getGoogleMapsCoordLink(this.lonlat[0], this.lonlat[1])); - } - - onOpenGoogleStreetView() { - window.open( - GoogleLinks.getGoogleStreetViewLink(this.lonlat[0], this.lonlat[1]) - ); - } - -} diff --git a/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.html b/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.html deleted file mode 100644 index 4c580613..00000000 --- a/src/app/pages/portal/sideresult/search-results-tool/search-results-tool.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
-
-
{{ 'igo.integration.searchResultsTool.noResults' | translate }}
-
{{ 'igo.integration.searchResultsTool.doSearch' | translate }}
-
-
-
- - - -
- - - - - - -
- - - -
diff --git a/src/app/pages/portal/sideresult/sideresult.component.html b/src/app/pages/portal/sideresult/sideresult.component.html deleted file mode 100644 index 77dd4ebf..00000000 --- a/src/app/pages/portal/sideresult/sideresult.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
- - -
- - - - -
-
- -
\ No newline at end of file diff --git a/src/app/pages/portal/sideresult/sideresult.component.scss b/src/app/pages/portal/sideresult/sideresult.component.scss deleted file mode 100644 index bd688d55..00000000 --- a/src/app/pages/portal/sideresult/sideresult.component.scss +++ /dev/null @@ -1,93 +0,0 @@ -@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; - - @include mobile { - width: $app-sidenav-width-mobile; - max-width: 400px; - } - overflow: visible; -} - -.app-content, igo-panel { - height: 100%; -} - -.app-sideresult-content { - height: 100%; - display: block; - position: relative; - overflow: hidden; -} - -app-sideresult { - width: $app-sidenav-width; -} - -::ng-deep 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 * #{$igo-margin}); - top: calc(2 * #{$igo-margin}); - height: 100%; -} - -#sideresult-button { - display: block; - position: absolute; - top: 50%; - left:calc(#{$app-sidenav-width} + 1px); - z-index: 1; - padding: 0; - min-width: 12px!important; - animation-duration: 3s; - visibility: visible; - opacity: 1; - opacity: 0; - animation: fadeIn 0.9s; - animation-delay: 0.5s; - animation-fill-mode: forwards; - width: 24px; -} - -.sideresult-closed { -left: 0!important; -position: absolute; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -/*mat-sidenav::backdrop { - z-index: 999; -}*/ - -app-search-results-tool { - width: 100%; - height: 100%; - padding: 0 16px; - display: flex; - padding-top: 60px; -} - -::ng-deep app-search-results-tool > div > section > h4 > strong { - font-size: 21px!important; -} \ No newline at end of file diff --git a/src/app/pages/portal/sideresult/sideresult.component.ts b/src/app/pages/portal/sideresult/sideresult.component.ts deleted file mode 100644 index 075f07f1..00000000 --- a/src/app/pages/portal/sideresult/sideresult.component.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { - Component, - Input, - Output, - OnInit, - OnDestroy, - EventEmitter, - ChangeDetectionStrategy, - ElementRef, - ViewChild -} from '@angular/core'; - -import * as proj from 'ol/proj'; -import { MediaService } from '@igo2/core'; -import { EntityStore, ActionStore } from '@igo2/common'; - -import { BehaviorSubject } from 'rxjs'; - -import { - IgoMap, - FEATURE, - Feature, - FeatureMotion, - GoogleLinks, - LayerService, - MapService, - Layer, - Research, - SearchResult, - SearchService -} from '@igo2/geo'; -import { SearchState } from '@igo2/integration'; -import { ConfigService } from '@igo2/core'; - -@Component({ - selector: 'app-sideresult', - templateUrl: './sideresult.component.html', - styleUrls: ['./sideresult.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SideResultComponent implements OnInit, OnDestroy { - title$: BehaviorSubject = new BehaviorSubject(undefined); - @Input() hasSearchQuery: boolean = undefined; - - @Input() - public hasBackdrop: boolean; - - @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(); - - events: string[] = []; - public hasToolbox: boolean = undefined; - public hasSideresultButton: boolean = undefined; - public store = new ActionStore([]); - public igoSearchPointerSummaryEnabled: boolean = false; - public useEmbeddedVersion: boolean = false; - - public termSplitter: string = '|'; - - public view = { - center: [-73, 47.2], - zoom: 7 - }; - - public osmLayer: Layer; - - @ViewChild('mapBrowser', { read: ElementRef, static: true }) mapBrowser: ElementRef; - - public lonlat; - public mapProjection: string; - public term: string; - public settingsChange$ = new BehaviorSubject(undefined); - get searchStore(): EntityStore { - return this.searchState.store; - } - - get isTouchScreen(): boolean { - return this.mediaService.isTouchScreen(); - } - - public selectedFeature: Feature; - - constructor( - private configService: ConfigService, - private mapService: MapService, - private layerService: LayerService, - private searchState: SearchState, - private searchService: SearchService, - private mediaService: MediaService - ) { - this.mapService.setMap(this.map); - this.hasToolbox = this.configService.getConfig('hasToolbox') === undefined ? false : - this.configService.getConfig('hasToolbox'); - this.useEmbeddedVersion = this.configService.getConfig('embeddedVersion.useEmbeddedVersion') === undefined ? false : this.configService.getConfig('embeddedVersion.useEmbeddedVersion'); - this.hasSideresultButton = this.configService.getConfig('hasSideresultButton') !== undefined && this.useEmbeddedVersion === false ? - this.configService.getConfig('hasSideresultButton') : false; - - this.layerService - .createAsyncLayer({ - title: 'OSM', - sourceOptions: { - type: 'osm' - } - }) - .subscribe(layer => { - this.osmLayer = layer; - }); - } - - onPointerSummaryStatusChange(value) { - this.igoSearchPointerSummaryEnabled = value; - } - - onSearchTermChange(term = '') { - this.term = term; - const termWithoutHashtag = term.replace(/(#[^\s]*)/g, '').trim(); - if (termWithoutHashtag.length < 2) { - this.searchStore.clear(); - this.selectedFeature = undefined; - } - } - - onSearch(event: { research: Research; results: SearchResult[] }) { - 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); - } - - onSearchSettingsChange() { - this.settingsChange$.next(true); - } - - /** - * 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 - ); - } - - ngOnInit() { - this.store.load([ - { - id: 'coordinates', - title: 'coordinates', - handler: this.onSearchCoordinate.bind(this) - }, - { - id: 'googleMaps', - title: 'googleMap', - handler: this.onOpenGoogleMaps.bind(this), - args: ['1'] - }, - { - id: 'googleStreetView', - title: 'googleStreetView', - handler: this.onOpenGoogleStreetView.bind(this) - } - ]); - } - - ngOnDestroy() { - this.store.destroy(); - } - - /* - * Remove a feature to the map overlay - */ - removeFeatureFromMap() { - this.map.searchResultsOverlay.clear(); - } - - onContextMenuOpen(event: { x: number; y: number }) { - const position = this.mapPosition(event); - const coord = this.mapService.getMap().ol.getCoordinateFromPixel(position); - this.mapProjection = this.mapService.getMap().projection; - this.lonlat = proj.transform(coord, this.mapProjection, 'EPSG:4326'); - } - - mapPosition(event: { x: number; y: number }) { - const contextmenuPoint = event; - contextmenuPoint.y = - contextmenuPoint.y - - this.mapBrowser.nativeElement.getBoundingClientRect().top + - window.scrollY; - contextmenuPoint.x = - contextmenuPoint.x - - this.mapBrowser.nativeElement.getBoundingClientRect().left + - window.scrollX; - const position = [contextmenuPoint.x, contextmenuPoint.y]; - return position; - } - - onPointerSearch(event) { - this.lonlat = event; - this.onSearchCoordinate(); - } - - 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 }); - }); - } - } - } - - onOpenGoogleMaps() { - window.open(GoogleLinks.getGoogleMapsCoordLink(this.lonlat[0], this.lonlat[1])); - } - - onOpenGoogleStreetView() { - window.open( - GoogleLinks.getGoogleStreetViewLink(this.lonlat[0], this.lonlat[1]) - ); - } -} diff --git a/src/config/config.json b/src/config/config.json index 051c9c76..bfd88ad3 100644 --- a/src/config/config.json +++ b/src/config/config.json @@ -63,7 +63,6 @@ "available": false } }, - "showRotationButtonIfNoRotation": false, "title": "IGO", "header": { "hasHeader": true, @@ -73,6 +72,8 @@ "hasMenu": false, "hasFooter": true, "hasLegendButton": true, + "legendInPanel": true, + "showRotationButtonIfNoRotation": true, "hasSideresultButton": true, "hasGeolocateButton": true, "geolocate": { @@ -80,8 +81,11 @@ "followPosition": false, "activateDefault": false }, + "hasFeatureEmphasisOnSelection": true, + "addFeaturetoLayer": false, "useStaticIcon": false, - "hasExpansionPanel": false, + "customFeatureTitle": false, + "customFeatureDetails": false, "searchBar": { "showSearchBar": true, "showSearchButton": true @@ -89,10 +93,8 @@ "importExport": { "url": "/apis/ogre" }, - "showMenuButton": false, - "hasToolbox": false, "mobileBreakPoint": "'(min-width: 768px)'", "language": { - "prefix": ["./locale/", "./particular/locale/"] + "prefix": ["./locale/"] } } diff --git a/src/environments/environment.github.ts b/src/environments/environment.github.ts index fb8af7a9..d26e18ec 100644 --- a/src/environments/environment.github.ts +++ b/src/environments/environment.github.ts @@ -30,7 +30,7 @@ export const environment: Environment = { production: true, igo: { app: { - forceCoordsNA: false, + forceCoordsNA: true, install: { enabled: true, promote: true, @@ -41,9 +41,13 @@ export const environment: Environment = { } }, language: { - prefix: ['./locale/', './particular/locale/'] + prefix: ['./locale/'] }, searchSources: { + workspace: { + available: false, + enabled: false + }, nominatim: { available: false }, @@ -52,11 +56,13 @@ export const environment: Environment = { available: false }, icherche: { - searchUrl: 'https://geoegl.msp.gouv.qc.ca/apis/icherche', + searchUrl: 'https://geoegl.msp.gouv.qc.ca/apis/icherche/', order: 2, params: { limit: '5' - } + }, + settings:[], + showInPointerSummary: true }, coordinatesreverse: { showInPointerSummary: true @@ -68,6 +74,7 @@ export const environment: Environment = { enabled: true }, ilayer: { + enabled: false, searchUrl: 'https://geoegl.msp.gouv.qc.ca/apis/icherche/layers', order: 4, params: { @@ -92,70 +99,6 @@ export const environment: Environment = { +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', extent: [31796.5834, 158846.2231, 1813323.4284, 2141241.0978] } - ], - searchOverlayStyle: { - base: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 0.8, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#a7e7ff', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.2, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#5ed0fb', // line and poly - strokeOpacity: 0.7, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - focus: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#DFF7FF', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#DFF7FF', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - selection: { - markerColor: '#00a1de', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#ffffff', // marker contour - fillColor: '#00a1de', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#00A1DE', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - } - }, - queryOverlayStyle: { - base: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 0.8, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#a7e7ff', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.2, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#5ed0fb', // line and poly - strokeOpacity: 0.7, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - focus: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#DFF7FF', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#DFF7FF', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - selection: { - markerColor: '#00a1de', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#ffffff', // marker contour - fillColor: '#00a1de', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#00A1DE', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - } - } + ] } }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 2280f4a2..bbc4b583 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -38,11 +38,11 @@ export const environment: Environment = { promote: false }, pwa: { - enabled: false, + enabled: false } }, language: { - prefix: ['./locale/', './particular/locale/'] + prefix: ['./locale/'] }, optionsApi: { url: '/apis/igo2/layers/options' @@ -66,6 +66,10 @@ export const environment: Environment = { } ], searchSources: { + workspace: { + available: false, + enabled: false + }, nominatim: { available: false }, @@ -78,7 +82,9 @@ export const environment: Environment = { order: 2, params: { limit: '5' - } + }, + settings:[], + showInPointerSummary: true }, coordinatesreverse: { showInPointerSummary: true @@ -90,76 +96,13 @@ export const environment: Environment = { enabled: true }, ilayer: { + enabled: false, searchUrl: '/apis/icherche/layers', order: 4, params: { limit: '3' } } - }, - searchOverlayStyle: { - base: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 0.8, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#a7e7ff', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.2, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#5ed0fb', // line and poly - strokeOpacity: 0.7, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - focus: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#DFF7FF', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#DFF7FF', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - selection: { - markerColor: '#00a1de', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#ffffff', // marker contour - fillColor: '#00a1de', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#00A1DE', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - } - }, - queryOverlayStyle: { - base: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 0.8, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#a7e7ff', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.2, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#5ed0fb', // line and poly - strokeOpacity: 0.7, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - focus: { - markerColor: '#5ed0fb', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#DFF7FF', // marker contour - fillColor: '#5ed0fb', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#DFF7FF', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - }, - selection: { - markerColor: '#00a1de', // marker fill - markerOpacity: 1, // marker opacity not applied if a rgba markerColor is provided - markerOutlineColor: '#ffffff', // marker contour - fillColor: '#00a1de', // poly - fillOpacity: 0.3, // poly fill opacity not applied if a rgba fillColor is provided - strokeColor: '#00A1DE', // line and poly - strokeOpacity: 1, // line and poly not applied if a rgba strokeColor is provided - strokeWidth: 2 // line and poly - } } } }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6514fc17..600ade6e 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -44,7 +44,7 @@ export const environment: Environment = { } }, language: { - prefix: ['./locale/', './particular/locale/'] + prefix: ['./locale/'] }, projections: [ { @@ -95,6 +95,7 @@ export const environment: Environment = { enabled: true }, ilayer: { + enabled: false, searchUrl: '/apis/icherche/layers', order: 4, params: { diff --git a/src/locale/en.json b/src/locale/en.json index b5d091bd..f398c4c6 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -1,5 +1,5 @@ { - "IGO": "IGO", + "IGO": "IGO ― Infrastructure géomatique ouverte", "oldBrowser": { "title": "Too old browser", "message": "IGO2 does not fully support your browser. Please update it or use a different one." @@ -23,11 +23,22 @@ "icherche": { "name": "Search result" }, - "sideresult": { - "close": "Close", - "open": "Open" + "searchBar": "Search", + "sidePanel": { + "close": "Hide panel", + "open": "Open panel" + }, + "feature": { + "title" : "Object", + "close": "Close panel", + "tooltip": "Customise this template as you wish, even by layer using feature.meta.id or other layers metadatas" + }, + "legend": { + "title": "Legend", + "button": "Legend", + "close": "Close legend", + "open": "Open legend" }, - "search-bar": "Search", "pwa": { "new-version-title": "New version available. ", "new-version": "Do you want to reload the app?", @@ -77,5 +88,22 @@ "selected": "selected", "selecteds": "selected" + }, + "igo":{ + "integration": { + "searchResultsTool": { + "noResults": "No result", + "doSearch": "Enter a text in the searchbar", + "examples": "

You can search for adresses, locations, businesses or coordinates.

Address searches can be done combining :

  • civic number
  • street name
  • city
  • postal code

Coordinates search example :

  • Decimal degree:
    -68.165547, 48.644546
  • Degrees, Minutes, Seconds:
    -68 9 56, 48 38 40

" + } + }, + "geo": { + "search": { + "icherche": { + "name": "Search results", + "placeholder": "Search for a location" + } + } + } } } diff --git a/src/locale/fr.json b/src/locale/fr.json index 2bba49a9..266f615f 100644 --- a/src/locale/fr.json +++ b/src/locale/fr.json @@ -1,5 +1,5 @@ { - "IGO": "IGO", + "IGO": "IGO ― Infrastructure géomatique ouverte", "oldBrowser": { "title": "Navigateur obsolète", "message": "IGO2 ne supporte pas pleinement votre navigateur. Veuillez le mettre à jour ou utilisez un navigateur différent." @@ -23,11 +23,22 @@ "icherche": { "name": "Résultats de recherche" }, - "sideresult": { - "close": "Fermer", - "open": "Ouvrir" + "legend": { + "title": "Légende", + "button": "Légende", + "close": "Fermer la légende", + "open": "Afficher la légende" + }, + "feature": { + "title" : "Objet", + "close": "Fermer le panneau", + "tooltip": "Personnalisez ce template, même par couche avec une condition sous feature.meta.id ou autres metadatas" + }, + "searchBar": "Faire une recherche", + "sidePanel": { + "close": "Masquer le panneau", + "open": "Ouvrir le panneau" }, - "search-bar": "Faire une recherche", "pwa": { "new-version-title": "Une nouvelle version de l'application est disponible. ", "new-version": "Voulez-vous recharger l'application?", @@ -76,5 +87,22 @@ "selected": "sélection", "selecteds": "sélections" + }, + "igo":{ + "integration": { + "searchResultsTool": { + "noResults": "Aucun résultat", + "doSearch": "Veuillez effectuer une recherche dans la barre de recherche ci-dessus.", + "examples": "

Permet de rechercher des adresses, des lieux, des entreprises ou des coordonnées.

Les recherches d'adresses peuvent être effectuées selon une combinaison :

  • du no. immeuble
  • du nom de la rue
  • de la ville
  • du code postal

Exemple de coordonnées :

  • Degré décimal:
    -68.165547, 48.644546
  • Degré minute seconde:
    -68 9 56, 48 38 40

" + } + }, + "geo": { + "search": { + "icherche": { + "name": "Résultats de recherche", + "placeholder": "Rechercher une localisation" + } + } + } } } diff --git a/src/particular/locale/en.json b/src/particular/locale/en.json deleted file mode 100644 index 9096badb..00000000 --- a/src/particular/locale/en.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "igo":{ - "integration": { - "searchResultsTool": { - "noResults": "Aucun résultat", - "doSearch": "Veuillez effectuer une recherche dans la barre de recherche ci-dessus.", - "examples": "

Permet de rechercher des adresses, des lieux, des entreprises ou des coordonnées.

Les recherches d'adresses peuvent être effectuées selon une combinaison :

  • du no. immeuble
  • du nom de la rue
  • de la ville
  • du code postal

Exemple de coordonnées :

  • Degré décimal:
    -68.165547, 48.644546
  • Degré minute seconde:
    -68 9 56, 48 38 40

" - } - }, - "geo": { - "search": { - "icherche": { - "name": "Résultats de recherche" - } - } - } - } -} diff --git a/src/particular/locale/fr.json b/src/particular/locale/fr.json deleted file mode 100644 index 9096badb..00000000 --- a/src/particular/locale/fr.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "igo":{ - "integration": { - "searchResultsTool": { - "noResults": "Aucun résultat", - "doSearch": "Veuillez effectuer une recherche dans la barre de recherche ci-dessus.", - "examples": "

Permet de rechercher des adresses, des lieux, des entreprises ou des coordonnées.

Les recherches d'adresses peuvent être effectuées selon une combinaison :

  • du no. immeuble
  • du nom de la rue
  • de la ville
  • du code postal

Exemple de coordonnées :

  • Degré décimal:
    -68.165547, 48.644546
  • Degré minute seconde:
    -68 9 56, 48 38 40

" - } - }, - "geo": { - "search": { - "icherche": { - "name": "Résultats de recherche" - } - } - } - } -} diff --git a/src/qcca-theme/qcca-theme.scss b/src/qcca-theme/qcca-theme.scss index 8cfd9c97..ec05e046 100644 --- a/src/qcca-theme/qcca-theme.scss +++ b/src/qcca-theme/qcca-theme.scss @@ -17,7 +17,7 @@ margin: 0 !important; width: 100%; height: 100%; font-size: 16px; -font-family: 'Open sans', sans-serif; +font-family: 'Open Sans', sans-serif; color: #223654; } @@ -26,7 +26,7 @@ color: #223654; margin: 0 !important; width: 100%; font-size: 16px; - font-family: 'Open sans', sans-serif; + font-family: 'Open Sans', sans-serif; //overflow: hidden; } @@ -68,7 +68,7 @@ vertical-align: baseline; } html, body { - font-family: 'Open sans', sans-serif; + font-family: 'Open Sans', sans-serif; } .col-12 { @@ -96,7 +96,7 @@ margin: auto; .page { overflow-y: auto!important; - overflow-x: auto; + overflow-x: clip; height: calc($portal-height-hasMenu-hasHeader - (4 * $igo-margin)); //if no header: $portal-height-hasMenu width: 100%; } @@ -204,8 +204,6 @@ igo-baselayers-switcher, igo-geolocate-button { box-shadow: 0 1px 4px rgb(34 54 84 / 24%); } -// ANGULAR MATERIAL - app-search-results-tool ul{ margin-left: 1rem; } @@ -214,21 +212,51 @@ app-search-results-tool ul{ border: 2px solid currentColor; } -::ng-deep .mat-button, .mat-icon-button, .mat-stroked-button, .mat-flat-button { +::ng-deep .mat-button, .mat-icon-button, .mat-stroked-button, .mat-flat-button, mat-button-base, +mat-stroked-button .mat-primary, .qcca-theme .mat-button, .qcca-theme .mat-icon-button, .qcca-theme .mat-stroked-button { border-radius: 0 !important; } -::ng-deep .qcca-theme app-legend-button button, ::ng-deep .qcca-theme .mat-raised-button { +.mat-raised-button { color: #095797 !important; border-color: #095797 !important; } .headline { background-color: #223654 !important; - } +} +// give a different class to each tooltip depending on the arrow side +::ng-deep .tooltip { + 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::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); +} + // Pour afficher le symbole de lien externe. Ex de HTML : /*
Lien Cet hyperlien s'ouvrira dans une nouvelle fenêtre. @@ -328,8 +356,8 @@ text-decoration: underline; background-color: transparent; } -p, ul, li, u { - font-family: 'Open Sans',sans-serif; +p, ul, li, u, .text { + font-family: 'Open Sans', sans-serif; color: #223654; font-size: 16px; line-height: 24px; @@ -354,10 +382,10 @@ h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { // removed h3 for conflic h1 { font-size: 48px; - line-height: 40px; + line-height: 56px; font-weight: bold; - margin-top: 48px; - margin-bottom: 16px; + margin-top: 72px; + margin-bottom: 32px; } @media (max-width: 1200px) { @@ -373,9 +401,9 @@ h1:after { content: ""; display: block; margin: 0; - width: 4.8rem; - padding-top: 0.4rem; - border-bottom: 4px solid #f09686; + width: 48px; + padding-top: 4px; + border-bottom: 4px solid #e58271; } h2 { @@ -388,7 +416,7 @@ h2 { @media (max-width: 1200px) { h2 { - font-size: 29px; + font-size: 28px; line-height: 32px; font-weight: bold; margin-top: 48px; @@ -432,12 +460,17 @@ h5 { h6 { font-size: 16px!important; - line-height: 24px!important; + line-height: 20px!important; font-weight: bold; margin-top: 16px; margin-bottom: 0px; } +// class to capitalize the first letter +.cap-first::first-letter { + text-transform: capitalize; +} + .headline { background-color: #223654!important; } @@ -465,7 +498,7 @@ border-radius: 0 !important; .qcca-theme input.mat-input-element { color: #647287; - font-family: 'open sans'; + font-family: 'Open Sans', sans-serif; } @@ -474,10 +507,105 @@ igo-list [igolistitem][color=accent].igo-list-item-selected > mat-list-item { color: white!important; } +.qcca-theme igo-list [igolistitem][color=accent].igo-list-item-selected > mat-list-item { + background-color: #4A98D9; + + h4, small { + color: white!important; + } +} + ::ng-deep igo-search-results-item small { color: white!important; } +// Exemple de template d'AVIS: + +// Structure html : +//

+// +// +// Title
+// Label +//
+//

+// + +.avis { + height: max-content; + width: 100%; + display: flex; +} + +.avis-icon { + width: 40px; + display: inline-block; +} + +.avis-icon .mat-icon { + margin: 24px 8px 0 8px; +} + +.avis-content { + padding: 24px 24px 24px 16px; + display: inline-block; +} + +.avis-title { + font-weight: bold; + font-size: 14px; + color: #223654; + display: inline-block; + width: 100%; +} + +.avis-label { + display: inline-block; + width: 100%; + font-size: 14px; + color: #223654; +} + +.avis-general { + background-color:#D9E6F0; +} + +.avis-general svg { // + color: #095797!important; +} + +.avis-important { + background-color:#F8E69A; +} + +.avis-important svg { // + color: #E0AD03!important; +} + +.avis-success { + background-color:#BCDA9A; +} + +.avis-success svg { // + color: #4F813D!important; +} + +.avis-erreur { + background-color:#EDBAB1; +} + +.avis-erreur svg { // + color: #CB381F!important; +} + +.avis-complementaire { + background-color:#F2F1F1; +} + +.avis-complementaire svg { // + color: #83868B!important; +} + // input fields ::ng-deep .mat-form-field-outline-end, ::ng-deep .mat-form-field-outline-start { @@ -535,34 +663,4 @@ border-radius: 0 !important; ::ng-deep .toast-message, ::ng-deep #toast-container, ::ng-deep .toast-title, toast-container, ::ng-deep .toast-error { color: white!important; -} - -//* Spinner *// - -igo-spinner { - position: absolute; - top: calc(#{$header-height-mobile} + 4px) !important; - right: 4px; - z-index: 100; -} - -spinner-with-legend-dialog { - position: absolute; - top: calc(#{$header-height-mobile} + 4px) !important; - right: 345px; - z-index: 100; -} - -@media (min-width: 768px){ - igo-spinner { - top: calc(#{$header-height} + 4px)!important; - right: 4px; - z-index: 100; - } - - spinner-with-legend-dialog { - top: calc(#{$header-height} + 4px)!important; - right: 345px!important; - z-index: 100; - } -} +} \ No newline at end of file