From 7058ffa19dde75c14eb89270c1a57926c0bce4cc Mon Sep 17 00:00:00 2001 From: Aijana Schumann Date: Mon, 31 Aug 2020 13:24:43 +0200 Subject: Add networkMap Add NetworkMap to odlux Issue-ID: CCSDK-2560 Signed-off-by: Aijana Schumann Change-Id: I204bcace9d12f8a26edfa347ee9b7d292c52f030 --- .../src/components/connectionInfo.tsx | 59 ++ .../networkMapApp/src/components/denseTable.tsx | 121 ++++ .../src/components/details/details.tsx | 205 +++++++ .../src/components/details/linkDetails.tsx | 101 ++++ .../src/components/details/siteDetails.tsx | 153 ++++++ .../apps/networkMapApp/src/components/map.tsx | 606 +++++++++++++++++++++ .../apps/networkMapApp/src/components/mapPopup.tsx | 94 ++++ 7 files changed, 1339 insertions(+) create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx create mode 100644 sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx (limited to 'sdnr/wt/odlux/apps/networkMapApp/src/components') diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx new file mode 100644 index 000000000..d1e2d978f --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx @@ -0,0 +1,59 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react' + +import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; +import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; +import { Paper, Typography } from "@material-ui/core"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; + + +type props = Connect; + +const ConnectionInfo: React.FunctionComponent = (props) => { + + return ((props.isTopoServerReachable === false || props.isTileServerReachable === false )? +
+
Connection Error
+ {props.isTileServerReachable === false && Tile data can't be loaded.} + {props.isTopoServerReachable === false && Network data can't be loaded.} +
+
: null +) + +} + +const mapStateToProps = (state: IApplicationStoreState) => ({ + isTopoServerReachable: state.network.connectivity.isToplogyServerAvailable, + isTileServerReachable: state.network.connectivity.isTileServerAvailable + +}); + + + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + + //zoomToSearchResult: (lat: number, lon: number) => dispatcher.dispatch(new ZoomToSearchResultAction(lat, lon)) + +});; + + +export default connect(mapStateToProps,mapDispatchToProps)(ConnectionInfo) + diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx new file mode 100644 index 000000000..9846a22c0 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx @@ -0,0 +1,121 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import { makeStyles, Button, Tooltip } from '@material-ui/core'; + +type props = { headers: string[], height:number, navigate?(applicationName: string, path?: string):void, onLinkClick?(id: string): void, data: any[], hover: boolean, onClick?(id: string): void, actions?:boolean }; + + +const styles = makeStyles({ + container: { + overflow:"auto" + }, + button: { + margin: 0, + padding: "6px 6px", + minWidth: 'unset' + } + + }); + + +const DenseTable: React.FunctionComponent = (props) => { + + const classes = styles(); + + const handleClick = (event: any, id: string) =>{ + event.preventDefault(); + props.onClick !== undefined && props.onClick(id); + + } + + const handleHover = (event: any, id: string) =>{ + event.preventDefault(); + + } + + return ( + +
+ + + + { + props.headers.map((data) => { + return {data} + }) + } + + + + {props.data.map((row, index) => { + + + var filteredRows = Object.keys(row).filter(function(e) { if(e!=="simulatorId") return row }); + + //var filteredRows = Object.keys(row).filter(function(e) { if(e!=="simulatorId") return row[e] }); + var values = Object.keys(row).map(function(e) { if(e!=="simulatorId"){ return row[e];} else return undefined }); + + + return ( + handleHover(e,row.name)} onClick={ e => handleClick(e, row.name)}> + + { + values.map((data:any) => { + + if(data!== undefined) + return {data} + else + return null; + }) + } + { + + props.actions && +
+ + + + + + +
+
+ + } +
) + }) + } + +
+
+
+
+ ); + +} + +export default DenseTable; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx new file mode 100644 index 000000000..a2e51d30f --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx @@ -0,0 +1,205 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react' + +import connect, { IDispatcher, Connect } from '../../../../../framework/src/flux/connect'; + +import { site, Device } from '../../model/site'; +import Typography from '@material-ui/core/Typography'; +import { link } from '../../model/link'; +import { Breadcrumbs, Link, Paper } from '@material-ui/core'; +import SiteDetails from './siteDetails'; +import LinkDetails from './linkDetails'; +import { URL_API, URL_BASEPATH } from '../../config'; +import { SelectSiteAction, SelectLinkAction, AddToHistoryAction, ClearHistoryAction, CheckDeviceList, ClearDetailsAction } from '../../actions/detailsAction'; +import { HistoryEntry } from '../../model/historyEntry'; +import { HighlightLinkAction, HighlightSiteAction, RemoveHighlightingAction } from '../../actions/mapActions'; +import { isSite } from '../../utils/utils'; +import { IApplicationStoreState } from '../../../../../framework/src/store/applicationStore'; +import { NavigateToApplication } from '../../../../../framework/src/actions/navigationActions'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; + + +const Details: React.FunctionComponent = (props) => { + + const [message, setMessage] = React.useState("No data selected."); + + + //on mount + React.useEffect(() => { + const detailsId = getDetailsIdFromUrl(); + if (detailsId !== null && props.data?.name !== detailsId) { + loadDetailsData(detailsId) + } + + }, []); + + // if url changed + React.useEffect(() => { + const detailsId = getDetailsIdFromUrl(); + console.log(detailsId) + if (detailsId !== null && props.data?.name !== detailsId) { + loadDetailsData(detailsId) + } + else if(detailsId===null){ + setMessage("No data selected."); + props.clearDetails(); + props.undoMapSelection(); + } + + }, [props.location.pathname]); + + //update url if new element loaded + React.useEffect(() => { + if (props.data !== null) { + const currentUrl = window.location.href; + const parts = currentUrl.split(URL_BASEPATH); + const detailsPath = parts[1].split("/details/"); + props.history.replace(`/${URL_BASEPATH}${detailsPath[0]}/details/${props.data.name}`) + } + + }, [props.data]) + + const onLinkClick = async (id: string) => { + const result = await fetch(`${URL_API}/link/${id}`); + if(result.ok){ + const resultAsJson = await result.json(); + const link = resultAsJson as link; + props.selectLink(link); + props.addHistory({ id: props.data!.name, data: props.data! }); + props.highlightLink(link); + + } + } + + const backClick = (e: any) => { + if (isSite(props.breadcrumbs[0].data)) { + props.selectSite(props.breadcrumbs[0].data) + props.highlightSite(props.breadcrumbs[0].data); + + } else { + props.selectLink(props.breadcrumbs[0].data); + props.highlightLink(props.breadcrumbs[0].data); + + } + + props.clearHistory(); + e.preventDefault(); + } + + const createDetailPanel = (data: site | link) => { + + if (isSite(data)) { + return + } else { + return + } + } + + const getDetailsIdFromUrl = () =>{ + const currentUrl = window.location.href; + const parts = currentUrl.split(URL_BASEPATH); + const detailsPath = parts[1].split("/details/") + return detailsPath[1] ? detailsPath[1] : null; + } + + const loadDetailsData = (id: string) =>{ + + fetch(`${URL_API}/link/${id}`) + .then(res => { + if (res.ok) + return res.json() + else + return Promise.reject() + + }) + .then(result => { + props.selectLink(result) + props.highlightLink(result); + + }) + .catch(error => { + + fetch(`${URL_API}/site/${id}`) + .then(res => { + if (res.ok) + return res.json() + else return Promise.reject(); + }) + .then(result => { + props.selectSite(result); + props.highlightSite(result); + }) + .catch(error =>{ + setMessage("No element with name " + id + " found"); + props.clearDetails(); + props.undoMapSelection(); + }); + }) + } + + + return (
+ + { + props.breadcrumbs.length > 0 && + + + {props.breadcrumbs[0].id} + + + {props.data?.name} + + + } + { + props.data !== null ? + createDetailPanel(props.data) + : {message} + + } + +
) +} + +type porps = RouteComponentProps & Connect; + +//select always via details? +const mapStateToProps = (state: IApplicationStoreState) => ({ + data: state.network.details?.data, + breadcrumbs: state.network.details.history, + updatedDevices: state.network.details.checkedDevices +}); + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)), + selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)), + clearDetails: () => dispatcher.dispatch(new ClearDetailsAction()), + addHistory: (newEntry: HistoryEntry) => dispatcher.dispatch(new AddToHistoryAction(newEntry)), + clearHistory: () => dispatcher.dispatch(new ClearHistoryAction()), + highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)), + highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)), + loadDevices: async (networkElements: Device[]) => { await dispatcher.dispatch(CheckDeviceList(networkElements)) }, + navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path, "test3")), + undoMapSelection: () => dispatcher.dispatch(new RemoveHighlightingAction()) + +}) + + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Details)); \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx new file mode 100644 index 000000000..de1bf6b16 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx @@ -0,0 +1,101 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; + +import { link } from '../../model/link'; +import { TextField, Tabs, Tab, Typography, AppBar, Button, Link } from '@material-ui/core'; +import DenseTable from '../denseTable'; +import { LatLonToDMS } from '../../utils/mapUtils'; + +type panelId = "siteA" | "siteB"; +type props = { link: link }; + +const LinkDetails: React.FunctionComponent = (props) => { + + const [value, setValue] = React.useState("siteA"); + const [height, setHeight] = React.useState(330); + + const handleResize = () =>{ + console.log("resize") + const el = document.getElementById('site-details-panel')?.getBoundingClientRect(); + const el2 = document.getElementById('site-tabs')?.getBoundingClientRect(); + + if(el && el2){ + if(props.link.type==="microwave") + setHeight(el!.height - el2!.y -30); + else + setHeight(el!.height - el2!.y +20); + + } + } + + //on mount + React.useEffect(()=>{ + handleResize(); + + //window.addEventListener("resize", handleResize); + },[]); + + React.useEffect(()=>{ + handleResize(); + }, [props.link]) + + const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => { + setValue(newValue); + } + + const onCalculateLinkClick = (e: React.MouseEvent) =>{ + e.preventDefault(); + const siteA= props.link.locationA; + const siteB =props.link.locationB; + const nameA = props.link.siteA; + const nameB = props.link.siteB; + const distance = props.link.length > 0 ? props.link.length : props.link.calculatedLength; + const azimuthA = props.link.azimuthA; + const azimuthB = props.link.azimuthB; + window.open(`/#/linkCalculation?lat1=${siteA.lat}&lon1=${siteA.lon}&lat2=${siteB.lat}&lon2=${siteB.lon}&siteA=${nameA}&siteB=${nameB}&azimuthA=${azimuthA}&azimuthB=${azimuthB}&distance=${distance}`) + + } + + const data = [ + + {name:"Site Name", val1: props.link.siteA, val2: props.link.siteB}, + {name:"Latitude", val1: LatLonToDMS(props.link.locationA.lat), val2: LatLonToDMS(props.link.locationB.lat)}, + {name:"Longitude", val1: LatLonToDMS(props.link.locationA.lon, true), val2: LatLonToDMS(props.link.locationB.lon, true)}, + {name:"Azimuth in °", val1: props.link.azimuthA.toFixed(2), val2: props.link.azimuthB.toFixed(2)} +]; + + return (
+

