Commit 3e0645e5 authored by ext.sopra.ncastejon's avatar ext.sopra.ncastejon
Browse files

Add the 3D layer. Available at zoom 14+

parent b6d78546
......@@ -6,7 +6,6 @@ import { geosource, notificationMessages } from '../../../i18n/traductions';
import { ErrorService } from '../../core/services';
import { APP_CONFIG } from '../../core/services/app-config.service';
import { scopesResearch } from '../../shared/variables';
// tslint:disable-next-line: max-line-length
import { Aggregation, ElasticsearchOptions, Filter, IElasticsearchResponse, IPostsESOptions } from '../models';
@Injectable()
......@@ -128,7 +127,7 @@ export class ElasticsearchService {
}
if (options.searchString !== '') {
const searchString = this.escapeSpecialCharacters(options.searchString, options.fromAutocompletion);
const searchString = this.escapeSpecialCharacters(options.searchString, options.fromAutocompletion, 'AND');
body.query.bool['must'] = [
{
query_string: {
......@@ -622,7 +621,7 @@ export class ElasticsearchService {
/**
* Escape special characters except logical operators.
* */
escapeSpecialCharacters(searchString, fromAutocompletion) {
escapeSpecialCharacters(searchString: string, fromAutocompletion: boolean, joinOperator = '+') {
let escapedSearchString = '';
/** If the request:
......@@ -639,7 +638,7 @@ export class ElasticsearchService {
// We join each words with the '+' logical operator to accurate the results
const words = escapedSearchString.split(/\s+/);
escapedSearchString = words.join(' + ');
escapedSearchString = words.join(` ${joinOperator} `);
/** If not all this, we don't escape the "()" (and will be interpreted as logical characters by ES)
* but we still do escape some other special characters
......
......@@ -24,6 +24,7 @@
</svg>
</ng-template>
</button>
<div class="buttons has-addons column-content is-hidden-mobile">
<ng-container *ngFor="let l of settings.baseLayers">
<button class="button selectBase" [disabled]="l.id === selectedBaseLayer.id"
......@@ -34,6 +35,12 @@
</ng-container>
</div>
</div>
<div class="button-3d" *ngIf="displayButton3d">
<button class="button" [ngClass]="{'is-active': display3d}"
(click)="switch3DLayer()" *ngIf="selectedBaseLayer.id===settings.baseLayers[0].id">
3D
</button>
</div>
<div class="column is-narrow is-hidden-mobile">
<app-search-address [optionsAutocomplete]="searchLocationResult" (searchAddress)="searchAdress($event)"
(addressSelected)="flyTo($event)" (clearAddress)="removeMarker()">
......@@ -43,20 +50,17 @@
</div>
<div class="geolocation-container">
<button class="geolocation button is-medium" type="button"
i18n-aria-label="@@dataset.detail.map.center" aria-label="Center to my position"
(click)="centerToMyPosition()">
<button class="geolocation button is-medium" type="button" i18n-aria-label="@@dataset.detail.map.center"
aria-label="Center to my position" (click)="centerToMyPosition()">
</button>
</div>
<div class="copy-map">
<input type="text" class="input" id="mapUrlCopy" i18n-aria-label="@@dataset.detail.map.share"
aria-label="Share the map"
[value]="mapUrl()" #mapUrlElement>
aria-label="Share the map" [value]="mapUrl()" #mapUrlElement>
<button class="button is-medium has-tooltip-left tooltip" i18n-aria-label="@@dataset.detail.map.share"
aria-label="Share the map"
type="button" (click)="copyMaplink(mapUrlElement)"
[attr.data-tooltip]="shareMessage">
aria-label="Share the map" type="button" (click)="copyMaplink(mapUrlElement)" [attr.data-tooltip]="shareMessage">
<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"
......
......@@ -61,6 +61,23 @@
border: none;
}
.button-3d {
position: absolute;
top: 50px;
right: 0;
@media screen and (min-width: $tablet) {
left: 0;
}
& > .button.is-active {
background-color: $blue-color;
color: white;
}
}
.copy-map,
.geolocation-container {
position: absolute;
......
......@@ -31,6 +31,7 @@ export class MapComponent implements OnInit, OnDestroy {
// Key of the lay to be displayed from baseLayers in environment files
selectedBaseLayer = this.settings.baseLayers[this.settings.defaultBaseLayer];
displayPitchSlider = false;
displayButton3d = false;
display3d = false;
// Attributes to manage the display of the pudate map button
......@@ -39,8 +40,6 @@ export class MapComponent implements OnInit, OnDestroy {
previousDatasetId: string;
availableLayers: string[];
baseLayer3d = 1;
geolocation = false;
searchLocationResult = [];
......@@ -206,12 +205,30 @@ export class MapComponent implements OnInit, OnDestroy {
this.map.on('style.load', () => {
this._mapService.addLayers();
});
this.map.on('zoomend', () => {
if (this.map.getZoom() < 14) {
this.displayButton3d = false;
if (this.map.getLayer('3d-layer')) {
// this.display3d = false;
this.map.removeLayer('3d-layer');
}
} else if (this.map.getZoom() > 14) {
this.displayButton3d = true;
if (this.display3d) {
this._mapService.switch3DLayer();
}
}
});
}
}
switchLayer(baseLayer) {
this.selectedBaseLayer = baseLayer;
this._mapService.switchLayer(baseLayer);
this.display3d = false;
}
// [WARNING] This toggle only works with two base layers
......@@ -222,6 +239,7 @@ export class MapComponent implements OnInit, OnDestroy {
this.selectedBaseLayer = this.settings.baseLayers[0];
}
this._mapService.switchLayer(this.selectedBaseLayer);
this.display3d = false;
}
// Looks for the language to be used, if not indicated in the url takes the navigator default language
......@@ -239,7 +257,7 @@ export class MapComponent implements OnInit, OnDestroy {
switch3DLayer() {
if (this.map.isStyleLoaded()) {
if (this.map.getSource('openmaptiles')) {
if (this.map.getSource('3d-source')) {
this.display3d = !this.display3d;
this._mapService.switch3DLayer();
}
......
......@@ -8,12 +8,10 @@ import { Notification } from '../../core/models';
import { NotificationService } from '../../core/services';
import { APP_CONFIG } from '../../core/services/app-config.service';
import { DataType, MapOptions } from '../models/map-options';
import { settings } from '../settings';
@Injectable()
export class MapService {
settings = settings;
private _map: mapboxgl.Map;
private url: string;
selectedBaseLayer: any;
......@@ -22,11 +20,10 @@ export class MapService {
eventPopupAdded = false;
mapIsConstructed: boolean = false;
// Map
// Map features colors
featureColor: string = '#1d92ff';
featureHoverColor: string = '#E19190';
featureHighlightedColor: string = '#da322f'; // Tomato color
visitedColor: string = '#4668ab';
hoveredFeatureId: string;
highlightedFeatureId: string;
......@@ -46,8 +43,6 @@ export class MapService {
this.featureHighlightedColor,
['boolean', ['feature-state', 'hover'], false],
this.featureHoverColor,
['boolean', ['feature-state', 'visited'], false],
this.visitedColor,
'transparent',
];
......@@ -55,6 +50,7 @@ export class MapService {
private _notificationService: NotificationService,
) { }
// Init the map with basic options for controls, transform request etc...
createMap(
mapOptions: MapOptions,
url: string, baseLayer: any, options?: mapboxgl.MapboxOptions): mapboxgl.Map {
......@@ -108,10 +104,22 @@ export class MapService {
return this._map;
}
// This adds 2 layers:
// - a WMS layer to display the visual part of the features (WMS service send a png or jpeg)
// - a data layer, created from a geojson or an MVT service. It is used for the features interaction (hover, click)
addLayers() {
this.addWMSLayer();
// Add a geojson layer only if data from the metropole
// Add the 3d source. Constructed with MVT tiles from the 'fpc_fond_plan_communaut.fpctoit' dataset
const domain = this.mapOptions.vectorService.url.split('wfs')[1];
const url = `${this.mapOptions.mvtUrl}${domain}?LAYERS=
fpc_fond_plan_communaut.fpctoit&map.imagetype=mvt&tilemode=gmap&tile={x}+{y}+{z}&mode=tile`;
this._map.addSource('3d-source', {
type: 'vector',
tiles: [url],
});
// Add the data layer only if it comes from the Lyon Metropole
if (this.mapOptions.rasterService.url.includes(APP_CONFIG.backendUrls.wms)) {
// There is two ways to add tha data layers: from a geojson or from a MVT url
if (!this.mapOptions.isMVT) {
......@@ -153,7 +161,7 @@ export class MapService {
// Set highlited style for the current feature
this.highlightedFeatureId = this.selectedFeature;
this.changeFeatureState(this.highlightedFeatureId, { visited: true, highlight: true });
this.changeFeatureState(this.highlightedFeatureId, { highlight: true });
if (e.point.x > (this._map.getCanvas().width - 400)) {
// If the screen is not mobile the dataset data details panel push the map that is then smaller
......@@ -206,7 +214,6 @@ export class MapService {
// Add the raster (WMS) layer
addWMSLayer() {
// ------------------- WMS Source & Layer -------------------
this._map.addSource('wms-source', {
type: 'raster',
tiles: [
......@@ -225,10 +232,11 @@ export class MapService {
}
// Add the data layer (from geojson or MVT)
addDataLayer() {
let layerOptions = {};
// Set the paint options depending the geometry type
// For 'Polygon' and 'MultiPolygon' features
if (this.mapOptions.dataType.isAreal) {
layerOptions = {
......@@ -236,15 +244,12 @@ export class MapService {
paint: {
'fill-color': this.COLOR_EXPRESSION,
'fill-opacity': 0.7,
'fill-outline-color': ['case',
['boolean', ['feature-state', 'visited'], false],
'white',
'transparent',
],
'fill-outline-color': 'transparent',
},
};
}
// For 'Line' and 'MultiLine' features
if (this.mapOptions.dataType.isLinear) {
layerOptions = {
type: 'line',
......@@ -259,9 +264,8 @@ export class MapService {
};
}
// For "Point" features
if (this.mapOptions.dataType.isPunctual) {
// Add layer + style for the points
// Get paint options depending the dataset size
layerOptions = {
type: 'circle',
paint: {
......@@ -274,15 +278,11 @@ export class MapService {
'circle-stroke-width': ['case',
['boolean', ['feature-state', 'highlight'], false],
5,
['boolean', ['feature-state', 'visited'], false],
1,
0,
],
'circle-stroke-color': ['case',
['boolean', ['feature-state', 'highlight'], false],
this.featureHighlightedColor,
['boolean', ['feature-state', 'visited'], false],
'white',
'transparent',
],
},
......@@ -294,11 +294,13 @@ export class MapService {
id: 'data-layer',
source: 'vector-source',
...layerOptions,
...(this.mapOptions.isMVT ? { 'source-layer': this.mapOptions.vectorService.name } : {}), // if not MVT, this property is not needed
...(this.mapOptions.isMVT ? { 'source-layer': this.mapOptions.vectorService.name } : {}), // if from MVT, this property is needed
});
// If not already done, add all the events listeners
if (!this.eventPopupAdded) {
// Manage the cursor and feature state for point-features layer when mouse events
// Manage the cursor and feature state for data layer when mouse events
this._map.on('mousemove', 'data-layer', (e) => {
this.manageFeatureOnMouseMove(e.features);
});
......@@ -331,10 +333,8 @@ export class MapService {
);
}
});
this.eventPopupAdded = true;
}
}
manageFeatureOnMouseMove(features: any) {
......@@ -364,40 +364,25 @@ export class MapService {
}
switch3DLayer() {
if (!this._map.getLayer('building-3d')) {
// Insert the layer beneath any symbol layer.
const layers = this._map.getStyle().layers;
let labelLayerId;
for (let i = 0; i < layers.length; i += 1) {
if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
labelLayerId = layers[i].id;
break;
}
}
if (!this._map.getLayer('3d-layer')) {
this._map.addLayer(
{
id: 'building-3d',
id: '3d-layer',
type: 'fill-extrusion',
source: 'openmaptiles',
'source-layer': 'building',
source: '3d-source',
'source-layer': 'fpc_fond_plan_communaut.fpctoit',
paint: {
'fill-extrusion-color': 'hsla(40, 37%, 50%, 1)',
'fill-extrusion-color': '#E0E4EF',
'fill-extrusion-height': {
property: 'render_height',
property: 'htotale',
type: 'identity',
},
'fill-extrusion-base': {
property: 'render_min_height',
type: 'identity',
},
'fill-extrusion-opacity': 0.8,
'fill-extrusion-opacity': 0.7,
},
},
labelLayerId);
});
} else {
this._map.removeLayer('building-3d');
this._map.removeLayer('3d-layer');
}
}
......@@ -430,7 +415,7 @@ export class MapService {
// Set highlited style for the current feature
this.highlightedFeatureId = this.selectedFeature;
this.changeFeatureState(this.highlightedFeatureId, { visited: true, highlight: true });
this.changeFeatureState(this.highlightedFeatureId, { highlight: true });
const pointCenter = selectedFeature.geometry.type === 'Point' ?
selectedFeature.geometry.coordinates : centroid(selectedFeature).geometry.coordinates;
......@@ -448,31 +433,56 @@ export class MapService {
/*
* When the search value has been changed, we add a text expression
* that filters the features containing this text value in one of its properties.
* that filters the features containing this text value in at least one of its properties.
* If there is a match we:
* - decrease the opacity for the raster layer (WMS) which displays all the features
* - show the found features from our data layer (WMT or GeoJSON)
*/
filterBySearchValue(searchValue: string, properties: string[]) {
const filters = [];
if (searchValue) {
// Add the "in" expression (look for substring in string). To make it case insensitive, set
// both the text value and the property value to uppercase.
properties.forEach((property) => {
filters.push(['in', ['upcase', searchValue], ['upcase', ['to-string', ['get', property]]]]);
const escapedSearchString = searchValue.replace(/[\=~><\"\?^\${}\(\)\|\&\:\!\/[\]\\]/g, '\\$&');
const words = escapedSearchString.split(/\s+/);
// Some basic explanations for the operators expression we use:
// - "all": returns true if all the conditions are true
// - "any": returns true if one of the the conditions are true
// - "in": can be used in many context, here it looks for a substring in a string. To make it case insensitive, set
// both the search value and the property value to uppercase with the operator 'upcase'.
// To learn more about it: https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions
// Here is the format of the expression, for example with 2 words:
// ['all',
// ['any',
// ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property1]]]],
// ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property2]]]],
// ['in', ['upcase', 'word1'], ['upcase', ['to-string', ['get', property3]]]]
// ],
// ['any',
// ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property1]]]],
// ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property2]]]],
// ['in', ['upcase', 'word2'], ['upcase', ['to-string', ['get', property3]]]]
// ],
// ]
const anyFilter = [];
words.forEach((word) => {
const propertiesFilters = [];
properties.forEach((property) => {
propertiesFilters.push(['in', ['upcase', word], ['upcase', ['to-string', ['get', property]]]]);
});
anyFilter.push(['any', ...propertiesFilters]);
});
// For each type layer if exists (point, line, polyon), add this filter and set a color to replace transparent
const copyColor = [...this.COLOR_EXPRESSION]
copyColor.splice(copyColor.length - 1, 0, ['any', ...filters], 'green');
// Once this search filter expression is done, we add it to the existing paint options (for hover, selected)
// of the data-layer
const copyPaintOptions = [...this.COLOR_EXPRESSION];
copyPaintOptions.splice(copyPaintOptions.length - 1, 0, ['all', ...anyFilter], 'green');
this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, copyColor);
this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, copyPaintOptions);
this._map.setPaintProperty('wms-layer', 'raster-opacity', 0.5);
} else {
// If value is empty, remove the filter and set the opacity for the raster layer back to 1.
// If value is empty, remove the search expression and set the opacity for the raster layer back to 1.
this.paintPropertyForLayer('data-layer', this.mapOptions.dataType, this.COLOR_EXPRESSION);
this._map.setPaintProperty('wms-layer', 'raster-opacity', 1);
......
export const settings = {
maxDisplayFeatures: 500,
// Map
defaultBaseLayer: 0,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment