diff --git a/config/config-dev.json b/config/config-dev.json index cafd55e7e661a8fdf906253b4bc1bd45663a1736..9745e7ac71abbf0a8c1c45d46f92be59a52ce420 100644 --- a/config/config-dev.json +++ b/config/config-dev.json @@ -10,6 +10,7 @@ "matomo": "https://kong-dev.alpha.grandlyon.com/analytics/pageStats", "elasticsearch": "https://kong-dev.alpha.grandlyon.com/es-consumer-aware", "catalogue": "https://kong-dev.alpha.grandlyon.com/catalogue", - "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses" + "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses", + "geocoder": "https://download.data.grandlyon.com/geocoding/photon" } } \ No newline at end of file diff --git a/config/config-rec.json b/config/config-rec.json index 8ed3d957ed9fd3d145db555cf66a1bd767692350..307a5c400d3e522a3ed2da1a1beb22dc25b3776c 100644 --- a/config/config-rec.json +++ b/config/config-rec.json @@ -10,6 +10,7 @@ "matomo": "https://kong-rec.alpha.grandlyon.com/analytics/pageStats", "elasticsearch": "https://kong-rec.alpha.grandlyon.com/es-consumer-aware", "catalogue": "https://kong-rec.alpha.grandlyon.com/catalogue", - "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses" + "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses", + "geocoder": "https://download.data.grandlyon.com/geocoding/photon" } } \ No newline at end of file diff --git a/src/app/core/services/app-config.service.ts b/src/app/core/services/app-config.service.ts index fbc6c3297e57ec025cda88d34a3e8a7118558a15..bd1498b69d17be24675c72862306fb9644b75537 100644 --- a/src/app/core/services/app-config.service.ts +++ b/src/app/core/services/app-config.service.ts @@ -13,6 +13,7 @@ export class AppConfig { elasticsearch: string; catalogue: string; reuses: string; + geocoder: string; }; } diff --git a/src/app/geosource/components/search-bar/search-bar.component.scss b/src/app/geosource/components/search-bar/search-bar.component.scss index ecc4c7f35c4c2b777f95efacc59dd70f91ca34be..6ce3e691d6579fd99c0e0a7b23e1632ddb0ff6f5 100644 --- a/src/app/geosource/components/search-bar/search-bar.component.scss +++ b/src/app/geosource/components/search-bar/search-bar.component.scss @@ -1,7 +1,6 @@ @import "../../../../scss/variables.scss"; @import "../../../../../node_modules/bulma/sass/utilities/_all.sass"; - .row { padding-right: 1.875rem; padding-left: 1.875rem; @@ -23,7 +22,8 @@ border-top: none; z-index: 99; background-color: white; - /*position the autocomplete items to be the same width as the container:*/ + + /* position the autocomplete items to be the same width as the container: */ top: 100%; left: 0; right: 0; @@ -38,9 +38,10 @@ border-top: 0.0625rem solid #d4d4d4; } } -.autocomplete-items p:hover, .autocomplete-items p.isActive { - /*when hovering an item:*/ - background-color: #DEE6EE; +.autocomplete-items p:hover, +.autocomplete-items p.isActive { + /* when hovering an item: */ + background-color: #dee6ee; } .search-bar { @@ -49,7 +50,9 @@ .field { height: 100%; - .button, .input, .reset-research-icon { + .button, + .input, + .reset-research-icon { height: 100%; } @@ -67,15 +70,15 @@ border: 0; } - .button-research .button{ + .button-research .button { color: #818080; padding: 0 1.25rem 0 1.25rem; border-top-right-radius: 4px; border-bottom-right-radius: 4px; box-shadow: none; - &:after { - content: ''; + &::after { + content: ""; position: absolute; top: 25%; left: 1px; @@ -85,7 +88,9 @@ z-index: 5; } - &:hover, &:focus, &:active { + &:hover, + &:focus, + &:active { color: $tomato-color; z-index: 5; } @@ -110,4 +115,4 @@ .research-input { min-width: unset; } -} \ No newline at end of file +} diff --git a/src/app/map/components/index.ts b/src/app/map/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fe7372251c48c29342d31f6b3acaa0981b9f6ff --- /dev/null +++ b/src/app/map/components/index.ts @@ -0,0 +1,12 @@ +import { DataDetailsComponent } from './data-details/data-details.component'; +import { MapComponent } from './map.component'; +import { SearchAddressComponent } from './search-address/search-address.component'; + +export { DataDetailsComponent, MapComponent, SearchAddressComponent }; + +// tslint:disable-next-line:variable-name +export const MapComponents = [ + DataDetailsComponent, + MapComponent, + SearchAddressComponent, +]; diff --git a/src/app/map/components/map.component.html b/src/app/map/components/map.component.html index 8eb83b02827ec548fc95d34441a69a61abba3877..0ccba37b98ec283c53495e3ac7d5f544a422cbb4 100644 --- a/src/app/map/components/map.component.html +++ b/src/app/map/components/map.component.html @@ -6,37 +6,32 @@ <div class="column is-narrow"> <div class="buttons has-addons column-content"> <ng-container *ngFor="let l of settings.baseLayers"> - <button class="button" [disabled]="l.id === selectedBaseLayer.id" - [ngClass]="{'is-selected': l.id === selectedBaseLayer.id, 'is-danger': l.id === selectedBaseLayer.id}" + <button class="button selectBase" [disabled]="l.id === selectedBaseLayer.id" + [ngClass]="{'is-selected': l.id === selectedBaseLayer.id, 'is-active': l.id === selectedBaseLayer.id}" (click)="switchLayer(l)" type="button"> {{ l.labels[lang] }} </button> </ng-container> </div> </div> + <div class="column"> + <app-search-address [optionsAutocomplete]="searchLocationResult" + (searchAddress)="searchAdress($event)" (addressSelected)="flyTo($event)" (clearAddress)="removeMarker()"> + </app-search-address> + </div> </div> - <div class="columns is-mobile is-marginless"> + <div class="toggleFullScreen columns is-mobile is-marginless"> <div class="column"> <div class="mapboxgl-ctrl-group column-content"> <button class="mapboxgl-ctrl-icon" [ngClass]="{'btn-exit-fullscreen': fullscreen === true, 'btn-fullscreen': fullscreen === false}" type="button" (click)="toogleFullscreen()"> </button> - </div> - </div> - </div> - - <div class="columns is-mobile is-hidden-tablet is-marginless"> - <div class="column"> - <div class="mapboxgl-ctrl-group column-content" (click)="displayPitchSlider = !displayPitchSlider"> - <button class="mapboxgl-ctrl-icon btn-pitch" type="button"></button> - </div> - </div> - <div class="column" *ngIf="displayPitchSlider"> - <div class="pitch-input-container column-content"> - <input class="slider is-danger pitch-input" step="1" min="0" max="60" [value]="map.getPitch()" type="range" - (input)="changeMapPitchValue($event.target.value)"> + <button class="mapboxgl-ctrl-icon" + class="geolocation" + type="button" (click)="centerToMyPosition()"> + </button> </div> </div> </div> @@ -46,8 +41,10 @@ <input type="text" class="input" id="mapUrlCopy" [value]="mapUrl()" #mapUrlElement> <button type="button" (click)="copyMaplink(mapUrlElement)" class="button is-medium"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="0 0 20 20"> - <path fill="#818080" fill-rule="evenodd" d="M1.852 7.777a2.619 2.619 0 0 1 0-3.703l2.222-2.222a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.703l-.37.37.74.741.37-.37a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.704l-2.222 2.222a2.619 2.619 0 0 1-3.704 0L9.26 15.185a2.619 2.619 0 0 1 0-3.704l.37-.37-.74-.741-.37.37a2.619 2.619 0 0 1-3.704 0L1.852 7.777zm7.037-.37l-.74-.74a1.048 1.048 0 0 0-1.482 1.48l.74.742-.37.37a.524.524 0 0 1-.74 0L3.332 6.296a.524.524 0 0 1 0-.74l2.223-2.223a.524.524 0 0 1 .74 0L9.26 6.296a.524.524 0 0 1 0 .74l-.37.371zm2.222 5.185l-.37.37a.524.524 0 0 0 0 .742l2.963 2.962a.524.524 0 0 0 .74 0l2.223-2.222a.524.524 0 0 0 0-.74l-2.963-2.963a.524.524 0 0 0-.741 0l-.37.37.74.74a1.047 1.047 0 1 1-1.481 1.482l-.74-.74z" clip-rule="evenodd"/> - </svg> + <path fill="#818080" fill-rule="evenodd" + d="M1.852 7.777a2.619 2.619 0 0 1 0-3.703l2.222-2.222a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.703l-.37.37.74.741.37-.37a2.619 2.619 0 0 1 3.704 0l2.963 2.963a2.619 2.619 0 0 1 0 3.704l-2.222 2.222a2.619 2.619 0 0 1-3.704 0L9.26 15.185a2.619 2.619 0 0 1 0-3.704l.37-.37-.74-.741-.37.37a2.619 2.619 0 0 1-3.704 0L1.852 7.777zm7.037-.37l-.74-.74a1.048 1.048 0 0 0-1.482 1.48l.74.742-.37.37a.524.524 0 0 1-.74 0L3.332 6.296a.524.524 0 0 1 0-.74l2.223-2.223a.524.524 0 0 1 .74 0L9.26 6.296a.524.524 0 0 1 0 .74l-.37.371zm2.222 5.185l-.37.37a.524.524 0 0 0 0 .742l2.963 2.962a.524.524 0 0 0 .74 0l2.223-2.222a.524.524 0 0 0 0-.74l-2.963-2.963a.524.524 0 0 0-.741 0l-.37.37.74.74a1.047 1.047 0 1 1-1.481 1.482l-.74-.74z" + clip-rule="evenodd" /> + </svg> <span class="is-hidden-touch">{{ shareMessage }}</span> </button> </div> diff --git a/src/app/map/components/map.component.scss b/src/app/map/components/map.component.scss index eefaf217a1230169fb0f861f5b8cefd5ac77d5c9..112d257ed83cd80113a3b1ac88d135cac8070b73 100644 --- a/src/app/map/components/map.component.scss +++ b/src/app/map/components/map.component.scss @@ -45,6 +45,14 @@ } } +::ng-deep .mapboxgl-ctrl-top-right { + display: none; + + @media screen and (min-width: $tablet) { + display: block; + } +} + #minimap.mapboxgl-map { height: 12.5rem; } @@ -93,8 +101,8 @@ @media screen and (max-width: $desktop) { width: 30px; - right: 9px; - top: 105px; + right: 5px; + top: 80px; margin-top: 0; .button { @@ -147,28 +155,51 @@ z-index: 1; position: absolute; margin: 10px 0 0 10px; + width: 100%; + + @media screen and (max-width: $tablet) { + .toggleFullScreen { + position: absolute; + top: 0; + right: 0; + transform: translateX(-50%); + } + } .columns { + display: block; + + @media screen and (min-width: $tablet) { + display: flex; + } + .column { display: flex; padding: 0 0 0.625rem 0; } - .column:not(:first-of-type) { - padding-left: 0.625rem; + @media screen and (min-width: $tablet) { + .column:not(:first-of-type) { + padding-left: 0.625rem; + } } .buttons { - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1); border-radius: 0.25rem; margin-bottom: 0; .button { margin-bottom: 0; width: 70px; + height: 2.5em; } } + .selectBase.is-active { + background-color: $blue-color; + color: white; + } + button { &:disabled { opacity: 1; @@ -196,20 +227,6 @@ background-position: center; } - .btn-pitch { - background-image: url('../../../assets/img/angle.png'); - background-size: 1.25rem 1.25rem; - } - - .btn-3d { - font-weight: bold; - - &.is-active { - background-color: $tomato-color; - color: white; - } - } - .btn-exit-fullscreen { background-image: url('../../../assets/img/exit-fullscreen.svg'); background-size: 20px 20px; @@ -220,31 +237,17 @@ background-size: 20px 20px; } + .geolocation { + background-image: url('../../../assets/img/geolocation.svg'); + background-size: 36px 36px; + border-top: 1px solid $grey-super-light-color; + } + button:focus, button:hover { background-color: rgba(0, 0, 0, 0.05); } } - - input.pitch-input { - background-color: rgba(255, 255, 255, 1); - margin: 0; - padding: 0.75rem; - border-radius: 0.25rem; - box-shadow: 0 0 0 0.125rem rgba(0, 0, 0, 0.1); - display: block; - } - - .pitch-input:focus { - background-color: rgba(0, 0, 0, 0.05); - } - - .pitch-input-container { - background-color: #fff; - border-radius: 0.25rem; - box-shadow: 0 0 0 0.125rem rgba(0, 0, 0, 0.1); - display: inline-block; - } } } diff --git a/src/app/map/components/map.component.spec.ts b/src/app/map/components/map.component.spec.ts deleted file mode 100644 index 95d5c97dc71ef24194e434a796c8e081ca971eef..0000000000000000000000000000000000000000 --- a/src/app/map/components/map.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -// import { DatasetMapComponent } from './dataset-map.component'; - -// describe('DatasetMapComponent', () => { -// let component: DatasetMapComponent; -// let fixture: ComponentFixture<DatasetMapComponent>; - -// beforeEach(async(() => { -// TestBed.configureTestingModule({ -// declarations: [DatasetMapComponent], -// }) -// .compileComponents(); -// })); - -// beforeEach(() => { -// fixture = TestBed.createComponent(DatasetMapComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); - -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); diff --git a/src/app/map/components/map.component.ts b/src/app/map/components/map.component.ts index 53eeedffe632059cb0a7a38e6c74557589206ced..f1ebfb5201b881c71ee101ff63494a1899def8d7 100644 --- a/src/app/map/components/map.component.ts +++ b/src/app/map/components/map.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input, ViewEncapsulation } from '@angular/core'; import * as mapboxgl from 'mapbox-gl'; import { Subscription } from 'rxjs'; import { Metadata } from '../../geosource/models'; @@ -7,6 +7,9 @@ import { MapService } from '../services/map.service'; import { settings } from '../settings'; import { geosource } from '../../../i18n/traductions'; import { linkFormats } from '../../geosource/models/metadata.model'; +import { FormControl } from '@angular/forms'; +import { debounceTime } from 'rxjs/operators'; +import { GeocoderService } from '../services/geocoder.service'; @Component({ selector: 'app-map', @@ -48,15 +51,26 @@ export class MapComponent implements OnInit, OnDestroy { selectedData = null; // Contains the properties of the selected feature fullscreen = false; + geolocation = false; + + searchLocationResult = []; + markerLocation: mapboxgl.Marker; + // markerLocation: HTMLDivElement; constructor( private _datasetDetailService: DatasetDetailService, private _mapService: MapService, + private _geocoderService: GeocoderService, ) { } ngOnInit() { this.initilizeLanguage(); + // Geolocation availability in the browser + if ('geolocation' in navigator) { + this.geolocation = true; + } + this.constructMap(); // Events received here contain a state (if the data-detail panel has to be displayed or not) @@ -92,6 +106,7 @@ export class MapComponent implements OnInit, OnDestroy { this.totalData = totalData; } }); + } // To avoid call the constructMap when we left the component @@ -102,6 +117,64 @@ export class MapComponent implements OnInit, OnDestroy { this._mapService.destroyMap(); } + searchAdress(value: string) { + console.log(value); + // Subscribe to search location input + const item = this.searchLocationResult.find(e => e.caption === value); + if (item) { + this.flyTo(item.value); + } else { + this._geocoderService.searchLocation(value).subscribe((response) => { + this.searchLocationResult = response; + }); + } + } + + flyTo(option) { + if (this.markerLocation) { + this.markerLocation.remove(); + } + + this.map.flyTo({ + center: option.value, + zoom: 15, + speed: 1, + curve: 1, + }); + + this.addMarkerLocation(option.value); + } + + addMarkerLocation(position) { + // create a DOM element for the marker + const el = document.createElement('div'); + el.className = 'marker'; + el.style.backgroundImage = 'url(/assets/img/position.svg)'; + el.style.backgroundRepeat = 'no-repeat'; + el.style.backgroundPosition = 'center'; + el.style.width = '50px'; + el.style.height = '50px'; + this.markerLocation = new mapboxgl.Marker(el).setLngLat(position).addTo(this.map); + } + + removeMarker() { + this.markerLocation.remove(); + } + + centerToMyPosition() { + navigator.geolocation.getCurrentPosition((position) => { + this.map.flyTo({ + center: [position.coords.longitude, position.coords.latitude], + zoom: 15, + speed: 1, + curve: 1, + }); + + this.addMarkerLocation([position.coords.longitude, position.coords.latitude]); + }); + + } + // When we get a metadata, we display the following: // - load Mapbox style file to get the styles and the base layers (vector, plan, satellite) constructMap() { @@ -116,11 +189,11 @@ export class MapComponent implements OnInit, OnDestroy { // Set the basic and default options const options = { + bounds, container: 'map', center: [4.85, 45.75] as mapboxgl.LngLatLike, zoom: 12, maxZoom: 21, - bounds, }; // Check if in the url we have map options @@ -227,7 +300,8 @@ export class MapComponent implements OnInit, OnDestroy { this.shareMessage = geosource.mapMessages.copied; setTimeout(() => { this.shareMessage = geosource.mapMessages.share; - }, 2000); + // tslint:disable-next-line:align + }, 2000); } mapUrl() { diff --git a/src/app/map/components/search-address/search-address.component.html b/src/app/map/components/search-address/search-address.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2b02e45902306756990b6966d546e6e268cff465 --- /dev/null +++ b/src/app/map/components/search-address/search-address.component.html @@ -0,0 +1,22 @@ +<div class="search-bar"> + <div class="field"> + + <div class="control research-input" [ngClass]="{'has-icons-right': searchValue}"> + <input type="text" class="input is-medium" [(ngModel)]="searchValue" #inputSearch + [placeholder]="geosource.placeholders.address" title="Research one dataset" i18n-title="@@research.dataset" + (keyup)="keyUpChanged($event.key)" (blur)="displayAutocomplete = false; currentAutocompleteFocus= -1;" + (focus)="displayAutocompleteList()" /> + <span class="icon is-small is-right reset-research-icon" *ngIf="searchValue" (click)="resetResearch()"> + <i class="fas fa-times-circle"></i> + </span> + <div class="autocomplete-items" *ngIf="autocompleteIsValid" + [ngStyle]="{'display': displayAutocomplete ? 'block' : 'none'}"> + <p *ngFor="let option of optionsAutocomplete; let i = index" (mousedown)="selectOption(option)" + [ngClass]="{'isActive' : i == currentAutocompleteFocus}" [innerHTML]="option.caption"> + + </p> + </div> + </div> + </div> + +</div> \ No newline at end of file diff --git a/src/app/map/components/search-address/search-address.component.scss b/src/app/map/components/search-address/search-address.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..7592f3c6c9b5d190762e2b3c63093b8541154e99 --- /dev/null +++ b/src/app/map/components/search-address/search-address.component.scss @@ -0,0 +1,79 @@ +@import "../../../../scss/variables.scss"; +@import "../../../../../node_modules/bulma/sass/utilities/_all.sass"; + +.research-input { + width: 100%; + min-width: 20rem; + input { + font-size: 0.875rem; + } +} + +@media screen and (max-width: $desktop) { + + .research-input { + min-width: unset; + } +} + +.autocomplete-items { + position: absolute; + border: 0.0625rem solid #d4d4d4; + border-bottom: none; + border-top: none; + z-index: 99; + background-color: white; + + /* position the autocomplete items to be the same width as the container: */ + top: 100%; + left: 0; + right: 0; +} + +.autocomplete-items p { + font-size: 0.875rem; + padding: 0.3125rem; + padding-left: 1rem; + cursor: pointer; + background-color: #fff; + border-bottom: 0.0625rem solid #d4d4d4; + + &:first-of-type { + border-top: 0.0625rem solid #d4d4d4; + } +} + +.autocomplete-items p:hover, +.autocomplete-items p.isActive { + /* when hovering an item: */ + background-color: #dee6ee; +} + +.search-bar { + height: 34px; + + .field { + height: 100%; + + .button, + .input, + .reset-research-icon { + height: 100%; + } + + .reset-research-icon { + cursor: pointer; + pointer-events: auto; + + i { + font-size: 0.875rem; + } + } + } + + input { + text-overflow: ellipsis; + box-shadow: none; + border: 1px solid $grey-super-light-color; + } +} diff --git a/src/app/map/components/search-address/search-address.component.ts b/src/app/map/components/search-address/search-address.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4f1d931c51478896c4496ef13aea5ac2e255a24 --- /dev/null +++ b/src/app/map/components/search-address/search-address.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, Input, EventEmitter, Output } from '@angular/core'; +import { geosource } from '../../../../i18n/traductions'; +import { debounceTime } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ViewportScroller } from '@angular/common'; + +@Component({ + selector: 'app-search-address', + templateUrl: './search-address.component.html', + styleUrls: ['./search-address.component.scss'], +}) +export class SearchAddressComponent implements OnInit { + + @Input() optionsAutocomplete: any; + @Output() searchAddress = new EventEmitter(); + @Output() clearAddress = new EventEmitter(); + @Output() addressSelected = new EventEmitter(); + @ViewChild('inputSearch') inputSearch: ElementRef; + + searchValue: string; + geosource = geosource; + + // Autocomplete + currentAutocompleteFocus = -1; + displayAutocomplete = false; + autocompleteIsValid = true; + + keyUpSubject: Subject<any> = new Subject<any>(); + + constructor( + ) { } + + ngOnInit() { + + /** + * We create this subject to have specific behaviour for the keyup event. + * When keyup event, this subject will emit (see below in the switch of the searchChanged() function) + * an event and will be catched here. + * Then we do API call only if users didn't type the last 800ms to avoid + * multiple api calls. + */ + this.keyUpSubject.pipe(debounceTime(600)).subscribe(() => { + this.requestAutocomplete(); + }); + } + + // This function is triggered when: + // - the click press key Enter or select option with the mouse: + // we make a search query & clean the completion list + // - any keyup in the search bar : + // we make the autocomplete query + // - arrow up and down buttons: + // we change the focus item in the completion list + keyUpChanged(key: string) { + switch (key) { + // These 2 keys are to go up and down in the autocomplete list items + case 'ArrowDown': + if (this.currentAutocompleteFocus < 4) { + this.currentAutocompleteFocus = this.currentAutocompleteFocus + 1; + } + break; + case 'ArrowUp': + if (this.currentAutocompleteFocus > 0) { + this.currentAutocompleteFocus = this.currentAutocompleteFocus - 1; + } + break; + case 'Enter': + // When Enter is pressed and that we are inside the autocomplete list items + if (this.currentAutocompleteFocus > -1) { + this.searchValue = this.optionsAutocomplete[this.currentAutocompleteFocus].caption; + this.selectOption(this.optionsAutocomplete[this.currentAutocompleteFocus]); + } + this.displayAutocomplete = false; + this.inputSearch.nativeElement.blur(); + break; + default: + this.keyUpSubject.next(); + break; + } + } + + // When one autocomplete option is selected + selectOption(option: any) { + this.currentAutocompleteFocus = -1; + this.searchValue = option.caption; + this.addressSelected.emit(option); + } + + searchChanged() { + this.optionsAutocomplete = []; + } + + /* + * Display the autocomplete list: if the list is empty, we request it to ES. + */ + displayAutocompleteList() { + this.displayAutocomplete = true; + if (this.optionsAutocomplete.length < 1) { + this.requestAutocomplete(); + } + } + + requestAutocomplete() { + // tslint:disable-next-line:max-line-length + console.log(this.searchValue); + this.searchAddress.emit(this.searchValue); + } + + resetResearch() { + this.searchValue = ''; + this.searchChanged(); + this.clearAddress.emit(); + } + + // get isLoading() { + // return this._datasetResearchService.isLoading; + // } +} diff --git a/src/app/map/map.module.ts b/src/app/map/map.module.ts index 88bdd7d06fa4deab6256a9c4d9e362c62d0e6227..739f022f034a5bc42a8e56e84fb15378569ef553 100644 --- a/src/app/map/map.module.ts +++ b/src/app/map/map.module.ts @@ -1,26 +1,27 @@ import { NgModule } from '@angular/core'; import { CommonModule, DatePipe } from '@angular/common'; -import { MapComponent } from './components/map.component'; // tslint:disable-next-line:max-line-length import { DataDetailPropertiesComponent } from './components/data-details/data-detail-properties/data-detail-properties.component'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SharedModule } from '../shared/shared.module'; import { MapService } from './services/map.service'; -import { DataDetailsComponent } from './components/data-details/data-details.component'; - +import { GeocoderService } from './services/geocoder.service'; +import { MapComponents } from './components'; @NgModule({ imports: [ CommonModule, FormsModule, SharedModule, + ReactiveFormsModule, ], - declarations: [MapComponent, DataDetailsComponent, DataDetailPropertiesComponent], + declarations: [MapComponents, DataDetailPropertiesComponent], providers: [ MapService, + GeocoderService, DatePipe, ], exports: [ - MapComponent, + MapComponents, ], }) export class MapModule { } diff --git a/src/app/map/services/geocoder.service.ts b/src/app/map/services/geocoder.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbaf248178c5620b77987d9755825b6b58104c3a --- /dev/null +++ b/src/app/map/services/geocoder.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { APP_CONFIG } from '../../core/services/app-config.service'; +import { map } from 'rxjs/operators'; +import { } from 'geojson'; + +@Injectable() +export class GeocoderService { + + geoServiceUrl = APP_CONFIG.backendUrls.geocoder; + constructor(private http: HttpClient) { + } + + searchLocation(term) { + // tslint:disable-next-line: max-line-length + return this.http.get<GeoJSON.FeatureCollection>( + // tslint:disable-next-line:max-line-length + `${this.geoServiceUrl}/api?q=${term}&lat=45.75&lon=4.85&lang=fr&limit=3&osm_tag=:!construction&osm_tag=:!bus_stop`).pipe( + map((res) => { + return res.features.map((item) => { + console.log(item) + // const stop = item.properties.osm_value === 'bus_stop' ? 'Arrêt ' : ''; + const housenumber = item.properties.housenumber ? item.properties.housenumber : null; + const name = item.properties.name ? item.properties.name : ''; + const street = item.properties.street ? item.properties.street : ''; + const city = item.properties.city ? item.properties.city : ''; + const postalCode = item.properties.postcode ? item.properties.postcode : ''; + + let caption = null; + + if (housenumber && street) { + caption = `${housenumber} ${street}, ${city} ${postalCode}`; + } else if (name) { + caption = `${name}, ${city} ${postalCode}`; + } else if (street) { + caption = `${street}, ${city} ${postalCode}`; + } + + if (!caption) { + console.log("couldn't format caption"); + return null; + } + + const obj = { + caption, + value: (item.geometry as GeoJSON.Point).coordinates, + }; + return obj; + }); + }), + ); + } + + // reverseGeocode(coords: number[]) { + // // tslint:disable-next-line:max-line-length + // return this.http.get<FeatureCollection>(this.geoServiceUrl + `/reverse?distance_sort=true&lon=${coords[0]}&lat=${coords[1]}`).map(res => { + // const item = res.features[0]; + // const stop = item.properties.osm_value === 'stop' ? 'Arrêt ' : ''; + // const housenumber = item.properties.housenumber ? item.properties.housenumber + ', ' : ''; + // const name = item.properties.name ? item.properties.name + ' ' : ''; + // const street = item.properties.street ? item.properties.street : ''; + // const city = item.properties.city; + // return `${stop}${housenumber}${name}${street}, ${city}`; + // } + // ); + // } +} diff --git a/src/assets/config/config.json b/src/assets/config/config.json index 7d5fa5186fe6a9be79a2975c1403800a4ddfa6cd..481d951e8b627ef4648af7da597d7287dc5a6f45 100644 --- a/src/assets/config/config.json +++ b/src/assets/config/config.json @@ -10,6 +10,7 @@ "matomo": "https://matomo-intothesky.alpha.grandlyon.com", "elasticsearch": "https://kong-dev.alpha.grandlyon.com/es-consumer-aware", "catalogue": "https://kong-dev.alpha.grandlyon.com/catalogue", - "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses" + "reuses": "https://kong-dev.alpha.grandlyon.com/reuses/reuses", + "geocoder": "https://download.data.grandlyon.com/geocoding/photon" } } \ No newline at end of file diff --git a/src/assets/img/geolocation.svg b/src/assets/img/geolocation.svg new file mode 100644 index 0000000000000000000000000000000000000000..9a724838a69863a3916ec46ec0946a7765890376 --- /dev/null +++ b/src/assets/img/geolocation.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="none"> + <path fill="#464A57" d="M22.3617 13.2106c.8348-.3209 1.6389.5317 1.2696 1.3463l-5.4489 12.0206c-.4317.9524-1.851.6869-1.9092-.3571l-.3207-5.7465-5.7444-.5616c-1.04976-.1026-1.24606-1.5502-.26154-1.9287l12.41514-4.773z"/> +</svg> diff --git a/src/assets/img/position.svg b/src/assets/img/position.svg new file mode 100644 index 0000000000000000000000000000000000000000..658d46c80c5681f5d14c712efab34a4815ab7829 --- /dev/null +++ b/src/assets/img/position.svg @@ -0,0 +1,18 @@ +<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle opacity="0.2" cx="21" cy="21" r="21" fill="#4668AB"/> +<g filter="url(#filter0_d)"> +<circle cx="21" cy="21" r="9" fill="#4668AB"/> +<circle cx="21" cy="21" r="8" stroke="white" stroke-width="2"/> +</g> +<defs> +<filter id="filter0_d" x="4" y="4" width="34" height="34" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/> +<feOffset/> +<feGaussianBlur stdDeviation="4"/> +<feColorMatrix type="matrix" values="0 0 0 0 0.141176 0 0 0 0 0.168627 0 0 0 0 0.247059 0 0 0 0.45 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/i18n/messages.en.xlf b/src/i18n/messages.en.xlf index 6e5a68ce65f04ba097e656d10666406df3ee725d..b8c9972d3368d55f5d05fd56fd51a6358863d083 100644 --- a/src/i18n/messages.en.xlf +++ b/src/i18n/messages.en.xlf @@ -1283,7 +1283,6 @@ Here is the list of the last evolutions of the portal. If you wish to contribute <trans-unit id="reuses.oneDatasetReused" datatype="html"> <source>dataset used</source> <target>dataset used</target> - </trans-unit> </body> </file> </xliff> diff --git a/src/i18n/messages.fr.xlf b/src/i18n/messages.fr.xlf index 8b8dafb8054ed2704dd7a0e7f3382f5a30d8a5d6..180a375c0b3436708e285e9e82fed2077ab87e81 100644 --- a/src/i18n/messages.fr.xlf +++ b/src/i18n/messages.fr.xlf @@ -1272,6 +1272,7 @@ Voici la liste des dernières évolutions du portail. Si vous souhaitez contribu <source>Contact us</source> <target>Contactez-nous</target> </trans-unit> +<<<<<<< HEAD <trans-unit id="reuses.description.part1" datatype="html"> <source>Data from data.grandlyon.com feeds many services.</source> <target>Les données de data.grandlyon.com alimentent de nombreux services.</target> diff --git a/src/i18n/traductions.fr.ts b/src/i18n/traductions.fr.ts index 9a3fdb083736eb8715b185e0b33570f5ad24bd28..9b0b85c60a609ba0d00c81477768479e9a8731fa 100644 --- a/src/i18n/traductions.fr.ts +++ b/src/i18n/traductions.fr.ts @@ -185,6 +185,7 @@ export const geosource = { series: 'ex: ortho', services: 'ex: calculateur', posts: 'ex: traboules', + address: 'Entrer une adresse', }, errorItem: { all: 'document', diff --git a/src/i18n/traductions.ts b/src/i18n/traductions.ts index 0b1e692237a2b465b770343291d5ce62bf1c2b6a..afdc02ca93c73fc8314e407e92e8174b54312e2f 100644 --- a/src/i18n/traductions.ts +++ b/src/i18n/traductions.ts @@ -183,6 +183,7 @@ export const geosource = { series: 'e.g. ortho', services: 'e.g. calculateur', posts: 'e.g. traboules', + address: 'Enter an address', }, errorItem: { all: 'document',