{props.link.name}

+ + + + + + + SITE DETAILS + + + { + props.link.type==="microwave" && + } +
) +} + +export default LinkDetails; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx new file mode 100644 index 000000000..a95666e38 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx @@ -0,0 +1,153 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; +import { TextField, Tabs, Tab, Typography, AppBar, Button, Tooltip } from '@material-ui/core'; + + +import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../../framework/src/components/material-table"; + + +import { site, Device } from '../../model/site'; +import DenseTable from '../denseTable'; +import { LatLonToDMS } from '../../utils/mapUtils'; + +type minLinks = { name: string, azimuth: string} + +const FaultAlarmNotificationTable = MaterialTable as MaterialTableCtorType; + + +type panelId="links" | "nodes"; +type props = { site: site, updatedDevices: Device[]|null, navigate(applicationName: string, path?: string):void, onLinkClick(id: string): void, loadDevices(devices:Device[]): void }; + +const SiteDetails: React.FunctionComponent = (props) => { + + const [value, setValue] = React.useState("links"); + const [height, setHeight] = React.useState(330); + + const handleResize = () =>{ + //console.log("resize") + const el = document.getElementById('site-details-panel')?.getBoundingClientRect(); + const el2 = document.getElementById('site-tabs')?.getBoundingClientRect(); + + if(el && el2){ + setHeight(el!.height - el2!.y +20); + } + + } + + //on mount + React.useEffect(()=>{ + handleResize(); + + window.addEventListener("resize", ()=>{console.log("really got resized.")}); + },[]); + + // on update + React.useEffect(()=>{ + + props.loadDevices(props.site.devices); + handleResize(); + + }, [props.site]) + + const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => { + setValue(newValue); + } + + const linkRows: minLinks[] = props.site.links.map(link=> + { + return {name: link.name, azimuth: link.azimuthB.toFixed(2) } + }); + + + + return (
+

{props.site.name}

+ { + props.site.operator !== '' && props.site.operator !== null ? + : + + } + { + props.site.type !== undefined && props.site.type.length > 0 && + + } + { + props.site.address !== undefined && props.site.address.length > 0 && + + } + { + props.site.heighAGLInMeters !== undefined && props.site.heighAGLInMeters > 0 && + + } + { + props.site.antennaHeightAGLInMeters !== undefined && props.site.antennaHeightAGLInMeters > 0 && + + } + + + + + + + + + + + { + value === "links" && + <> + { + props.site.links.length === 0 && + No links available. + } + + { + props.site.links.length > 0 && + + /** + * + * */ + + + } + + + + } + { + value === "nodes" && + <> + { + props.site.devices.length === 0 && + No nodes available. + } + + { + props.site.devices.length>0 && props.updatedDevices !== null && + + } + + } +
+ ) + +} + +export default SiteDetails; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx new file mode 100644 index 000000000..1aabb92c6 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx @@ -0,0 +1,606 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react' +import * as mapboxgl from 'mapbox-gl'; +import InfoIcon from '@material-ui/icons/Info'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; + + +import { site } from '../model/site'; +import { SelectSiteAction, ClearHistoryAction, SelectLinkAction } from '../actions/detailsAction'; +import { OSM_STYLE, URL_API, URL_BASEPATH, URL_TILE_API } from '../config'; +import { link } from '../model/link'; +import MapPopup from './mapPopup'; +import { SetPopupPositionAction, SelectMultipleLinksAction, SelectMultipleSitesAction } from '../actions/popupActions'; +import { Feature } from '../model/Feature'; +import { HighlightLinkAction, HighlightSiteAction, SetCoordinatesAction, SetStatistics } from '../actions/mapActions'; +import { addDistance, getUniqueFeatures } from '../utils/mapUtils'; +import { location } from '../handlers/mapReducer' +import { Typography, Paper, Tooltip } from '@material-ui/core'; +import { elementCount } from '../model/count'; +import lamp from '../../icons/lamp.png'; +import apartment from '../../icons/apartment.png'; +import datacenter from '../../icons/datacenter.png'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { verifyResponse, IsTileServerReachableAction, handleConnectionError } from '../actions/connectivityAction'; +import ConnectionInfo from './connectionInfo' +import { ApplicationStore } from '../../../../framework/src/store/applicationStore'; +import { showIconLayers, addBaseLayers, swapLayersBack } from '../utils/mapLayers'; + + + + + +type coordinates = { lat: number, lon: number, zoom: number } + +let alarmElements: Feature[] = []; +let map: mapboxgl.Map; +let isLoadingInProgress = false; +let notLoadedBoundingBoxes: mapboxgl.LngLatBounds[] = []; + +let lastBoundingBox: mapboxgl.LngLatBounds | null = null; +let myRef = React.createRef(); + + +class Map extends React.Component { + + constructor(props: mapProps) { + super(props); + //any state stuff + this.state = { isPopupOpen: false } + + } + + componentDidMount() { + + window.addEventListener("menu-resized", this.handleResize); + + fetch(URL_TILE_API + '/10/0/0.png') + .then(res => { + if (res.ok) { + this.setupMap(); + } else { + this.props.setTileServerLoaded(false); + console.error("tileserver " + URL_TILE_API + "can't be reached.") + } + }) + .catch(err => { + this.props.setTileServerLoaded(false); + console.error("tileserver " + URL_TILE_API + "can't be reached.") + }); + + fetch(URL_API + "/info") + .then(result => verifyResponse(result)) + .catch(error => this.props.handleConnectionError(error)); + } + + setupMap = () => { + + let lat = this.props.lat; + let lon = this.props.lon; + let zoom = this.props.zoom; + + const coordinates = this.extractCoordinatesFromUrl(); + // override lat/lon/zoom with coordinates from url, if available + if (this.areCoordinatesValid(coordinates)) { + lat = coordinates.lat; + lon = coordinates.lon; + zoom = !Number.isNaN(coordinates.zoom) ? coordinates.zoom : zoom; + } + + map = new mapboxgl.Map({ + container: myRef.current!, + style: OSM_STYLE as any, + center: [lon, lat], + zoom: zoom, + accessToken: '' + }); + + map.on('load', (ev) => { + + addBaseLayers(map, this.props.selectedSite, this.props.selectedLink); + map.loadImage( + lamp, + function (error: any, image: any) { + if (error) throw error; + map.addImage('lamp', image); + }); + + map.loadImage( + datacenter, + function (error: any, image: any) { + if (error) throw error; + map.addImage('data-center', image); + }); + + map.loadImage( + apartment, + function (error: any, image: any) { + if (error) throw error; + map.addImage('house', image); + }); + + const boundingBox = map.getBounds(); + + + fetch(`${URL_API}/links/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`) + .then(result => verifyResponse(result)) + .then(result => result.json()) + .then(features => { + if (map.getLayer('lines')) { + (map.getSource('lines') as mapboxgl.GeoJSONSource).setData(features); + } + }) + .catch(error => this.props.handleConnectionError(error)); + + + fetch(`${URL_API}/sites/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`) + .then(result => verifyResponse(result)) + .then(result => result.json()) + .then(features => { + if (map.getLayer('points')) { + (map.getSource('points') as mapboxgl.GeoJSONSource).setData(features); + } + }) + .catch(error => this.props.handleConnectionError(error));; + + }); + + map.on('click', (e: any) => { + + if (map.getLayer('points')) { // data is shown as points + + var clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5], + [e.point.x + 5, e.point.y + 5]], { + layers: ['lines'] + }), "id"); + + const clickedPoints = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['points'] }), "id"); + const alarmedSites = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['alarmedPoints'] }), "id"); + + if (clickedPoints.length != 0) { + + + if (alarmedSites.length > 0) { + alarmedSites.forEach(alarm => { + const index = clickedPoints.findIndex(item => item.properties!.id === alarm.properties!.id); + console.log(index); + + if (index !== -1) { + clickedPoints[index].properties!.alarmed = true; + clickedPoints[index].properties!.type = "alarmed"; + } + }); + console.log(clickedPoints); + } + + this.showSitePopup(clickedPoints, e.point.x, e.point.y); + } else if (clickedLines.length != 0) { + this.showLinkPopup(clickedLines, e.point.x, e.point.y); + } + + + } else { // data is shown as icons + + const clickedLamps = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-lamps'] }), "id"); + const buildings = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-building'] }), "id"); + const houses = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-data-center'] }), "id"); + + const combinedFeatures = [...clickedLamps, ...buildings, ...houses]; + + const clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5], + [e.point.x + 5, e.point.y + 5]], { + layers: ['lines'] + }), "id"); + + if (combinedFeatures.length > 0) + this.showSitePopup(combinedFeatures, e.point.x, e.point.y); + else if (clickedLines.length != 0) { + this.showLinkPopup(clickedLines, e.point.x, e.point.y); + } + } + + }); + + map.on('moveend', () => { + + const mapZoom = Number(map.getZoom().toFixed(2)); + const lat = Number(map.getCenter().lat.toFixed(4)); + const lon = Number(map.getCenter().lng.toFixed(4)); + + + if (this.props.lat !== lat || this.props.lon !== lon || this.props.zoom !== mapZoom) { + this.props.updateMapPosition(lat, lon, mapZoom) + } + + const currentUrl = window.location.href; + const parts = currentUrl.split(URL_BASEPATH); + const detailsPath = parts[1].split("/details/"); + + if (detailsPath[1] !== undefined && detailsPath[1].length > 0) { + this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}/details/${detailsPath[1]}`) + } + else { + this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}`) + } + + const boundingBox = map.getBounds(); + + showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id); + + fetch(`${URL_API}/info/count/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`) + .then(result => verifyResponse(result)) + .then(res => res.json()) + .then(result => { + console.log(result); + if (result.links !== this.props.linkCount || result.sites !== this.props.siteCount) { + this.props.setStatistics(result.links, result.sites); + } + }) + .catch(error => this.props.handleConnectionError(error));; + }) + + map.on('move', () => { + const mapZoom = map.getZoom(); + + const boundingBox = map.getBounds(); + + this.loadNetworkData(boundingBox); + if (mapZoom > 9) { + + if (map.getLayer('points')) { + map.setLayoutProperty('selectedPoints', 'visibility', 'visible'); + map.setPaintProperty('points', 'circle-radius', 7); + } + } else { + + // reduce size of points / lines if zoomed out + map.setPaintProperty('points', 'circle-radius', 2); + map.setLayoutProperty('selectedPoints', 'visibility', 'none'); + + if (mapZoom <= 4) { + map.setPaintProperty('lines', 'line-width', 1); + } else { + map.setPaintProperty('lines', 'line-width', 2); + } + } + }); + } + + componentDidUpdate(prevProps: mapProps, prevState: {}) { + + if (map !== undefined) { + if (prevProps.selectedSite?.properties.id !== this.props.selectedSite?.properties.id) { + + if (this.props.selectedSite != null) { + if (map.getSource("selectedLine") !== undefined) { + (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] }); + (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedSite] }); + } + + + if (map.getLayer('point-lamps') !== undefined) { + + map.setFilter('point-lamps', ['==', 'type', 'street lamp']); + map.setFilter('point-data-center', ['==', 'type', 'data center']); + map.setFilter('point-building', ['==', 'type', 'high rise building']) + + if (this.props.selectedSite?.properties.type !== undefined) { + switch (this.props.selectedSite?.properties.type) { + case 'street lamp': + map.setFilter('point-lamps', ["all", ['==', 'type', 'street lamp'], ['!=', 'id', this.props.selectedSite.properties.id]]); + break; + case 'data center': + map.setFilter('point-data-center', ["all", ['==', 'type', 'data center'], ['!=', 'id', this.props.selectedSite.properties.id]]); + break; + case 'high rise building': + map.setFilter('point-building', ["all", ['==', 'type', 'high rise building'], ['!=', 'id', this.props.selectedSite.properties.id]]) + + break; + } + } + } + + + } + else + { + if (map.getSource("selectedPoints") !== undefined) + (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] }); + + } + } + + if (prevProps.selectedLink !== this.props.selectedLink) { + if (this.props.selectedLink != null) { + + if (map.getLayer('point-lamps') !== undefined) { + map.setFilter('point-lamps', ['==', 'type', 'street lamp']); + map.setFilter('point-data-center', ['==', 'type', 'data center']); + map.setFilter('point-building', ['==', 'type', 'high rise building']); + } + + if (map.getSource("selectedLine") !== undefined) { + (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] }); + (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedLink] }); + } + } + else + { + if (map.getSource("selectedLine") !== undefined) + (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] }); + } + } + + if (prevProps.location.pathname !== this.props.location.pathname) { + if (map) { + const coordinates = this.extractCoordinatesFromUrl(); + this.moveMapToCoordinates(coordinates); + } + } + + if (prevProps.alarmlement !== this.props.alarmlement) { + if (this.props.alarmlement !== null && !alarmElements.includes(this.props.alarmlement)) { + if (map.getSource("alarmedPoints")) + (map.getSource("alarmedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: alarmElements }); + alarmElements.push(this.props.alarmlement) + } + } + + if (prevProps.showIcons !== this.props.showIcons) { + if (map && map.getZoom() > 11) { + console.log(this.props.showIcons); + showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id); + } + } + + if (prevProps.zoomToElement !== this.props.zoomToElement) { + if (this.props.zoomToElement !== null) { + const currentZoom = map?.getZoom(); + + map.flyTo({ + center: [ + this.props.zoomToElement.lon, + this.props.zoomToElement.lat + ], zoom: currentZoom < 10 ? 10 : currentZoom, + essential: true + }); + } + } + } + } + + handleResize = () => { + if (map) { + // wait a moment until resizing actually happened + window.setTimeout(() => map.resize(), 500); + } + } + + extractCoordinatesFromUrl = (): coordinates => { + const currentUrl = window.location.href; + const mainPathParts = currentUrl.split(URL_BASEPATH); + const coordinatePathPart = mainPathParts[1].split("/details/"); // split by details if present + const allCoordinates = coordinatePathPart[0].replace("/", ""); + const coordinates = allCoordinates.split(","); + return { lat: Number(coordinates[0]), lon: Number(coordinates[1]), zoom: Number(coordinates[2]) } + } + + areCoordinatesValid = (coordinates: coordinates) => { + + if ((!Number.isNaN(coordinates.lat)) && (!Number.isNaN(coordinates.lon))) { + return true; + } else { + return false; + } + } + + moveMapToCoordinates = (coordinates: coordinates) => { + + if (this.areCoordinatesValid(coordinates)) { + let zoom = -1; + + if (!Number.isNaN(coordinates.zoom)) { + zoom = coordinates.zoom; + } + + map.flyTo({ + center: [ + coordinates.lon, + coordinates.lat + ], zoom: zoom !== -1 ? zoom : this.props.zoom, + essential: true + }) + } + } + + + //TODO: how to handle if too much data gets loaded? (1 mio points...?) + // data might have gotten collected, reload if necessary! + //always save count, if count and current view count differ -> reload last boundingbox + loadNetworkData = async (bbox: mapboxgl.LngLatBounds) => { + if (!isLoadingInProgress) { // only load data if loading not in progress + isLoadingInProgress = true; + + if (lastBoundingBox == null) { + lastBoundingBox = bbox; + await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`); + await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`); + } else { + + // new bbox is bigger than old one + if (bbox.contains(lastBoundingBox.getNorthEast()) && bbox.contains(lastBoundingBox.getSouthWest()) && lastBoundingBox !== bbox) { //if new bb is bigger than old one + + lastBoundingBox = bbox; + + const distance = map.getCenter().distanceTo(bbox.getNorthEast()); // radius of visible area (center -> corner) (in meters) + + //calculate new boundingBox + const increasedBoundingBox = addDistance(bbox.getSouth(), bbox.getWest(), bbox.getNorth(), bbox.getEast(), (distance / 1000) / 2) + + await this.draw('lines', `${URL_API}/links/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`); + await this.draw('points', `${URL_API}/sites/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`); + console.log("bbox is bigger"); + + } else if (lastBoundingBox.contains(bbox.getNorthEast()) && lastBoundingBox.contains(bbox.getSouthWest())) { // last one contains new one + // bbox is contained in last one, do nothing + isLoadingInProgress = false; + + } else { // bbox is not fully contained in old one, extend + + lastBoundingBox.extend(bbox); + + await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`); + await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`); + } + + } + + + if (notLoadedBoundingBoxes.length > 0) { // load last not loaded boundingbox + this.loadNetworkData(notLoadedBoundingBoxes.pop()!) + notLoadedBoundingBoxes = []; + } + + } else { + notLoadedBoundingBoxes.push(bbox); + } + } + + showSitePopup = (sites: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => { + if (sites.length > 1) { + const ids = sites.map(feature => feature.properties!.id); + + this.props.setPopupPosition(top, left); + this.props.selectMultipleSites(ids); + this.setState({ isPopupOpen: true }); + + } else { + const id = sites[0].properties!.id; + + fetch(`${URL_API}/site/${id}`) + .then(result => verifyResponse(result)) + .then(res => res.json() as Promise) + .then(result => { + this.props.selectSite(result); + this.props.highlightSite(result); + this.props.clearDetailsHistory(); + }) + .catch(error => this.props.handleConnectionError(error));; + } + + } + + + + showLinkPopup = (links: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => { + + if (links.length > 1) { + + const ids = links.map(feature => feature.properties!.id as string); + + this.props.setPopupPosition(top, left); + this.props.selectMultipleLinks(ids); + this.setState({ isPopupOpen: true }); + + } else { + var id = links[0].properties!.id; + + fetch(`${URL_API}/link/${id}`) + .then(result => verifyResponse(result)) + .then(res => res.json() as Promise) + .then(result => { + this.props.selectLink(result); + this.props.highlightLink(result); + + this.props.clearDetailsHistory(); + }) + .catch(error => this.props.handleConnectionError(error));; + } + } + + draw = async (layer: string, url: string) => { + + fetch(url) + .then(result => verifyResponse(result)) + .then(res => res.json()) + .then(result => { + isLoadingInProgress = false; + if (map.getSource(layer)) { + (map.getSource(layer) as mapboxgl.GeoJSONSource).setData(result); + } + }) + .catch(error => this.props.handleConnectionError(error));; + } + + render() { + + const reachabe = this.props.isTopoServerReachable && this.props.isTileServerReachable; + + return <> + +
+ { + this.state.isPopupOpen && + { this.setState({ isPopupOpen: false }); }} /> + } + +
+ + } + +} + +type mapProps = RouteComponentProps & Connect; + +const mapStateToProps = (state: IApplicationStoreState) => ({ + selectedLink: state.network.map.selectedLink, + selectedSite: state.network.map.selectedSite, + zoomToElement: state.network.map.zoomToElement, + alarmlement: state.network.map.alarmlement, + lat: state.network.map.lat, + lon: state.network.map.lon, + zoom: state.network.map.zoom, + linkCount: state.network.map.statistics.links, + siteCount: state.network.map.statistics.sites, + isTopoServerReachable: state.network.connectivity.isToplogyServerAvailable, + isTileServerReachable: state.network.connectivity.isTileServerAvailable, + showIcons: state.network.map.allowIconSwitch + + + +}); + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)), + selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)), + clearDetailsHistory: () => dispatcher.dispatch(new ClearHistoryAction()), + selectMultipleLinks: (ids: string[]) => dispatcher.dispatch(new SelectMultipleLinksAction(ids)), + selectMultipleSites: (ids: string[]) => dispatcher.dispatch(new SelectMultipleSitesAction(ids)), + setPopupPosition: (x: number, y: number) => dispatcher.dispatch(new SetPopupPositionAction(x, y)), + highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)), + highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)), + updateMapPosition: (lat: number, lon: number, zoom: number) => dispatcher.dispatch(new SetCoordinatesAction(lat, lon, zoom)), + setStatistics: (linkCount: string, siteCount: string) => dispatcher.dispatch(new SetStatistics(siteCount, linkCount)), + setTileServerLoaded: (reachable: boolean) => dispatcher.dispatch(new IsTileServerReachableAction(reachable)), + handleConnectionError: (error: Error) => dispatcher.dispatch(handleConnectionError(error)) +}) + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Map)); \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx new file mode 100644 index 000000000..040024760 --- /dev/null +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx @@ -0,0 +1,94 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react'; +import { Typography, Select, MenuItem, ClickAwayListener, Popper, Paper, FormGroup, Portal, Popover } from '@material-ui/core'; +import { SelectSiteAction, ClearHistoryAction, ClearDetailsAction } from '../actions/detailsAction'; +import { site } from '../model/site'; +import { link } from '../model/link'; +import { URL_API } from '../config'; +import { HighlightLinkAction, HighlightSiteAction } from '../actions/mapActions'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { verifyResponse, handleConnectionError } from '../actions/connectivityAction'; + + + + +const MapPopup: React.FunctionComponent = (props) => { + + const [value, setValue] = React.useState(""); + + const handleChange = (event: any) => { + setValue(event.target.value); + + const id = event.target.value; + + + fetch(`${URL_API}/${props.type}/${id}`) + .then(result => verifyResponse(result)) + .then(res => res.json()) + .then(result => { + props.clearDetailsHistory(); + props.selectElement(result); + props.type === "link" ? props.highlightLink(result) : props.highlightSite(result) + props.onClose(); + }) + .catch(error => { + props.handleConnectionError(error); + props.onClose(); + // props.clearDetails(); + }); + }; + + return <> + + + {`Multiple ${props.type.toLowerCase()}s were selected`} + Please select one. + + + + +} + +type props = Connect& { onClose(): void } + +const mapStateToProps = (state: IApplicationStoreState) => ({ + ids: state.network.popup.selectionPendingForIds, + type: state.network.popup.pendingDataType, + position: state.network.popup.position + +}); + +const mapDispatchToProps = (dispatcher: IDispatcher) => ({ + selectElement: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)), + clearDetailsHistory:()=> dispatcher.dispatch(new ClearHistoryAction()), + highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)), + highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)), + handleConnectionError: (error:Error) => dispatcher.dispatch(handleConnectionError(error)), + clearDetails: () => dispatcher.dispatch(new ClearDetailsAction()), + +}); + +export default (connect(mapStateToProps, mapDispatchToProps))(MapPopup); \ No newline at end of file -- cgit 1.2.3-korg