2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
19 import * as React from 'react'
20 import * as mapboxgl from 'mapbox-gl';
21 import InfoIcon from '@material-ui/icons/Info';
22 import { RouteComponentProps, withRouter } from 'react-router-dom';
25 import { site } from '../model/site';
26 import { SelectSiteAction, ClearHistoryAction, SelectLinkAction } from '../actions/detailsAction';
27 import { OSM_STYLE, URL_API, URL_BASEPATH, URL_TILE_API } from '../config';
28 import { link } from '../model/link';
29 import MapPopup from './mapPopup';
30 import { SetPopupPositionAction, SelectMultipleLinksAction, SelectMultipleSitesAction } from '../actions/popupActions';
31 import { Feature } from '../model/Feature';
32 import { HighlightLinkAction, HighlightSiteAction, SetCoordinatesAction, SetStatistics } from '../actions/mapActions';
33 import { addDistance, getUniqueFeatures } from '../utils/mapUtils';
34 import { location } from '../handlers/mapReducer'
35 import { Typography, Paper, Tooltip } from '@material-ui/core';
36 import { elementCount } from '../model/count';
37 import lamp from '../../icons/lamp.png';
38 import apartment from '../../icons/apartment.png';
39 import datacenter from '../../icons/datacenter.png';
40 import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
41 import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect';
42 import SearchBar from './searchBar';
43 import { verifyResponse, IsTileServerReachableAction, handleConnectionError } from '../actions/connectivityAction';
44 import ConnectionInfo from './connectionInfo'
45 import { ApplicationStore } from '../../../../framework/src/store/applicationStore';
46 import { showIconLayers, addBaseLayers, swapLayersBack } from '../utils/mapLayers';
52 type coordinates = { lat: number, lon: number, zoom: number }
54 let alarmElements: Feature[] = [];
55 let map: mapboxgl.Map;
56 let isLoadingInProgress = false;
57 let notLoadedBoundingBoxes: mapboxgl.LngLatBounds[] = [];
59 let lastBoundingBox: mapboxgl.LngLatBounds | null = null;
60 let myRef = React.createRef<HTMLDivElement>();
63 class Map extends React.Component<mapProps, { isPopupOpen: boolean }> {
65 constructor(props: mapProps) {
68 this.state = { isPopupOpen: false }
74 window.addEventListener("menu-resized", this.handleResize);
76 fetch(URL_TILE_API + '/10/0/0.png')
81 this.props.setTileServerLoaded(false);
82 console.error("tileserver " + URL_TILE_API + "can't be reached.")
86 this.props.setTileServerLoaded(false);
87 console.error("tileserver " + URL_TILE_API + "can't be reached.")
90 fetch(URL_API + "/info")
91 .then(result => verifyResponse(result))
92 .catch(error => this.props.handleConnectionError(error));
97 let lat = this.props.lat;
98 let lon = this.props.lon;
99 let zoom = this.props.zoom;
101 const coordinates = this.extractCoordinatesFromUrl();
102 // override lat/lon/zoom with coordinates from url, if available
103 if (this.areCoordinatesValid(coordinates)) {
104 lat = coordinates.lat;
105 lon = coordinates.lon;
106 zoom = !Number.isNaN(coordinates.zoom) ? coordinates.zoom : zoom;
109 map = new mapboxgl.Map({
110 container: myRef.current!,
111 style: OSM_STYLE as any,
117 map.on('load', (ev) => {
119 addBaseLayers(map, this.props.selectedSite, this.props.selectedLink);
122 function (error: any, image: any) {
123 if (error) throw error;
124 map.addImage('lamp', image);
129 function (error: any, image: any) {
130 if (error) throw error;
131 map.addImage('data-center', image);
136 function (error: any, image: any) {
137 if (error) throw error;
138 map.addImage('house', image);
141 const boundingBox = map.getBounds();
144 fetch(`${URL_API}/links/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
145 .then(result => verifyResponse(result))
146 .then(result => result.json())
148 if (map.getLayer('lines')) {
149 (map.getSource('lines') as mapboxgl.GeoJSONSource).setData(features);
152 .catch(error => this.props.handleConnectionError(error));
155 fetch(`${URL_API}/sites/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
156 .then(result => verifyResponse(result))
157 .then(result => result.json())
159 if (map.getLayer('points')) {
160 (map.getSource('points') as mapboxgl.GeoJSONSource).setData(features);
163 .catch(error => this.props.handleConnectionError(error));;
167 map.on('click', (e: any) => {
169 if (map.getLayer('points')) { // data is shown as points
171 var clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5],
172 [e.point.x + 5, e.point.y + 5]], {
176 const clickedPoints = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['points'] }), "id");
177 const alarmedSites = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['alarmedPoints'] }), "id");
179 if (clickedPoints.length != 0) {
182 if (alarmedSites.length > 0) {
183 alarmedSites.forEach(alarm => {
184 const index = clickedPoints.findIndex(item => item.properties!.id === alarm.properties!.id);
188 clickedPoints[index].properties!.alarmed = true;
189 clickedPoints[index].properties!.type = "alarmed";
192 console.log(clickedPoints);
195 this.showSitePopup(clickedPoints, e.point.x, e.point.y);
196 } else if (clickedLines.length != 0) {
197 this.showLinkPopup(clickedLines, e.point.x, e.point.y);
201 } else { // data is shown as icons
203 const clickedLamps = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-lamps'] }), "id");
204 const buildings = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-building'] }), "id");
205 const houses = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-data-center'] }), "id");
207 const combinedFeatures = [...clickedLamps, ...buildings, ...houses];
209 const clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5],
210 [e.point.x + 5, e.point.y + 5]], {
214 if (combinedFeatures.length > 0)
215 this.showSitePopup(combinedFeatures, e.point.x, e.point.y);
216 else if (clickedLines.length != 0) {
217 this.showLinkPopup(clickedLines, e.point.x, e.point.y);
223 map.on('moveend', () => {
225 const mapZoom = Number(map.getZoom().toFixed(2));
226 const lat = Number(map.getCenter().lat.toFixed(4));
227 const lon = Number(map.getCenter().lng.toFixed(4));
230 if (this.props.lat !== lat || this.props.lon !== lon || this.props.zoom !== mapZoom) {
231 this.props.updateMapPosition(lat, lon, mapZoom)
234 const currentUrl = window.location.href;
235 const parts = currentUrl.split(URL_BASEPATH);
236 const detailsPath = parts[1].split("/details/");
238 if (detailsPath[1] !== undefined && detailsPath[1].length > 0) {
239 this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}/details/${detailsPath[1]}`)
242 this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}`)
245 const boundingBox = map.getBounds();
247 showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id);
249 fetch(`${URL_API}/info/count/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
250 .then(result => verifyResponse(result))
251 .then(res => res.json())
254 if (result.links !== this.props.linkCount || result.sites !== this.props.siteCount) {
255 this.props.setStatistics(result.links, result.sites);
258 .catch(error => this.props.handleConnectionError(error));;
261 map.on('move', () => {
262 const mapZoom = map.getZoom();
264 const boundingBox = map.getBounds();
266 this.loadNetworkData(boundingBox);
269 if (map.getLayer('points')) {
270 map.setLayoutProperty('selectedPoints', 'visibility', 'visible');
271 map.setPaintProperty('points', 'circle-radius', 7);
275 // reduce size of points / lines if zoomed out
276 map.setPaintProperty('points', 'circle-radius', 2);
277 map.setLayoutProperty('selectedPoints', 'visibility', 'none');
280 map.setPaintProperty('lines', 'line-width', 1);
282 map.setPaintProperty('lines', 'line-width', 2);
288 componentDidUpdate(prevProps: mapProps, prevState: {}) {
290 if (map !== undefined) {
291 if (prevProps.selectedSite?.properties.id !== this.props.selectedSite?.properties.id) {
293 if (this.props.selectedSite != null) {
294 if (map.getSource("selectedLine") !== undefined) {
295 (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
296 (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedSite] });
300 if (map.getLayer('point-lamps') !== undefined) {
302 map.setFilter('point-lamps', ['==', 'type', 'street lamp']);
303 map.setFilter('point-data-center', ['==', 'type', 'data center']);
304 map.setFilter('point-building', ['==', 'type', 'high rise building'])
306 if (this.props.selectedSite?.properties.type !== undefined) {
307 switch (this.props.selectedSite?.properties.type) {
309 map.setFilter('point-lamps', ["all", ['==', 'type', 'street lamp'], ['!=', 'id', this.props.selectedSite.properties.id]]);
312 map.setFilter('point-data-center', ["all", ['==', 'type', 'data center'], ['!=', 'id', this.props.selectedSite.properties.id]]);
314 case 'high rise building':
315 map.setFilter('point-building', ["all", ['==', 'type', 'high rise building'], ['!=', 'id', this.props.selectedSite.properties.id]])
326 if (map.getSource("selectedPoints") !== undefined)
327 (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
332 if (prevProps.selectedLink !== this.props.selectedLink) {
333 if (this.props.selectedLink != null) {
335 if (map.getLayer('point-lamps') !== undefined) {
336 map.setFilter('point-lamps', ['==', 'type', 'street lamp']);
337 map.setFilter('point-data-center', ['==', 'type', 'data center']);
338 map.setFilter('point-building', ['==', 'type', 'high rise building']);
341 if (map.getSource("selectedLine") !== undefined) {
342 (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
343 (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedLink] });
348 if (map.getSource("selectedLine") !== undefined)
349 (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
353 if (prevProps.location.pathname !== this.props.location.pathname) {
355 const coordinates = this.extractCoordinatesFromUrl();
356 this.moveMapToCoordinates(coordinates);
360 if (prevProps.alarmlement !== this.props.alarmlement) {
361 if (this.props.alarmlement !== null && !alarmElements.includes(this.props.alarmlement)) {
362 if (map.getSource("alarmedPoints"))
363 (map.getSource("alarmedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: alarmElements });
364 alarmElements.push(this.props.alarmlement)
368 if (prevProps.showIcons !== this.props.showIcons) {
369 if (map && map.getZoom() > 11) {
370 console.log(this.props.showIcons);
371 showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id);
375 if (prevProps.zoomToElement !== this.props.zoomToElement) {
376 if (this.props.zoomToElement !== null) {
377 const currentZoom = map?.getZoom();
381 this.props.zoomToElement.lon,
382 this.props.zoomToElement.lat
383 ], zoom: currentZoom < 10 ? 10 : currentZoom,
391 handleResize = () => {
393 // wait a moment until resizing actually happened
394 window.setTimeout(() => map.resize(), 500);
398 extractCoordinatesFromUrl = (): coordinates => {
399 const currentUrl = window.location.href;
400 const mainPathParts = currentUrl.split(URL_BASEPATH);
401 const coordinatePathPart = mainPathParts[1].split("/details/"); // split by details if present
402 const allCoordinates = coordinatePathPart[0].replace("/", "");
403 const coordinates = allCoordinates.split(",");
404 return { lat: Number(coordinates[0]), lon: Number(coordinates[1]), zoom: Number(coordinates[2]) }
407 areCoordinatesValid = (coordinates: coordinates) => {
409 if ((!Number.isNaN(coordinates.lat)) && (!Number.isNaN(coordinates.lon))) {
416 moveMapToCoordinates = (coordinates: coordinates) => {
418 if (this.areCoordinatesValid(coordinates)) {
421 if (!Number.isNaN(coordinates.zoom)) {
422 zoom = coordinates.zoom;
429 ], zoom: zoom !== -1 ? zoom : this.props.zoom,
436 //TODO: how to handle if too much data gets loaded? (1 mio points...?)
437 // data might have gotten collected, reload if necessary!
438 //always save count, if count and current view count differ -> reload last boundingbox
439 loadNetworkData = async (bbox: mapboxgl.LngLatBounds) => {
440 if (!isLoadingInProgress) { // only load data if loading not in progress
441 isLoadingInProgress = true;
443 if (lastBoundingBox == null) {
444 lastBoundingBox = bbox;
445 await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
446 await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
449 // new bbox is bigger than old one
450 if (bbox.contains(lastBoundingBox.getNorthEast()) && bbox.contains(lastBoundingBox.getSouthWest()) && lastBoundingBox !== bbox) { //if new bb is bigger than old one
452 lastBoundingBox = bbox;
454 const distance = map.getCenter().distanceTo(bbox.getNorthEast()); // radius of visible area (center -> corner) (in meters)
456 //calculate new boundingBox
457 const increasedBoundingBox = addDistance(bbox.getSouth(), bbox.getWest(), bbox.getNorth(), bbox.getEast(), (distance / 1000) / 2)
459 await this.draw('lines', `${URL_API}/links/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`);
460 await this.draw('points', `${URL_API}/sites/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`);
461 console.log("bbox is bigger");
463 } else if (lastBoundingBox.contains(bbox.getNorthEast()) && lastBoundingBox.contains(bbox.getSouthWest())) { // last one contains new one
464 // bbox is contained in last one, do nothing
465 isLoadingInProgress = false;
467 } else { // bbox is not fully contained in old one, extend
469 lastBoundingBox.extend(bbox);
471 await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
472 await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
478 if (notLoadedBoundingBoxes.length > 0) { // load last not loaded boundingbox
479 this.loadNetworkData(notLoadedBoundingBoxes.pop()!)
480 notLoadedBoundingBoxes = [];
484 notLoadedBoundingBoxes.push(bbox);
488 showSitePopup = (sites: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => {
489 if (sites.length > 1) {
490 const ids = sites.map(feature => feature.properties!.id);
492 this.props.setPopupPosition(top, left);
493 this.props.selectMultipleSites(ids);
494 this.setState({ isPopupOpen: true });
497 const id = sites[0].properties!.id;
499 fetch(`${URL_API}/site/${id}`)
500 .then(result => verifyResponse(result))
501 .then(res => res.json() as Promise<site>)
503 this.props.selectSite(result);
504 this.props.highlightSite(result);
505 this.props.clearDetailsHistory();
507 .catch(error => this.props.handleConnectionError(error));;
514 showLinkPopup = (links: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => {
516 if (links.length > 1) {
518 const ids = links.map(feature => feature.properties!.id as string);
520 this.props.setPopupPosition(top, left);
521 this.props.selectMultipleLinks(ids);
522 this.setState({ isPopupOpen: true });
525 var id = links[0].properties!.id;
527 fetch(`${URL_API}/link/${id}`)
528 .then(result => verifyResponse(result))
529 .then(res => res.json() as Promise<link>)
531 this.props.selectLink(result);
532 this.props.highlightLink(result);
534 this.props.clearDetailsHistory();
536 .catch(error => this.props.handleConnectionError(error));;
540 draw = async (layer: string, url: string) => {
543 .then(result => verifyResponse(result))
544 .then(res => res.json())
546 isLoadingInProgress = false;
547 if (map.getSource(layer)) {
548 (map.getSource(layer) as mapboxgl.GeoJSONSource).setData(result);
551 .catch(error => this.props.handleConnectionError(error));;
556 const reachabe = this.props.isTopoServerReachable && this.props.isTileServerReachable;
560 <div id="map" style={{ width: "70%", position: 'relative' }} ref={myRef} >
562 this.state.isPopupOpen &&
563 <MapPopup onClose={() => { this.setState({ isPopupOpen: false }); }} />
573 type mapProps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
575 const mapStateToProps = (state: IApplicationStoreState) => ({
576 selectedLink: state.network.map.selectedLink,
577 selectedSite: state.network.map.selectedSite,
578 zoomToElement: state.network.map.zoomToElement,
579 alarmlement: state.network.map.alarmlement,
580 lat: state.network.map.lat,
581 lon: state.network.map.lon,
582 zoom: state.network.map.zoom,
583 linkCount: state.network.map.statistics.links,
584 siteCount: state.network.map.statistics.sites,
585 isTopoServerReachable: state.network.connectivity.isToplogyServerAvailable,
586 isTileServerReachable: state.network.connectivity.isTileServerAvailable,
587 showIcons: state.network.map.allowIconSwitch
593 const mapDispatchToProps = (dispatcher: IDispatcher) => ({
594 selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)),
595 selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)),
596 clearDetailsHistory: () => dispatcher.dispatch(new ClearHistoryAction()),
597 selectMultipleLinks: (ids: string[]) => dispatcher.dispatch(new SelectMultipleLinksAction(ids)),
598 selectMultipleSites: (ids: string[]) => dispatcher.dispatch(new SelectMultipleSitesAction(ids)),
599 setPopupPosition: (x: number, y: number) => dispatcher.dispatch(new SetPopupPositionAction(x, y)),
600 highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)),
601 highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)),
602 updateMapPosition: (lat: number, lon: number, zoom: number) => dispatcher.dispatch(new SetCoordinatesAction(lat, lon, zoom)),
603 setStatistics: (linkCount: string, siteCount: string) => dispatcher.dispatch(new SetStatistics(siteCount, linkCount)),
604 setTileServerLoaded: (reachable: boolean) => dispatcher.dispatch(new IsTileServerReachableAction(reachable)),
605 handleConnectionError: (error: Error) => dispatcher.dispatch(handleConnectionError(error))
608 export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Map));