summaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/lineOfSightApp/src/components
diff options
context:
space:
mode:
authorAijana Schumann <aijana.schumann@highstreet-technologies.com>2021-08-05 08:50:16 +0200
committerAijana Schumann <aijana.schumann@highstreet-technologies.com>2021-08-05 08:50:16 +0200
commit3ba5eb125ac8890968e4437b098e39195d699434 (patch)
treeac58a39013fc9ab2cd468da83e4c41cd2d62b600 /sdnr/wt/odlux/apps/lineOfSightApp/src/components
parent437f67407aece6f7aed8e989638b0d64075f0c0a (diff)
Update ODLUX
Add LineOfSightApp, update Framework, Connect, Performance and LinkCalculatorApp Issue-ID: CCSDK-3417 Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com> Change-Id: I651a2fb771d2963aea70f916c70c8fdfd3443e87
Diffstat (limited to 'sdnr/wt/odlux/apps/lineOfSightApp/src/components')
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/components/ConnectionErrorPoup.tsx40
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/components/heightChart.tsx126
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/components/map.tsx329
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapContextMenu.tsx86
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapInfo.tsx171
5 files changed, 752 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/components/ConnectionErrorPoup.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/ConnectionErrorPoup.tsx
new file mode 100644
index 000000000..7d9339fc0
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/ConnectionErrorPoup.tsx
@@ -0,0 +1,40 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2021 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 { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import { Paper, Typography } from "@material-ui/core"
+import * as React from "react"
+
+type props = { reachable: boolean|null};
+
+
+const ConnectionErrorPoup: React.FunctionComponent<props> = (props) => {
+
+ return (props.reachable === false ?
+ <Paper style={{padding:5, position: 'absolute', top: 160, width: 230, left:"40%", zIndex:1}}>
+ <div style={{display: 'flex', flexDirection: 'column'}}>
+ <div style={{'alignSelf': 'center', marginBottom:5}}> <Typography> <FontAwesomeIcon icon={faExclamationTriangle} /> Connection Error</Typography></div>
+ <Typography>Service unavailable</Typography>
+ </div>
+ </Paper> : null
+)
+
+}
+
+export default ConnectionErrorPoup; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/components/heightChart.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/heightChart.tsx
new file mode 100644
index 000000000..3030fe7dd
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/heightChart.tsx
@@ -0,0 +1,126 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2021 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 type { FC } from 'react';
+import * as d3 from 'd3';
+
+import { useD3 } from "../hooks/d3";
+import { GPSProfileResult } from "../model/GPSProfileResult";
+import { max } from '../utils/math';
+
+type HeightMapProps = {
+ data: GPSProfileResult[];
+ dataMin: GPSProfileResult;
+ dataMax: GPSProfileResult;
+ width: number;
+ height: number;
+ heightPosA: number;
+ heightPosB: number;
+}
+
+const HeightChart: FC<HeightMapProps> = (props) => {
+ const { data, dataMin, dataMax, heightPosA, heightPosB } = props;
+ let ref: React.RefObject<SVGSVGElement>
+
+ const drawSvg = () => {
+ ref = useD3(
+ (svg) => {
+ const margin = 100;
+ const width = Number(svg.attr("width")) - margin;
+ const height = Number(svg.attr("height")) - margin;
+
+ // Add X axis
+ const x = d3.scaleBand()
+ .range([0, width])
+ .domain(data.map(d => (`${d.gps.latitude},${d.gps.latitude}`)))
+ .padding(0.2);
+
+ const maxHeight = max([dataMax.height, heightPosA, heightPosB], d => d)
+
+ // Add Y axis
+ const y = d3.scaleLinear()
+ .domain([dataMin.height, maxHeight])
+ .range([height, 0]);
+
+ svg.append("g")
+ .attr('transform', `translate(${margin / 2}, ${margin / 2})`)
+ .call(d3.axisLeft(y));
+
+ // Bars
+ svg.selectAll("myBar")
+ .data(data)
+ .join("rect")
+ .attr('transform', `translate(${margin / 2}, ${margin / 2})`)
+ .attr("x", d => x(`${d.gps.latitude},${d.gps.latitude}`) || '')
+ .attr("y", d => y(d.height))
+ .attr("width", x.bandwidth())
+ .attr("fill", "#69b3a2b0")
+ .attr("height", d => height - y(d.height)) // always equal to 0
+
+ const firstX = `${data[0].gps.latitude},${data[0].gps.latitude}`
+ const lastX = `${data[data.length - 1].gps.latitude},${data[data.length - 1].gps.latitude}`;
+
+ //add line
+ const x1 = x(firstX)!;
+ const x2 = x(lastX)!;
+
+ svg.append("line")
+ .attr('transform', `translate(${margin / 2}, ${margin / 2})`)
+ .attr("x1", x1)
+ .attr("y1", y(props.heightPosA))
+ .attr("x2", x2)
+ .attr("y2", y(props.heightPosB))
+
+ .style("stroke", "#88A")
+ .attr("stroke-width", "3px")
+
+ //append circle on start and end
+
+ svg.append("circle")
+ .attr('transform', `translate(${margin / 2}, ${margin / 2})`)
+ .attr('cx', x1)
+ .attr('cy', y(props.heightPosA))
+ .attr('r', 10)
+ .attr('stroke', '#223b53')
+ .attr('fill', '#225ba3');
+
+ svg.append("circle")
+ .attr('transform', `translate(${margin / 2}, ${margin / 2})`)
+ .attr('cx', x2)
+ .attr('cy', y(props.heightPosB))
+ .attr('r', 10)
+ .attr('stroke', '#223b53')
+ .attr('fill', '#225ba3');
+ },
+ [data]
+ );
+ }
+
+ drawSvg();
+
+
+
+ return (
+ <svg ref={ref!} width={props.width} height={props.height} />
+
+ );
+}
+
+export { HeightChart };
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/components/map.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/map.tsx
new file mode 100644
index 000000000..6f29d5993
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/map.tsx
@@ -0,0 +1,329 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2021 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 { render } from 'react-dom';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+import connect, { Connect, IDispatcher } from '../../../../framework/src/flux/connect';
+import { OSM_STYLE, URL_BASEPATH } from '../config';
+import { GPSProfileResult } from '../model/GPSProfileResult';
+import MapContextMenu from './mapContextMenu';
+import { getGPSProfile } from '../services/heightService';
+import { max, min } from '../utils/math';
+import { HeightChart } from './heightChart';
+import { makeStyles } from '@material-ui/core';
+import { ClearSavedChartAction, SetChartAction, SetEndpointAction, SetHeightA, SetHeightB, SetMapCenterAction, SetStartPointAction } from '../actions/mapActions';
+import { LatLon } from '../model/LatLon';
+import MapInfo from './mapInfo';
+import { Height } from 'model/Height';
+import { PictureAsPdf } from '@material-ui/icons';
+import ConnectionErrorPoup from './ConnectionErrorPoup';
+import { addBaseLayer, addBaseSource, addPoint } from '../utils/map';
+import { SetReachableAction } from '../actions/commonActions';
+
+import 'mapbox-gl/dist/mapbox-gl.css';
+
+type mapProps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+const mapStateToProps = (state: IApplicationStoreState) => ({
+ center: state.lineOfSight.map.center,
+ zoom: state.lineOfSight.map.zoom,
+ start: state.lineOfSight.map.start,
+ end: state.lineOfSight.map.end,
+ heightA: state.lineOfSight.map.heightA,
+ heightB: state.lineOfSight.map.heightB,
+ ready: state.lineOfSight.map.ready
+});
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({
+ ClearChartAction: () => dispatcher.dispatch(new ClearSavedChartAction),
+ SetMapPosition: (point: LatLon, zoom: number) => dispatcher.dispatch(new SetMapCenterAction(point, zoom)),
+ SetHeightStart: (height: Height) => dispatcher.dispatch(new SetHeightA(height)),
+ SetHeightEnd: (height: Height) => dispatcher.dispatch(new SetHeightB(height)),
+ setStartPosition: (position: LatLon|null) => dispatcher.dispatch(new SetStartPointAction(position)),
+ setEndPosition: (position: LatLon|null) => dispatcher.dispatch(new SetEndpointAction(position)),
+ setReachable : (reachable: boolean |null) => dispatcher.dispatch(new SetReachableAction(reachable)),
+
+
+
+})
+
+
+let map: mapboxgl.Map;
+
+const styles = makeStyles({
+ chart: {
+ position: "absolute",
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0
+
+ }
+ });
+
+
+const Map: React.FC<mapProps> = (props) => {
+
+ //const [start, setStart] = React.useState<mapboxgl.LngLat| undefined>();
+ //const [end, setEnd] = React.useState<mapboxgl.LngLat| undefined>();
+ const [data, setData] = React.useState<GPSProfileResult[] | number>(Number.NaN);
+ const [dataMin, setDataMin] = React.useState<GPSProfileResult|undefined>();
+ const [dataMax, setDataMax] = React.useState<GPSProfileResult|undefined>();
+ const [isMapLoaded, setMapLoaded] = React.useState<boolean>(false);
+
+
+const mapRef = React.useRef<{ map: mapboxgl.Map | null }>({ map: null });
+const mapContainerRef = React.useRef<HTMLDivElement>(null);
+
+
+
+const classes = styles();
+
+const heightA = props.heightA !== null ? props.heightA.amsl + props.heightA.antennaHeight : 0;
+const heightB = props.heightB !== null ? props.heightB.amsl + props.heightB.antennaHeight : 0;
+
+const {start, end} = props;
+
+const handleResize = () =>{
+
+ if (map) {
+ // wait a moment until resizing actually happened
+ window.setTimeout(() => map.resize(), 500);
+ }
+
+}
+
+//on mount
+React.useEffect(()=>{
+
+ window.addEventListener("menu-resized", handleResize);
+
+
+ return () =>{
+ console.log("unmount")
+ window.removeEventListener("menu-resized", handleResize);
+
+ const center = mapRef.current.map?.getCenter();
+ const mapZoom = mapRef.current.map?.getZoom();
+ if(center){
+ props.SetMapPosition({latitude: center.lat, longitude:center.lng}, mapZoom!);
+ }
+
+ props.setReachable(null);
+ }
+
+},[]);
+
+
+ React.useEffect(()=>{
+
+ if(props.ready){
+ setupMap();
+ }
+
+ },[props.ready]);
+
+ React.useEffect(() => {
+ if (props.ready && isMapLoaded) {
+ drawChart();
+ updateLosUrl();
+ }
+
+ }, [start, end, isMapLoaded]);
+
+ const drawChart = () =>{
+ if(start && end){
+
+ addBaseSource(map, 'route');
+ addBaseLayer(map, 'route');
+
+ const json = `{
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [${start.longitude}, ${start.latitude}],
+ [${end.longitude}, ${end.latitude}]
+ ]}
+ }`;
+
+
+
+ (map.getSource("route") as mapboxgl.GeoJSONSource).setData(JSON.parse(json));
+
+
+ getGPSProfile({ latitude: start.latitude, longitude: start.longitude }, { latitude: end.latitude, longitude: end.longitude }).then(data => {
+ if (Array.isArray(data)) {
+ setDataMin(min(data, d => d.height));
+ setDataMax(max(data, d => d.height));
+ }
+ setData(data);
+ });
+ }
+ else if (start || end){
+
+ const point = start!==null ? start: end!;
+ addBaseSource(map, 'route');
+ addBaseLayer(map, 'route');
+ addPoint(map, point);
+
+ }
+ else {
+ //delete layers and source
+ //used instead of clearing source data because it has better performance
+ //(setting data to empty results in a noticable lag of line being cleared)
+ mapRef.current.map?.getLayer('line') && mapRef.current.map?.removeLayer('line') && mapRef.current.map?.removeLayer('points') && mapRef.current.map?.removeSource('route');
+
+ }
+ }
+
+ const updateLosUrl = () =>{
+
+ if(start && end){
+
+ const locationPart = `lat1=${start.latitude}&lon1=${start.longitude}&lat2=${end.latitude}&lon2=${end.longitude}`;
+
+ let heightPart = '';
+
+ if(props.heightA && props.heightB){
+ heightPart = `&amslA=${props.heightA.amsl}&antennaHeightA=${props.heightA.antennaHeight}&amslB=${props.heightB.amsl}&antennaHeightB=${props.heightB.antennaHeight}`;
+
+ }
+
+ props.history.replace(`/${URL_BASEPATH}/los?${locationPart}${heightPart}`)
+
+ }else if(!start && !end){
+ props.history.replace(`/${URL_BASEPATH}`);
+ }
+ }
+
+
+ const updateHeightA = (value:number, value2: number) =>{
+ props.SetHeightStart({amsl: value, antennaHeight: value2});
+ }
+
+ const updateHeightB = (value:number, value2: number) =>{
+ props.SetHeightEnd({amsl: value, antennaHeight: value2});
+ }
+
+ const OnEndPosition = (position: mapboxgl.LngLat) =>{
+ props.setEndPosition({latitude: position.lat, longitude: position.lng})
+ }
+
+ const OnStartPosition = (position: mapboxgl.LngLat) =>{
+ props.setStartPosition({latitude: position.lat, longitude: position.lng})
+ }
+
+
+ const setupMap = () => {
+
+ let lat = props.center.latitude
+ let lon = props.center.longitude;
+ let zoom = props.zoom;
+
+ map = new mapboxgl.Map({
+ container: mapContainerRef.current!,
+ style: OSM_STYLE as any,
+ center: [lon, lat],
+ zoom: zoom,
+ accessToken: ''
+ });
+
+ mapRef.current.map = map;
+
+ map.on('load', (ev) => {
+
+ map.setMaxZoom(18);
+ setMapLoaded(true);
+
+ //add source, layer
+
+ addBaseSource(map, 'route');
+ addBaseLayer(map, 'route');
+
+ });
+
+ let currentPopup: mapboxgl.Popup | null = null;
+ map.on('contextmenu', (e) => {
+
+ if (currentPopup)
+ currentPopup.remove();
+
+ //change height if start/end changes
+ //??? -> show value? / reset after chart display?
+
+ const popupNode = document.createElement("div");
+ render(
+ <MapContextMenu pos={e.lngLat}
+ onStart={(p) => { OnStartPosition(p); if (currentPopup) currentPopup.remove(); }}
+ onEnd={(p) => { OnEndPosition(p); if (currentPopup) currentPopup.remove(); }}
+ onHeightA={(p,p1)=> updateHeightA(p, p1)}
+ onHeightB={(p, p1)=> updateHeightB(p, p1)} />,
+ popupNode);
+
+ currentPopup = new mapboxgl.Popup()
+ .setLngLat(e.lngLat)
+ .setDOMContent(popupNode)
+ .addTo(map);
+ });
+
+ map.on('moveend', mapMoveEnd);
+
+ };
+
+ const mapMoveEnd = () =>{
+ const mapZoom = Number(map.getZoom().toFixed(2));
+ const lat = Number(map.getCenter().lat.toFixed(4));
+ const lon = Number(map.getCenter().lng.toFixed(4));
+
+ props.SetMapPosition({latitude: lat, longitude: lon}, mapZoom);
+
+ }
+
+
+
+ return <>
+ <div id="map" style={{ width: "100%", height:'100%', position: 'relative' }} ref={mapContainerRef} >
+ <MapInfo minHeight={dataMin} maxHeight={dataMax} />
+ <ConnectionErrorPoup reachable={props.ready} />
+
+ {typeof data === "object"
+ ? (
+ < div className={classes.chart} onClick={() => {
+ setData(Number.NaN);
+ setDataMax(undefined);
+ setDataMin(undefined);
+ props.ClearChartAction();
+ }}>
+ <HeightChart heightPosA={heightA} heightPosB={heightB} width={mapContainerRef.current?.clientWidth!} height={mapContainerRef.current?.clientHeight!} data={data} dataMin={dataMin!} dataMax={dataMax!} />
+ </div>
+ )
+ : null
+ }
+
+ </div>
+ </>
+}
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Map));
+
+
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapContextMenu.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapContextMenu.tsx
new file mode 100644
index 000000000..0fc51cabf
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapContextMenu.tsx
@@ -0,0 +1,86 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2021 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 { Button, InputAdornment, makeStyles, TextField, Tooltip } from "@material-ui/core";
+import * as React from "react";
+import { FC, useEffect, useState } from "react";
+import { getGPSHeight } from "../services/heightService";
+
+type MapContextMenuProps = {
+ pos: mapboxgl.LngLat;
+ onStart: (pos: mapboxgl.LngLat) => void;
+ onEnd: (pos: mapboxgl.LngLat) => void;
+ onHeightA: (height: number, antennaHeight: number) => void;
+ onHeightB: (height: number, antennaHeight: number) => void;
+
+ }
+
+ const styles = makeStyles({
+ flexContainer: {display: "flex", flexDirection:"row"},
+ textField:{width:60},
+ button:{marginRight:5, marginTop:5, flexGrow:2}
+ });
+
+ const MapContextMenu: FC<MapContextMenuProps> = (props) => {
+ const { pos, onStart, onEnd } = props;
+ const [height, setHeight] = useState<number | undefined>(undefined);
+ const [value1, setValue1] = useState<string>('');
+ const [value2, setValue2] = useState<string>('');
+
+ const classes = styles();
+
+ useEffect(() => {
+ getGPSHeight({ longitude: pos.lng, latitude: pos.lat }).then(setHeight);
+ }, [pos.lat, pos.lng]);
+
+ const handleChangeHeight = (e:React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>, id: "heightA"|"heightB") =>{
+
+ //sanitize non numbers
+ const onlyNums = e.target.value.replace(/[^0-9]/g, '');
+
+ if(id==="heightA"){
+ setValue1(onlyNums);
+ }else{
+ setValue2(onlyNums);
+ }
+ }
+
+ return (
+ <div>
+ <div>Height: {height} m</div>
+ <div>
+ <div className={classes.flexContainer}>
+ <Button className={classes.button} variant="contained" onClick={() => { onStart(pos); props.onHeightA(height!,+value1); }}>Start</Button>
+ <Tooltip title="Please add the antenna height in meters above sea level.">
+ <TextField className={classes.textField} value={value1} onChange={(e)=>handleChangeHeight(e,"heightA")} InputProps={{endAdornment: <InputAdornment position="start">m</InputAdornment>}}/>
+ </Tooltip>
+ </div>
+ <div className={classes.flexContainer}>
+ <Button className={classes.button} variant="contained" onClick={() => { onEnd(pos); props.onHeightB(height!,+value2);}}>End</Button>
+ <Tooltip title="Please add the antenna height in meters above sea level.">
+ <TextField className={classes.textField} value={value2} onChange={(e)=>handleChangeHeight(e,"heightB")} InputProps={{endAdornment: <InputAdornment position="start">m</InputAdornment>}}/>
+ </Tooltip>
+ </div>
+ </div>
+
+ </div>
+ );
+ };
+
+
+ export default MapContextMenu; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapInfo.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapInfo.tsx
new file mode 100644
index 000000000..43a6e478a
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/components/mapInfo.tsx
@@ -0,0 +1,171 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2021 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 { Accordion, AccordionDetails, AccordionSummary, makeStyles, Paper, Typography } from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import { GPSProfileResult } from '../model/GPSProfileResult';
+import * as React from 'react';
+import { calculateDistanceInMeter } from '../utils/map';
+import connect, { Connect, IDispatcher } from '../../../../framework/src/flux/connect';
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+
+const mapStateToProps = (state: IApplicationStoreState) => ({
+ center: state.lineOfSight.map.center,
+ zoom: state.lineOfSight.map.zoom,
+ start: state.lineOfSight.map.start,
+ end: state.lineOfSight.map.end,
+ heightA: state.lineOfSight.map.heightA,
+ heightB: state.lineOfSight.map.heightB,
+});
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({
+
+
+})
+
+type props = Connect<typeof mapStateToProps, typeof mapDispatchToProps> & {
+ minHeight: GPSProfileResult | undefined;
+ maxHeight: GPSProfileResult | undefined;
+};
+
+const styles = (props: any) => makeStyles({
+ accordion: {padding: 5, position: 'absolute', top: 10, width: props.width, marginLeft: 10, zIndex:1},
+ container: { display: 'flex', flexDirection: "column", marginLeft:10, padding: 5 },
+ caption:{width:'40%'},
+ subTitleRow:{ width: '60%'},
+ titleRowElement:{width: '40%', fontWeight: "bold"},
+ secondRow:{width:'25%'},
+ thirdRow:{width:'20%'}
+ });
+
+const MapInfo: React.FC<props> = (props) =>{
+
+ const [expanded, setExpanded] = React.useState(false);
+ const [width, setWidth] = React.useState(470);
+ const [length, setLength] = React.useState<string | undefined>();
+
+ const classes = styles({width: width})();
+
+ const {start, end, center, zoom, heightA, heightB, minHeight, maxHeight} = props;
+
+ React.useEffect(()=>{
+
+ if(start && end){
+ setLength(calculateDistanceInMeter(start.latitude, start.longitude, end.latitude, end.longitude).toFixed(3))
+
+ }else{
+ setLength(undefined)
+ }
+
+ }, [start, end])
+
+ const handleChange = (event: any, isExpanded: boolean) => {
+ setExpanded(isExpanded);
+ };
+
+
+
+
+ return <Accordion className={classes.accordion} expanded={expanded} onChange={handleChange}>
+ <AccordionSummary
+ expandIcon={<ExpandMoreIcon />}
+ aria-controls="panel1a-content"
+ id="panel1a-header"
+ >
+ <Typography >Map Info</Typography>
+ </AccordionSummary>
+ <AccordionDetails className={classes.container}>
+
+
+ <Typography style={{ fontWeight: "bold", flex: "1" }} >Map Center</Typography>
+
+ <div >
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+ <Typography className={classes.caption}> Longitude</Typography><Typography>{center.longitude}</Typography></div>
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+ <Typography className={classes.caption}> Latitude</Typography><Typography>{center.latitude}</Typography></div>
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+ <Typography className={classes.caption}> Zoom</Typography><Typography> {zoom}</Typography></div>
+
+ </div>
+ <Typography style={{ fontWeight: "bold", flex: "1", marginTop:5 }} >Link</Typography>
+
+ <div>
+ <div style={{ display: 'flex', flexDirection: "row", marginLeft:"38%" }}>
+ <Typography className={classes.titleRowElement}> Start</Typography>
+ <Typography className={classes.titleRowElement}> End</Typography>
+ </div>
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Longitude</Typography>
+ <Typography className={classes.secondRow}> {start?.longitude.toFixed(3)}</Typography>
+ <Typography className={classes.secondRow}> {end?.longitude.toFixed(3)}</Typography></div>
+
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Latitude</Typography>
+ <Typography className={classes.secondRow}> {start?.latitude.toFixed(3)}</Typography>
+ <Typography className={classes.secondRow}> {end?.latitude.toFixed(3)}</Typography></div>
+
+
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Meassured height [m]</Typography>
+ <Typography className={classes.secondRow}> {heightA?.amsl}</Typography>
+ <Typography className={classes.secondRow}> {heightB?.amsl}</Typography>
+ </div>
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Antenna height [m] </Typography>
+ <Typography className={classes.secondRow}> {heightA?.antennaHeight}</Typography>
+ <Typography className={classes.secondRow}> {heightB?.antennaHeight}</Typography>
+ </div>
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Length [m]</Typography>
+ <Typography className={classes.secondRow}> {length}</Typography>
+
+ </div>
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Max height @ position </Typography>
+ <Typography className={classes.thirdRow}> {maxHeight? maxHeight.height+' m': ''}</Typography>
+ <Typography className={classes.thirdRow}> {maxHeight?.gps.longitude.toFixed(3)}</Typography>
+ <Typography className={classes.thirdRow}> {maxHeight?.gps.latitude.toFixed(3)}</Typography>
+ </div>
+
+ <div style={{ display: 'flex', flexDirection: "row" }}>
+
+ <Typography className={classes.caption}> Min height @ position</Typography>
+ <Typography className={classes.thirdRow}> {minHeight? minHeight.height +' m': ''}</Typography>
+ <Typography className={classes.thirdRow}> {minHeight?.gps.longitude.toFixed(3)}</Typography>
+ <Typography className={classes.thirdRow}> {minHeight?.gps.latitude.toFixed(3)}</Typography>
+ </div>
+
+ </div>
+</AccordionDetails>
+</Accordion>
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MapInfo); \ No newline at end of file