Commit 3991b454 authored by FORESTIER Fabien's avatar FORESTIER Fabien
Browse files

Replace data details popup by the right panel

parent 56de2fb8
<div *ngFor="let key of keys; let odd=odd; let even=even;" class="property" [ngClass]="{'even': even, 'odd': odd}">
<p class="key" *ngIf="!inputIsArray()">{{ key }}</p>
<ng-container [ngSwitch]="getTypeOfProperty(key)">
<div *ngSwitchCase="'object'">
<app-data-detail-properties [properties]="properties[key]"></app-data-detail-properties>
</div>
<div *ngSwitchCase="'array'">
<app-data-detail-properties [properties]="properties[key]"></app-data-detail-properties>
</div>
<div *ngSwitchDefault>{{ properties[key] }}</div>
</ng-container>
</div>
\ No newline at end of file
@import "../../../../../scss/variables";
.key {
font-weight: bold;
}
.property {
border: 1px solid #e8ecef;
padding: 0.625rem;
font-size: $size-7;
&.odd {
background-color: white;
}
&.even {
background-color: #f9f9f9;
}
}
\ No newline at end of file
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DataPropertiesComponent } from './data-properties.component';
describe('DataPropertiesComponent', () => {
let component: DataPropertiesComponent;
let fixture: ComponentFixture<DataPropertiesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DataPropertiesComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DataPropertiesComponent);
component = fixture.componentInstance;
component.object = {
gid: 1,
fuck: 2,
};
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-data-detail-properties',
templateUrl: './data-detail-properties.component.html',
styleUrls: ['./data-detail-properties.component.scss'],
})
export class DataDetailPropertiesComponent implements OnInit {
@Input() properties: Object;
keys: string[];
constructor() { }
ngOnInit() {
// Initilize the array with the keys of the json object received in the input
this.keys = this.getKeys(this.properties);
}
getKeys(obj) {
const keys = Object.keys(obj);
// sort by alphabetical order
keys.sort();
return keys;
}
// Return the type of the 'key' property of the input object
getTypeOfProperty(key: string) {
const type = typeof this.properties[key];
let res: string;
if (type === 'object' && Array.isArray(this.properties[key])) {
res = 'array';
} else {
res = type;
}
return res;
}
// Return true if the 'object' Input of the component is an array
inputIsArray() {
return Array.isArray(this.properties);
}
isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
}
<div class="data-details">
<button class="close-button button is-danger" (click)="closeSelf()"><i class="fas fa-angle-right"></i></button>
<div class="properties">
<app-data-detail-properties [properties]="properties"></app-data-detail-properties>
</div>
</div>
\ No newline at end of file
@import "../../../../scss/variables";
.data-details {
background-color: white;
width: 100%;
height: 100%;
padding: 20px;
box-shadow: -2px 0px 5px -1px rgba(0,0,0,0.3);
position: relative;
.close-button {
// background-color: $red;
// color: white;
border-radius: unset;
width: 2rem;
left: -2rem;
box-shadow: -2px 0px 5px -1px rgba(0,0,0,0.3);
position: absolute;
}
.properties {
margin-top: 0.625rem;
overflow-y: auto;
width: 100%;
height: 100%;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DataDetailsComponent } from './data-details.component';
describe('DataDetailsComponent', () => {
let component: DataDetailsComponent;
let fixture: ComponentFixture<DataDetailsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DataDetailsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DataDetailsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-data-details',
templateUrl: './data-details.component.html',
styleUrls: ['./data-details.component.scss'],
})
export class DataDetailsComponent implements OnInit {
@Input() properties;
@Output() close = new EventEmitter<boolean>();
constructor() { }
ngOnInit() {
}
get propertiesKeys() {
let keys;
if (this.properties) {
keys = Object.keys(this.properties);
}
return keys;
}
closeSelf() {
this.close.emit(true);
}
}
<div class="container">
<div id="map" [ngClass]="{'fullscreen': fullscreen===true}">
<div id="menu" *ngIf="displayControls">
<div class="columns is-mobile is-gapless">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content" (click)="displayLayersMenu = !displayLayersMenu">
<button class="mapboxgl-ctrl-icon btn-layers" type="button"></button>
<div id="map" class="" [ngClass]="{'fullscreen': fullscreen===true, 'display-details': selectedData !== null && displayDataDetails === true, 'hide-details': displayDataDetails === false && selectedData !== null}">
<div id="menu" *ngIf="displayControls">
<div class="columns is-mobile is-gapless">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content" (click)="displayLayersMenu = !displayLayersMenu">
<button class="mapboxgl-ctrl-icon btn-layers" type="button"></button>
</div>
</div>
</div>
<div class="column is-narrow" *ngIf="displayLayersMenu">
<div class="buttons has-addons layers-options column-content">
<ng-container *ngFor="let l of env.baseLayers">
<button class="button" [disabled]="l.id === selectedBaseLayer.id" [ngClass]="{'is-selected': l.id === selectedBaseLayer.id, 'is-success': l.id === selectedBaseLayer.id}"
(click)="switchLayer(l)" type="button">
{{ l.labels[lang] }}
</button>
</ng-container>
<div class="column is-narrow" *ngIf="displayLayersMenu">
<div class="buttons has-addons layers-options column-content">
<ng-container *ngFor="let l of env.baseLayers">
<button class="button" [disabled]="l.id === selectedBaseLayer.id" [ngClass]="{'is-selected': l.id === selectedBaseLayer.id, 'is-success': l.id === selectedBaseLayer.id}"
(click)="switchLayer(l)" type="button">
{{ l.labels[lang] }}
</button>
</ng-container>
</div>
</div>
</div>
</div>
<div class="columns is-mobile is-gapless">
<div class="column is-narrow">
<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 class="columns is-mobile is-gapless">
<div class="column is-narrow">
<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>
<div class="columns is-mobile is-gapless" *ngIf="selectedBaseLayer.id === baseLayer3d">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content">
<button class="mapboxgl-ctrl-icon btn-3d" [ngClass]="{'is-active': display3d === true}" type="button" (click)="switch3DLayer()">3D</button>
<div class="columns is-mobile is-gapless" *ngIf="selectedBaseLayer.id === baseLayer3d">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content">
<button class="mapboxgl-ctrl-icon btn-3d" [ngClass]="{'is-active': display3d === true}" type="button" (click)="switch3DLayer()">3D</button>
</div>
</div>
</div>
</div>
<div class="columns is-mobile is-gapless is-hidden-tablet">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content" (click)="displayPitchSlider = !displayPitchSlider">
<button class="mapboxgl-ctrl-icon btn-pitch" type="button"></button>
<div class="columns is-mobile is-gapless is-hidden-tablet">
<div class="column is-narrow">
<div class="mapboxgl-ctrl-group column-content" (click)="displayPitchSlider = !displayPitchSlider">
<button class="mapboxgl-ctrl-icon btn-pitch" type="button"></button>
</div>
</div>
</div>
<div class="column" *ngIf="displayPitchSlider">
<div class="pitch-input-container column-content">
<input class="slider is-success pitch-input" step="1" min="0" max="60" [value]="map.getPitch()" type="range" (input)="changeMapPitchValue($event.target.value)">
<div class="column" *ngIf="displayPitchSlider">
<div class="pitch-input-container column-content">
<input class="slider is-success pitch-input" step="1" min="0" max="60" [value]="map.getPitch()" type="range" (input)="changeMapPitchValue($event.target.value)">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="data-details-wrapper" *ngIf="selectedData !== null"
[ngClass]="{'data-details-opened': selectedData !== null && displayDataDetails === true, 'data-details-closed': displayDataDetails === false}">
<app-data-details (close)="closeDataDetails()" [properties]="selectedData"></app-data-details>
</div>
</div>
</div>
\ No newline at end of file
@import "../../../scss/variables";
#map {
background-color: white;
&.display-details {
::ng-deep .mapboxgl-ctrl-top-right, ::ng-deep .mapboxgl-ctrl-bottom-right {
animation: slide-controls-to-left 200ms;
animation-fill-mode: forwards;
@keyframes slide-controls-to-left {
from {
transform: translateX(0px);
}
to {
transform: translateX(-380px);
}
}
}
}
&.hide-details {
::ng-deep .mapboxgl-ctrl-top-right, ::ng-deep .mapboxgl-ctrl-bottom-right {
animation: slide-controls-to-right 200ms;
animation-fill-mode: forwards;
@keyframes slide-controls-to-rights {
from {
transform: translateX(-380px);
}
to {
transform: translateX(0px);
}
}
}
}
.data-details-opened {
animation: open_details 200ms;
animation-fill-mode: forwards;
visibility: visible;
}
.data-details-closed {
animation: close_details 200ms;
animation-fill-mode: forwards;
}
@keyframes open_details {
from {
transform: translateX(400px);
}
to {
transform: translateX(0px);
}
}
@keyframes close_details {
from {
transform: translateX(0px);
}
to {
transform: translateX(400px);
}
}
}
#map.mapboxgl-map {
......@@ -158,3 +221,16 @@
z-index: 999;
}
.data-details-wrapper {
position: absolute;
width: 350px;
height: 95%;
right: 0;
top: 2.5%;
z-index: 3;
max-width:80%;
app-data-details {
max-height: 100%;
}
}
......@@ -25,10 +25,12 @@ export class MapComponent implements OnInit, OnDestroy {
selectedBaseLayer = this.env.baseLayers[this.env.defaultBaseLayer];
displayLayersMenu = false; // Boolean used to hide or display the Layers menu
displayPitchSlider = false;
displayDataDetails = false; // Wether the data details should be displayed or not
display3d = false;
baseLayer3d = 1;
displayControls = false;
mapIsConstructed = false;
selectedData = null; // Contains the properties of the selected feature
fullscreen = false;
......@@ -42,6 +44,15 @@ export class MapComponent implements OnInit, OnDestroy {
this.constructMap();
// Events received here contain a state (if the data-detail panel has to be displayed or not)
// And the properties of the selected feature if one is selected
this._mapService.panelState$.subscribe((dataDetails) => {
this.displayDataDetails = dataDetails.state;
if (dataDetails.properties !== undefined) {
this.selectedData = dataDetails.properties;
}
});
// Subcribe to the dataset changes in the service. When the dataset is loaded
// (with the metadata), we construct the map and display the features
this.sub = this._datasetDetailService.dataset$.subscribe(() => {
......@@ -76,7 +87,7 @@ export class MapComponent implements OnInit, OnDestroy {
// 4: pitch
// 5: selectedBaseLayerId
const baseLayer = this.env.baseLayers.find(e => e.id === parseInt(parameters[5], 10));
const baseLayer = this.env.baseLayers.find(e => e.id === parseInt(parameters[5], 10));
if (baseLayer !== undefined) {
this.selectedBaseLayer = baseLayer;
}
......@@ -145,4 +156,9 @@ export class MapComponent implements OnInit, OnDestroy {
setTimeout(() => { this.map.resize(); }, 1);
}
// Close the data-details panel
closeDataDetails() {
this._mapService.closePanel();
}
}
import { NgModule } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common';
import { MapComponent } from './components/map.component';
import { DataDetailPropertiesComponent } from './components/data-details/data-detail-properties/data-detail-properties.component';
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';
@NgModule({
imports: [
......@@ -11,7 +13,7 @@ import { MapService } from './services/map.service';
FormsModule,
SharedModule,
],
declarations: [MapComponent],
declarations: [MapComponent, DataDetailsComponent, DataDetailPropertiesComponent],
providers: [
MapService,
DatePipe,
......
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Observable, Subject, BehaviorSubject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Metadata, IMetadataLink } from '../../geosource/models';
import { Notification } from '../../core/models';
import * as mapboxgl from 'mapbox-gl';
import { map } from 'rxjs/operators';
import { map, debounceTime, last, bufferTime, first, filter } from 'rxjs/operators';
import { ElasticsearchService } from '../../geosource/services/elasticsearch.service';
import { NotificationService } from '../../core/services';
import { errors } from '../../../i18n/error-messages/error-messages.fr';
......@@ -25,6 +25,9 @@ export class MapService {
minimap: Minimap;
private _mapSubject = new Subject<any>();
private _panelState = new BehaviorSubject<any>({ state: false });
selectedFeature; // Contains the gid of the selected feature
constructor(
private _http: HttpClient,
......@@ -35,6 +38,10 @@ export class MapService {
createMap(url: string, baseLayer: any, addControls: boolean, options?: mapboxgl.MapboxOptions): mapboxgl.Map {
this.metadata = this._datasetDetailService.datasetMetadata;
// Reset to false in ordre to set event listener
this.eventPopupAdded = false;
// Re-initialize panel state
this._panelState.next({ state: false });
this.selectedBaseLayer = baseLayer;
......@@ -53,7 +60,7 @@ export class MapService {
maxWidth: 80,
unit: 'metric',
});
this.map.addControl(scale);
this.map.addControl(scale, 'bottom-right');
const nav = new mapboxgl.NavigationControl();
this.map.addControl(nav, 'top-right');
......@@ -62,10 +69,12 @@ export class MapService {
{
style: `assets/mapbox-gl-styles/${this.selectedBaseLayer.fileName}`,
classes: 'is-hidden-mobile',
height: '150px',
width: '265px',
},
);
this.map.addControl(this.minimap);
this.map.addControl(this.minimap, 'bottom-left');
}
});
}
......@@ -242,6 +251,63 @@ export class MapService {
filter: ['==', '$type', 'LineString'],
});
// For 'LineString' feature selected highlight
this.map.addLayer({
id: 'line-features-highlight',
type: 'line',
source: 'wfs-polygon',
layout: {
'line-cap': 'round',
'line-join': 'round',
visibility: 'none',
},
paint: {
'line-color': '#ff0000',
'line-width': 3,
'line-opacity': 0.8,
},
});
// For 'Polygon' feature highlighted
this.map.addLayer({
id: 'polygon-features-highlight',
type: 'fill',
layout: {
visibility: 'none',
},
source: 'wfs-polygon',
paint: {
'fill-color': '#00ff00',
'fill-opacity': 0.4,
},
filter: ['==', '$type', 'Polygon'],
});
this.map.loadImage('./assets/img/marker-red.png', (error, image) => {
if (error) throw error;
this.map.addImage('marker-highlighted', image);
// Add layer + style for the unclustered points highlighted
this.map.addLayer({
id: 'unclustered-point-highlighted',
type: 'symbol',
source: 'wfs-clustered-points',
filter: ['!has', 'point_count'],
layout: {
'icon-image': 'marker-highlighted',
'icon-size': 1,
'icon-anchor': 'bottom',
'icon-allow-overlap': true,
visibility: 'none',
},
paint: {
'text-color': '#202',
'text-halo-color': '#fff',
'text-halo-width': 2,
},
});
});