summaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/networkMapApp/src/components
diff options
context:
space:
mode:
authorAijana Schumann <aijana.schumann@highstreet-technologies.com>2020-08-31 13:24:43 +0200
committerAijana Schumann <aijana.schumann@highstreet-technologies.com>2020-08-31 13:24:43 +0200
commit7058ffa19dde75c14eb89270c1a57926c0bce4cc (patch)
treed4d278eb926df35832fd69ea778bd9e6dec0f126 /sdnr/wt/odlux/apps/networkMapApp/src/components
parent4bd84bebdaa0c2d82050fbedd1fa8260eb62146d (diff)
Add networkMap
Add NetworkMap to odlux Issue-ID: CCSDK-2560 Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com> Change-Id: I204bcace9d12f8a26edfa347ee9b7d292c52f030
Diffstat (limited to 'sdnr/wt/odlux/apps/networkMapApp/src/components')
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx59
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx121
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx205
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx101
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx153
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx606
-rw-r--r--sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx94
7 files changed, 1339 insertions, 0 deletions
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<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+const ConnectionInfo: React.FunctionComponent<props> = (props) => {
+
+ return ((props.isTopoServerReachable === false || props.isTileServerReachable === false )? <Paper style={{padding:5, position: 'absolute', top: 160, width: 230, left:"40%"}}>
+ <div style={{display: 'flex', flexDirection: 'column'}}>
+ <div style={{'alignSelf': 'center', marginBottom:5}}> <Typography> <FontAwesomeIcon icon={faExclamationTriangle} /> Connection Error</Typography></div>
+ {props.isTileServerReachable === false && <Typography> Tile data can't be loaded.</Typography>}
+ {props.isTopoServerReachable === false && <Typography > Network data can't be loaded.</Typography>}
+ </div>
+ </Paper> : 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> = (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 (
+ <Paper style={{borderRadius:"0px"}}>
+ <div style={{ height:props.height, overflow:"auto"}}>
+ <Table stickyHeader size="small" aria-label="a dense table" >
+ <TableHead>
+ <TableRow>
+ {
+ props.headers.map((data) => {
+ return <TableCell>{data}</TableCell>
+ })
+ }
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {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 (
+ <TableRow key={index} hover={props.hover} onMouseOver={e => handleHover(e,row.name)} onClick={ e => handleClick(e, row.name)}>
+
+ {
+ values.map((data:any) => {
+
+ if(data!== undefined)
+ return <TableCell > {data} </TableCell>
+ else
+ return null;
+ })
+ }
+ {
+
+ props.actions && <TableCell >
+<div style={{display:"flex"}}>
+ <Tooltip title="Configure">
+ <Button className={classes.button} disabled={row.status!=="connected"} onClick={(e: any) =>{ e.preventDefault(); e.stopPropagation(); props.navigate && props.navigate("configuration", row.simulatorId ? row.simulatorId : row.name)}}>C</Button>
+ </Tooltip>
+ <Tooltip title="Fault">
+ <Button className={classes.button} onClick={(e: any) =>{ e.preventDefault(); e.stopPropagation(); props.navigate && props.navigate("fault", row.simulatorId ? row.simulatorId : row.name)}}>F</Button>
+ </Tooltip>
+ </div>
+ </TableCell>
+
+ }
+ </TableRow>)
+ })
+ }
+
+ </TableBody>
+ </Table>
+ </div>
+ </Paper>
+ );
+
+}
+
+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<porps> = (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 <SiteDetails navigate={props.navigateToApplication} updatedDevices={props.updatedDevices} loadDevices={props.loadDevices} site={data} onLinkClick={onLinkClick} />
+ } else {
+ return <LinkDetails link={data} />
+ }
+ }
+
+ 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 (<div style={{ width: '30%', background: "#bbbdbf", padding: "20px", alignSelf:"stretch" }}>
+ <Paper style={{ height:"100%"}} id="site-details-panel" >
+ {
+ props.breadcrumbs.length > 0 &&
+ <Breadcrumbs style={{ marginLeft: "15px", marginTop: "5px" }} aria-label="breadcrumb">
+ <Link color="inherit" href="/" onClick={backClick}>
+ {props.breadcrumbs[0].id}
+ </Link>
+ <Link>
+ {props.data?.name}
+ </Link>
+ </Breadcrumbs>
+ }
+ {
+ props.data !== null ?
+ createDetailPanel(props.data)
+ : <Typography style={{ marginTop: "5px" }} align="center" variant="body1">{message}</Typography>
+
+ }
+ </Paper>
+ </div>)
+}
+
+type porps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+//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> = (props) => {
+
+ const [value, setValue] = React.useState<panelId>("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<HTMLButtonElement, 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 (<div style={{ paddingLeft: "15px", paddingRight: "15px", paddingTop: "0px", display: 'flex', flexDirection: 'column' }}>
+ <h2>{props.link.name}</h2>
+ <TextField disabled style={{ marginTop: "5px" }} value="Unkown" label="Operator" />
+ <TextField disabled style={{ marginTop: "5px" }} value={props.link.type} label="Type" />
+ <TextField disabled style={{ marginTop: "5px" }} value={props.link.length.toFixed(2)} label="Distance planned in km" />
+ <TextField disabled style={{ marginTop: "5px" }} value={props.link.calculatedLength.toFixed(2)} label="Distance calculated in km" />
+
+ <AppBar position="static" id="site-tabs" style={{ marginTop: "20px", background: '#2E3B55' }}>
+ <Typography style={{ margin:"5px"}}>SITE DETAILS</Typography>
+ </AppBar>
+ <DenseTable height={height} hover={false} headers={["", "Site A", "Site B"]} data={data} />
+ {
+ props.link.type==="microwave" && <Button style={{marginTop:20}} fullWidth variant="contained" color="primary" onClick={onCalculateLinkClick}>Calculate link</Button>
+ }
+ </div>)
+}
+
+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<minLinks>;
+
+
+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> = (props) => {
+
+ const [value, setValue] = React.useState<panelId>("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 (<div style={{ padding: '15px', display: "flex", flexDirection:"column", minWidth:0, minHeight:0 }}>
+ <h2 >{props.site.name}</h2>
+ {
+ props.site.operator !== '' && props.site.operator !== null ?
+ <TextField disabled={true} value={props.site.operator} label="Operator" /> :
+ <TextField disabled={true} value="Unkown" label="Operator" style={{ marginTop: "5px" }} />
+ }
+ {
+ props.site.type !== undefined && props.site.type.length > 0 &&
+ <TextField disabled={true} value={props.site.type} label="Type" style={{ marginTop: "5px" }} />
+ }
+ {
+ props.site.address !== undefined && props.site.address.length > 0 &&
+ <TextField disabled={true} value={props.site.address} label="Adress" style={{ marginTop: "5px" }} />
+ }
+ {
+ props.site.heighAGLInMeters !== undefined && props.site.heighAGLInMeters > 0 &&
+ <TextField disabled={true} value={props.site.heighAGLInMeters} label="AMSL in meters" style={{ marginTop: "5px" }} />
+ }
+ {
+ props.site.antennaHeightAGLInMeters !== undefined && props.site.antennaHeightAGLInMeters > 0 &&
+ <TextField disabled={true} value={props.site.antennaHeightAGLInMeters} label="Atenna above ground in meters" style={{ marginTop: "5px" }} />
+ }
+
+ <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lat)} label="Latitude" />
+ <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lon, true)} label="Longitude" />
+
+ <AppBar position="static" style={{ marginTop: "5px", background: '#2E3B55' }}>
+ <Tabs id="site-tabs" value={value} onChange={onHandleTabChange} aria-label="simple tabs example">
+ <Tab label="Links" value="links" />
+ <Tab label="Nodes" value="nodes" />
+ </Tabs>
+ </AppBar>
+ {
+ value === "links" &&
+ <>
+ {
+ props.site.links.length === 0 &&
+ <Typography variant="body1" style={{ marginTop: '10px' }}>No links available.</Typography>
+ }
+
+ {
+ props.site.links.length > 0 &&
+ <DenseTable height={height} hover={true} headers={["Link Name", "Azimuth in °"]} data={linkRows} onClick={props.onLinkClick} ></DenseTable>
+ /**
+ *
+ * */
+
+
+ }
+
+ </>
+
+ }
+ {
+ value === "nodes" &&
+ <>
+ {
+ props.site.devices.length === 0 &&
+ <Typography variant="body1" style={{ marginTop: '10px' }}>No nodes available.</Typography>
+ }
+
+ {
+ props.site.devices.length>0 && props.updatedDevices !== null &&
+ <DenseTable navigate={props.navigate} height={height} hover={false} headers={["ID","Name","Type", "Manufacturer","Owner","Status", "Ports", "Actions"]} actions={true} data={props.updatedDevices!} />
+ }
+ </>
+ }
+ </div>
+ )
+
+}
+
+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<HTMLDivElement>();
+
+
+class Map extends React.Component<mapProps, { isPopupOpen: boolean }> {
+
+ 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<site>)
+ .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<link>)
+ .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 <>
+
+ <div id="map" style={{ width: "70%", position: 'relative' }} ref={myRef} >
+ {
+ this.state.isPopupOpen &&
+ <MapPopup onClose={() => { this.setState({ isPopupOpen: false }); }} />
+ }
+ <ConnectionInfo />
+ </div>
+ </>
+ }
+
+}
+
+type mapProps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+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> = (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 <>
+ <Popover open={true} anchorEl={undefined} onClose={props.onClose} anchorReference="anchorPosition" anchorPosition={{ top: props.position.left, left: props.position.top }}>
+ <Paper style={{ padding: "15px" }}>
+ <Typography variant="h5">{`Multiple ${props.type.toLowerCase()}s were selected`}</Typography>
+ <Typography variant="body1">Please select one.</Typography>
+ <Select style={{ width: 300 }} onChange={handleChange} value={value} native>
+ <option value={""} disabled>{props.type} ids</option>
+ {
+ props.ids.map(id => <option key={id} value={id}>{id}</option>)
+ }
+ </Select>
+ </Paper>
+ </Popover>
+ </>
+}
+
+type props = Connect<typeof mapStateToProps, typeof mapDispatchToProps>& { 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