Commit 015e38cb authored by ncastejon's avatar ncastejon
Browse files

Remove metadata model dependency from the map module. Add transition effect...

Remove metadata model dependency from the map module. Add transition effect for the table-map display
parent 481b28b2
<div>
<ng-container *ngIf="metadata">
<app-map [metadata]="metadata"></app-map>
<ng-container *ngIf="mapOptions">
<app-map [mapOptions]="mapOptions"></app-map>
</ng-container>
</div>
\ No newline at end of file
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { MapService } from '../../../../map/services/map.service';
import { DatasetDetailService } from '../../../services';
import { Subscription } from 'rxjs';
import { Metadata } from '../../../models';
import { MapComponent } from '../../../../map/components';
import { MapOptions } from '../../../../map/models/map-options';
import { linkFormats } from '../../../models/metadata.model';
@Component({
selector: 'app-dataset-map',
......@@ -19,6 +21,7 @@ export class DatasetMapComponent implements OnInit {
isSample = false;
datasetSub: Subscription; // Subscription to dataset change
metadata: Metadata;
mapOptions: MapOptions;
constructor(
private _mapService: MapService,
......@@ -27,11 +30,13 @@ export class DatasetMapComponent implements OnInit {
ngOnInit() {
this.metadata = this._datasetDetailService.datasetMetadata;
this.constructMapOptions();
// 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.datasetSub = this._datasetDetailService.dataset$.subscribe(() => {
this.metadata = this._datasetDetailService.datasetMetadata;
this.constructMapOptions();
});
this.isSample = this._datasetDetailService.dataset.editorialMetadata.isSample;
......@@ -40,8 +45,31 @@ export class DatasetMapComponent implements OnInit {
});
}
constructMapOptions() {
if (this.metadata && !this.mapOptions) {
this.mapOptions = {} as MapOptions;
this.mapOptions.vectorService = this.metadata.link.find((e) => { return e.service === linkFormats.wfs; });
this.mapOptions.rasterService = this.metadata.link.find((e) => { return e.service === linkFormats.wms; });
// tslint:disable-next-line: max-line-length
this.mapOptions.bbox = [this.metadata.max_east, this.metadata.max_south, this.metadata.max_west, this.metadata.max_north];
// This is used to remeber what is the previous dataset that the map displayed.
// The reason is to know if we need to display the same map settings (for example the user went to another
// tab), or if we display with default settings (if the user changed the dataset and go to the map).
// This is used by the map.component
if (this._datasetDetailService.lastDatasetIdForMap === this.metadata.dataset_id) {
this.mapOptions.initOptions = false;
} else {
this._datasetDetailService.lastDatasetIdForMap = this.metadata.dataset_id;
this.mapOptions.initOptions = true;
}
}
}
reDraw() {
this.mapComponent.reDraw();
if (this.mapComponent) {
this.mapComponent.reDraw();
}
}
// Generate the url with map options
......
......@@ -73,13 +73,13 @@
</button>
</div>
</div>
<div class="table-map-container">
<div class="table-component-container" *ngIf="hasTable && tableIsEnabled"
[ngClass]="{'is-fullwidth': !hasMap || (hasMap && !mapIsEnabled) }">
<div class="table-map-container"
[ngClass]="{'noMap': !hasMap || (hasMap && !mapIsEnabled), 'noTable': !hasTable || (hasTable && !tableIsEnabled)}">
<div class="table-component-container" [attr.aria-hidden]="!hasTable || (hasTable && !tableIsEnabled)">
<app-dataset-table [selectedProperties]="selectedProperties">
</app-dataset-table>
</div>
<div class="map-component-container" *ngIf="hasMap && mapIsEnabled">
<div class="map-component-container" [attr.aria-hidden]="!hasMap || (hasMap && !mapIsEnabled)">
<app-dataset-map></app-dataset-map>
</div>
</div>
......
......@@ -134,18 +134,29 @@
.table-component-container {
max-width: 50vw;
min-width: 0;
height: 100%;
border-right: 1px solid $grey-super-light-color;
transition: all 0.5s;
}
.map-component-container {
flex-grow: 1;
height: 100%;
}
&.is-fullwidth {
.table-map-container.noMap {
.table-component-container {
max-width: 99vw;
min-width: 99vw;
border-right: none;
}
}
.map-component-container {
flex-grow: 1;
height: 100%;
.table-map-container.noTable {
.table-component-container {
max-width: 0;
}
}
.is-fullscreen {
......
......@@ -59,9 +59,11 @@ export class DatasetTableMapComponent implements OnInit, OnDestroy {
if (this.hasTable && this.tableIsEnabled) {
this.mapIsEnabled = !this.mapIsEnabled;
setTimeout(() => {
this.datasetMapComponent.reDraw();
if (this.datasetMapComponent) {
// this.datasetMapComponent.reDraw();
}
// tslint:disable-next-line: align
}, 1);
}, 501);
}
}
......@@ -71,7 +73,7 @@ export class DatasetTableMapComponent implements OnInit, OnDestroy {
setTimeout(() => {
this.datasetMapComponent.reDraw();
// tslint:disable-next-line: align
}, 1);
}, 501);
}
}
......
......@@ -2,7 +2,7 @@
@import "../../../../../../node_modules/bulma/sass/utilities/_all";
.data-table {
display: inline-grid;
display: grid;
max-width: 100%;
height: 100%;
overflow: auto;
......@@ -35,6 +35,8 @@
justify-content: flex-start;
border-bottom: 1px solid $grey-super-light-color;
padding-left: 0.75rem;
// To be on top of the tooltip element (which has a position: relative)
z-index: 1;
span {
cursor: pointer;
}
......
......@@ -69,7 +69,7 @@ export class DatasetTableComponent implements OnInit, OnDestroy {
getStyle() {
return {
// 'grid-template-columns': `repeat(${this.selectedProperties.length}, minmax(50px, 1fr)`,
'grid-template-columns': `repeat(${this.selectedProperties.length}, max-content`,
'grid-template-columns': `repeat(${this.selectedProperties.length - 1}, max-content) 1fr`,
};
}
......
......@@ -33,6 +33,9 @@ export class DatasetDetailService {
// Copy the initial value to be able to reinitialize at any moment
private _elasticSearchOptions: ElasticsearchOptions;
// Used to avoid to construct the map multiple times for the same datasetId
private _lastDatasetIdForMap: string;
constructor(
private _errorService: ErrorService,
private _elasticsearchService: ElasticsearchService,
......@@ -208,6 +211,14 @@ export class DatasetDetailService {
return this._dataset;
}
get lastDatasetIdForMap(): string {
return this._lastDatasetIdForMap;
}
set lastDatasetIdForMap(datasetId: string) {
this._lastDatasetIdForMap = datasetId;
}
get datasetEditorialMetadata(): EditorialMetadata {
return this._dataset.editorialMetadata;
}
......
<div id="map" class="mapbox-map"
[ngClass]="{'display-details': selectedData !== null && displayDataDetails === true, 'hide-details': displayDataDetails === false && selectedData !== null}">
<div id="menu" *ngIf="displayControls">
<div id="menu">
<div class="columns is-mobile is-marginless">
<div class="column is-narrow">
<div class="buttons has-addons column-content">
......
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import * as mapboxgl from 'mapbox-gl';
import { Subscription } from 'rxjs';
import { Metadata } from '../../geosource/models';
import { MapService } from '../services/map.service';
import { settings } from '../settings';
import { geosource } from '../../../i18n/traductions';
import { linkFormats } from '../../geosource/models/metadata.model';
import { GeocoderService } from '../services/geocoder.service';
import { MapOptions } from '../models/map-options';
@Component({
selector: 'app-map',
......@@ -16,7 +16,7 @@ import { GeocoderService } from '../services/geocoder.service';
export class MapComponent implements OnInit, OnDestroy {
@Input() metadata: Metadata;
@Input() mapOptions: MapOptions;
settings = settings;
linkFormats = linkFormats;
......@@ -38,11 +38,9 @@ export class MapComponent implements OnInit, OnDestroy {
totalData = 0;
previousDatasetId: string;
currentLayerType: string;
availableLayers: string[];
baseLayer3d = 1;
displayControls = false;
selectedData = null; // Contains the properties of the selected feature
geolocation = false;
......@@ -63,7 +61,7 @@ export class MapComponent implements OnInit, OnDestroy {
}
this.initilizeLanguage();
this.initAvailableLayers();
// this.initAvailableLayers();
this.constructMap();
// Events received here contain a state (if the data-detail panel has to be displayed or not)
......@@ -78,8 +76,9 @@ export class MapComponent implements OnInit, OnDestroy {
}
reDraw() {
console.log('resize');
this.map.resize();
if (this.map) {
this.map.resize();
}
}
// To avoid call the constructMap when we left the component
......@@ -89,19 +88,6 @@ export class MapComponent implements OnInit, OnDestroy {
this._mapService.destroyMap();
}
initAvailableLayers() {
this.availableLayers = [];
this.metadata.link.forEach((link) => {
this.availableLayers.push(link.service);
});
if (this.availableLayers.includes(linkFormats.wfs)) {
this.currentLayerType = linkFormats.wfs;
} else {
this.currentLayerType = linkFormats.wms;
}
}
searchAdress(value: string) {
// Subscribe to search location input
const item = this.searchLocationResult.find(e => e.caption === value);
......@@ -163,16 +149,10 @@ export class MapComponent implements OnInit, OnDestroy {
// - load Mapbox style file to get the styles and the base layers (vector, plan, satellite)
constructMap() {
if (this.metadata) {
// Get the bounding box
const bounds = new mapboxgl.LngLatBounds(
[this.metadata.max_east, this.metadata.max_south, this.metadata.max_west, this.metadata.max_north],
);
if (this.mapOptions) {
// Set the basic and default options
const options = {
bounds,
const mapboxOptions = {
container: 'map',
center: [4.85, 45.75] as mapboxgl.LngLatLike,
zoom: 12,
......@@ -192,38 +172,40 @@ export class MapComponent implements OnInit, OnDestroy {
// 5: selectedBaseLayerId
// If parameters inside the url exists (from permalink), we set options form it
if (parameters.length === 6) {
options.zoom = parseFloat(parameters[0]);
options.center = [parseFloat(parameters[2]), parseFloat(parameters[1])];
options['bearing'] = parseFloat(parameters[3]);
options['pitch'] = parseFloat(parameters[4]);
mapboxOptions.zoom = parseFloat(parameters[0]);
mapboxOptions.center = [parseFloat(parameters[2]), parseFloat(parameters[1])];
mapboxOptions['bearing'] = parseFloat(parameters[3]);
mapboxOptions['pitch'] = parseFloat(parameters[4]);
const baseLayer = this.settings.baseLayers.find(e => e.id === parseInt(parameters[5], 10));
if (baseLayer !== undefined) {
this.selectedBaseLayer = baseLayer;
}
}
// If the map is already constructed inside the service (for example go to Info tab and come back to Map)
// we set the options from the existing map
// we set the mapboxOptions from the existing map
// If the datasetId is different from previous navigation (for example the user comes from the result page),
// we will display the map with default settings.
// tslint:disable-next-line:brace-style
else if (this._mapService.mapIsConstructed && this._mapService.previousDatasetId === this.metadata.dataset_id) {
options.zoom = this._mapService.map.getZoom();
options.center = [this._mapService.map.getCenter().lng, this._mapService.map.getCenter().lat];
options['bearing'] = this._mapService.map.getBearing();
options['pitch'] = this._mapService.map.getPitch();
else if (this._mapService.mapIsConstructed && !this.mapOptions.initOptions) {
mapboxOptions.zoom = this._mapService.map.getZoom();
mapboxOptions.zoom = this._mapService.map.getZoom();
mapboxOptions.center = [this._mapService.map.getCenter().lng, this._mapService.map.getCenter().lat];
mapboxOptions['bearing'] = this._mapService.map.getBearing();
mapboxOptions['pitch'] = this._mapService.map.getPitch();
this.selectedBaseLayer = this._mapService.selectedBaseLayer;
} else {
// If default options with no zoom or center values, we set the bbox
const bounds = new mapboxgl.LngLatBounds(this.mapOptions.bbox);
mapboxOptions['bounds'] = bounds;
}
options['style'] = `assets/mapbox-gl-styles/${this.selectedBaseLayer.fileName}`;
mapboxOptions['style'] = `assets/mapbox-gl-styles/${this.selectedBaseLayer.fileName}`;
// Create the map with the associated style Mapbox file
this.displayControls = true;
this.map = this._mapService.createMap(this.metadata, url, this.selectedBaseLayer, this.displayControls, options);
// tslint:disable-next-line: max-line-length
this.map = this._mapService.createMap(this.mapOptions, url, this.selectedBaseLayer, mapboxOptions);
this.map.on('style.load', () => {
this._mapService.addLayers();
this.currentLayerType = this._mapService.currentLayerType;
this._mapService.previousDatasetId = this.metadata.dataset_id;
});
}
}
......@@ -255,10 +237,6 @@ export class MapComponent implements OnInit, OnDestroy {
}
}
changeMapPitchValue(val: number) {
this.map.setPitch(val);
}
copyMaplink(inputElement) {
inputElement.select();
document.execCommand('copy');
......
export interface MapOptions {
vectorService: GeoService;
rasterService: GeoService;
bbox: [number, number, number, number];
initOptions: boolean;
}
export interface GeoService {
name: string;
url: string;
}
import { Injectable } from '@angular/core';
import { Observable, Subject, BehaviorSubject, fromEvent, Subscription } from 'rxjs';
import { Metadata, IMetadataLink } from '../../geosource/models';
import { Notification } from '../../core/models';
import * as mapboxgl from 'mapbox-gl';
import { debounceTime } from 'rxjs/operators';
......@@ -8,8 +7,7 @@ import { NotificationService } from '../../core/services';
import { notificationMessages } from '../../../i18n/traductions.fr';
import { Minimap } from '../components/minimap-control';
import { settings } from '../settings';
import * as cloneDeep from 'lodash.clonedeep';
import { linkFormats } from '../../geosource/models/metadata.model';
import { MapOptions, GeoService } from '../models/map-options';
@Injectable()
export class MapService {
......@@ -19,14 +17,12 @@ export class MapService {
private url: string;
selectedBaseLayer: any;
metadata: Metadata;
uriWFS: IMetadataLink;
uriWMS: IMetadataLink;
vectorService: GeoService;
rasterService: GeoService;
eventPopupAdded = false;
minimap: Minimap;
mapIsConstructed: boolean = false;
_currentLayerType: string;
// Map
featureColor: string = '#1d92ff';
......@@ -36,12 +32,6 @@ export class MapService {
hoveredFeatureId: string;
highlightedFeatureId: string;
// This is used to remeber what is the previous dataset that the map displayed.
// The reason is to know if we need to display the same map settings (for example the user went to another
// tab), or if we display with default settings (if the user changed the dataset and go to the map).
// This is used by the map.component
_previousDatasetId: string;
private _mapSubject = new Subject<any>();
private _panelState = new BehaviorSubject<any>({ state: false });
private _errorSubscription: Subscription;
......@@ -63,11 +53,10 @@ export class MapService {
) { }
createMap(
metadata: Metadata,
url: string, baseLayer: any, addControls: boolean, options?: mapboxgl.MapboxOptions): mapboxgl.Map {
this.metadata = metadata;
this.uriWFS = this.metadata.link.find((e) => { return e.service === linkFormats.wfs; });
this.uriWMS = this.metadata.link.find((e) => { return e.service === linkFormats.wms; });
mapOptions: MapOptions,
url: string, baseLayer: any, options?: mapboxgl.MapboxOptions): mapboxgl.Map {
this.vectorService = mapOptions.vectorService;
this.rasterService = mapOptions.rasterService;
// Reset to false in ordre to set event listener
this.eventPopupAdded = false;
......@@ -94,32 +83,31 @@ export class MapService {
);
});
if (addControls) {
this._map.on('load', () => {
if (this._map.isSourceLoaded) {
const scale = new mapboxgl.ScaleControl({
maxWidth: 80,
unit: 'metric',
});
this._map.addControl(scale, 'bottom-right');
const nav = new mapboxgl.NavigationControl();
this._map.addControl(nav, 'top-right');
this.minimap = new Minimap(
{
style: `assets/mapbox-gl-styles/${this.selectedBaseLayer.fileName}`,
classes: 'is-hidden-mobile',
height: '150px',
width: '265px',
},
);
this._map.on('load', () => {
if (this._map.isSourceLoaded) {
const scale = new mapboxgl.ScaleControl({
maxWidth: 80,
unit: 'metric',
});
this._map.addControl(scale, 'bottom-right');
const nav = new mapboxgl.NavigationControl();
this._map.addControl(nav, 'top-right');
this.minimap = new Minimap(
{
style: `assets/mapbox-gl-styles/${this.selectedBaseLayer.fileName}`,
classes: 'is-hidden-mobile',
height: '150px',
width: '265px',
},
);
// this._map.addControl(this.minimap, 'bottom-left');
}
this._mapUpdated.next(this.totalData);
});
// this._map.addControl(this.minimap, 'bottom-left');
}
this._mapUpdated.next(this.totalData);
});
}
this._mapSubject.next();
this.mapIsConstructed = true;
......@@ -127,16 +115,10 @@ export class MapService {
return this._map;
}
// Three strategies to display the map depending the number of data and the type of service:
// - if WFS exists and dataNumber < 100 000: we display 1000 features
// Then at every moveend and zoomend events we notify the component (to display the update button for example)
// - if WFS exists but data > 100 000, or if WFS no exist: we display the WMS layer
addLayers() {
this._currentLayerType = linkFormats.wfs;
const domain = this.uriWFS.url.includes('rdata') ? 'rdata' : 'grandlyon';
const domain = this.vectorService.url.includes('rdata') ? 'rdata' : 'grandlyon';
const url = `https://download.data.grandlyon.com/mvt/${domain}?LAYERS` +
`=${this.uriWFS.name}&map.imagetype=mvt&tilemode=gmap&tile={x}+{y}+{z}&mode=tile`;
`=${this.vectorService.name}&map.imagetype=mvt&tilemode=gmap&tile={x}+{y}+{z}&mode=tile`;
this._map.addSource('vector-source', {
type: 'vector',
......@@ -174,7 +156,7 @@ export class MapService {
this._map.setFeatureState(
{
source: 'vector-source',
sourceLayer: this.uriWFS.name,
sourceLayer: this.vectorService.name,
id: featureId,
},
state);
......@@ -197,8 +179,8 @@ export class MapService {
this._map.addSource('wms-source', {
type: 'raster',
tiles: [
`${this.uriWMS.url}?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&` +
`srs=EPSG:3857&width=256&height=256&transparent=true&layers=${this.uriWMS.name}`,
`${this.rasterService.url}?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&` +
`srs=EPSG:3857&width=256&height=256&transparent=true&layers=${this.rasterService.name}`,
],
tileSize: 256,
});
......@@ -219,7 +201,7 @@ export class MapService {
id: 'polygon-features',
type: 'fill',
source: 'vector-source',
'source-layer': this.uriWFS.name,
'source-layer': this.vectorService.name,
paint: {
'fill-color': ['case',
['boolean', ['feature-state', 'hover'], false],
......@@ -239,7 +221,7 @@ export class MapService {
id: 'line-features',
type: 'line',
source: 'vector-source',
'source-layer': this.uriWFS.name,
'source-layer': this.vectorService.name,
layout: {
'line-cap': 'round',
'line-join': 'round',
......@@ -271,7 +253,7 @@ export class MapService {
id: 'point-features',
type: 'circle',
source: 'vector-source',
'source-layer': this.uriWFS.name,
'source-layer': this.vectorService.name,
filter: ['match', ['geometry-type'], ['Point'], true, false],
paint: {
'circle-color': ['case',
......@@ -342,7 +324,7 @@ export class MapService {
// Display hover state only if not already highlighted
const state = this._map.getFeatureState({
source: 'vector-source',
sourceLayer: this.uriWFS.name,
sourceLayer: this.vectorService.name,
id: this.hoveredFeatureId,
});
if (!state.highlight) {
......@@ -446,20 +428,8 @@ export class MapService {
return this._mapToUpdate.asObservable();
}
get currentLayerType() {