summaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/lineOfSightApp/src
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
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')
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/actions/commonActions.ts80
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/actions/mapActions.ts67
-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
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/config.ts48
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/mapHandler.ts82
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/rootHandler.ts48
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/hooks/d3.ts37
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/index.html31
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/model/GPSProfileResult.ts19
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/model/Height.tsx22
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/model/LatLon.ts22
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/pluginLineOfSight.tsx138
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/services/heightService.ts62
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/styles/index.css13
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/styles/mapbox-gl.css1
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/utils/map.ts103
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/utils/math.ts30
-rw-r--r--sdnr/wt/odlux/apps/lineOfSightApp/src/views/main.tsx30
22 files changed, 1585 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/commonActions.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/commonActions.ts
new file mode 100644
index 000000000..3cc8ea4a7
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/commonActions.ts
@@ -0,0 +1,80 @@
+/**
+ * ============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 { Height } from "model/Height";
+import { isNumber } from "../utils/math";
+import { Action } from "../../../../framework/src/flux/action";
+ import { Dispatch } from "../../../../framework/src/flux/store";
+ import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
+
+ import { LatLon } from "../model/LatLon";
+
+
+ export class SetPassedInValuesAction extends Action{
+ constructor(public start: LatLon, public end: LatLon, public center: LatLon, public heightA : Height |null, public heightB: Height |null){
+ super();
+ }
+ }
+
+ export class SetReachableAction extends Action{
+ constructor(public reachable: boolean | null){
+ super();
+ }
+ }
+
+ export const SetPassedInValues = (values: (string|null)[]) => (dispatcher: Dispatch) =>{
+
+ const start: LatLon = {latitude: Number(values[0]), longitude: Number(values[1])}
+ const end: LatLon = {latitude: Number(values[2]), longitude: Number(values[3])};
+ const midpoint = calculateMidPoint(start.latitude, start.longitude, end.latitude, end.longitude);
+ const center: LatLon = {latitude: midpoint[1], longitude: midpoint[0]};
+ const heightA: Height | null = isNumber(values[4]) && isNumber(values[5]) ? {amsl:+values[4]!, antennaHeight: +values[5]!} : null;
+ const heightB: Height | null = isNumber(values[6]) && isNumber(values[7]) ? {amsl:+values[6]!, antennaHeight: +values[7]!} : null;
+
+
+ dispatcher(new SetPassedInValuesAction(start, end, center, heightA, heightB));
+ }
+
+ //taken from https://www.movable-type.co.uk/scripts/latlong.html
+const calculateMidPoint = (lat1: number, lon1: number, lat2: number, lon2: number) =>{
+
+ const dLon = degrees_to_radians(lon2 - lon1);
+
+ //convert to radians
+ lat1 = degrees_to_radians(lat1);
+ lat2 = degrees_to_radians(lat2);
+ lon1 = degrees_to_radians(lon1);
+
+ const Bx = Math.cos(lat2) * Math.cos(dLon);
+ const By = Math.cos(lat2) * Math.sin(dLon);
+ const lat3 = Math.atan2(Math.sin(lat1) + Math.sin(lat2), Math.sqrt((Math.cos(lat1) + Bx) * (Math.cos(lat1) + Bx) + By * By));
+ const lon3 = lon1 + Math.atan2(By, Math.cos(lat1) + Bx);
+
+ return [radians_to_degrees(lon3), radians_to_degrees(lat3)];
+}
+
+const degrees_to_radians = (degrees: number) =>
+{
+return degrees * (Math.PI/180);
+}
+
+const radians_to_degrees = (radians:number) =>{
+
+ var pi = Math.PI;
+ return radians * (180/pi);
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/mapActions.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/mapActions.ts
new file mode 100644
index 000000000..37ef5ee25
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/actions/mapActions.ts
@@ -0,0 +1,67 @@
+/**
+ * ============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 { LatLon } from "../model/LatLon";
+import { Action } from "../../../../framework/src/flux/action";
+import { Height } from "model/Height";
+
+export class SetChartAction extends Action{
+ constructor(public startPoint: LatLon, public endPoint: LatLon, public heightA: Height, public heightB: Height){
+ super();
+ }
+}
+
+export class SetStartPointAction extends Action{
+ constructor(public startPoint: LatLon|null){
+ super();
+ }
+}
+
+export class SetEndpointAction extends Action{
+ constructor(public endPoint: LatLon|null){
+ super();
+ }
+}
+
+export class SetHeightA extends Action{
+ constructor(public height: Height){
+ super();
+ }
+}
+
+export class SetHeightB extends Action{
+ constructor(public height: Height){
+ super();
+ }
+}
+
+export class ClearSavedChartAction extends Action{
+ constructor(){
+ super();
+ }
+}
+
+export class SetMapCenterAction extends Action{
+ /**
+ *
+ */
+ constructor(public point: LatLon, public zoom: number) {
+ super();
+
+ }
+} \ No newline at end of file
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
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/config.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/config.ts
new file mode 100644
index 000000000..bc1e1ff99
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/config.ts
@@ -0,0 +1,48 @@
+/**
+ * ============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==========================================================================
+ */
+
+export const URL_BASEPATH="lineOfSight";
+
+export const TERRAIN_URL="/terrain"; //http://10.20.11.163:5200 /terrain
+
+export const TILE_URL="/tiles"; //http://tile.openstreetmap.org /tiles
+
+
+export const OSM_STYLE = {
+ 'version': 8,
+ 'sources': {
+ 'raster-tiles': {
+ 'type': 'raster',
+ 'tiles': [
+ TILE_URL+'/{z}/{x}/{y}.png'
+ ],
+ 'tileSize': 256,
+ 'attribution':
+ '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+ }
+ },
+ 'layers': [
+ {
+ 'id': 'simple-tiles',
+ 'type': 'raster',
+ 'source': 'raster-tiles',
+ 'minZoom': 0,
+ 'maxZoom': 18
+ }
+ ]
+}; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/mapHandler.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/mapHandler.ts
new file mode 100644
index 000000000..6d11977bf
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/mapHandler.ts
@@ -0,0 +1,82 @@
+/**
+ * ============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 { LatLon } from "../model/LatLon";
+import { IActionHandler } from "../../../../framework/src/flux/action";
+import { SetPassedInValuesAction, SetReachableAction } from "../actions/commonActions";
+import { ClearSavedChartAction, SetChartAction, SetEndpointAction, SetHeightA, SetHeightB, SetMapCenterAction, SetStartPointAction } from "../actions/mapActions";
+import { Height } from "model/Height";
+import { isNullOrUndefined } from "util";
+
+
+
+ export interface IMap {
+ center: LatLon;
+ zoom: number;
+ start: LatLon |null;
+ heightA: Height | null;
+ end: LatLon|null;
+ heightB: Height | null;
+ ready: boolean |null;
+ }
+
+ const initialState: IMap = {
+ center: {latitude:52.4003, longitude:13.0584},
+ zoom: 12,
+ start: null,
+ end: null,
+ ready: null,
+ heightA: null,
+ heightB: null
+
+ }
+
+ export const mapHandler: IActionHandler<IMap> = (state = initialState, action) => {
+ if (action instanceof SetPassedInValuesAction) {
+ state = { ...state, start: action.start, end: action.end, center: action.center, heightA: action.heightA, heightB: action.heightB };
+ }
+ else if(action instanceof SetReachableAction){
+ state = { ...state, ready: action.reachable };
+
+ }else if(action instanceof SetChartAction){
+ state = {...state, start:action.startPoint, end: action.endPoint, heightA: action.heightA, heightB: action.heightB}
+ }
+ else if(action instanceof SetStartPointAction){
+ state = {...state, start:action.startPoint}
+
+ }
+ else if(action instanceof SetEndpointAction){
+ state = {...state, end:action.endPoint}
+
+ }
+ else if(action instanceof SetHeightA){
+ state = {...state, heightA:action.height}
+
+ }
+ else if(action instanceof SetHeightB){
+ state = {...state, heightB:action.height}
+
+ }
+ else if(action instanceof ClearSavedChartAction){
+ state= {...state, start: null, end: null, heightA:null, heightB: null}
+ }else if(action instanceof SetMapCenterAction){
+ state={...state, zoom: action.zoom,center:action.point}
+ }
+
+ return state;
+ } \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/rootHandler.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/rootHandler.ts
new file mode 100644
index 000000000..e7d58c41f
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/handlers/rootHandler.ts
@@ -0,0 +1,48 @@
+/**
+* ============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==========================================================================
+*/
+// main state handler
+
+import { combineActionHandler } from '../../../../framework/src/flux/middleware';
+
+// ** do not remove **
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+import { IActionHandler } from '../../../../framework/src/flux/action';
+
+
+import { IMap, mapHandler } from './mapHandler';
+
+export interface ILineOfSightAppStateState {
+ map: IMap;
+}
+
+
+
+
+declare module '../../../../framework/src/store/applicationStore' {
+ interface IApplicationStoreState {
+ lineOfSight: ILineOfSightAppStateState;
+ }
+}
+
+const actionHandlers = {
+ map: mapHandler,
+};
+
+export const lineofSightRootHandler = combineActionHandler<ILineOfSightAppStateState>(actionHandlers);
+export default lineofSightRootHandler;
+
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/hooks/d3.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/hooks/d3.ts
new file mode 100644
index 000000000..dfebe8605
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/hooks/d3.ts
@@ -0,0 +1,37 @@
+/**
+ * ============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==========================================================================
+ */
+
+/* eslint-disable react-hooks/exhaustive-deps */
+import { useEffect, useRef } from 'react';
+import type { DependencyList } from 'react';
+
+import * as d3 from 'd3';
+
+
+type SelectionType = d3.Selection<SVGSVGElement, d3.BaseType, null, undefined>;
+
+export const useD3 = (renderChartFn: (selection: SelectionType) => void, dependencies: DependencyList) => {
+ const ref = useRef<SVGSVGElement>(null);
+
+ useEffect(() => {
+ if (ref.current) renderChartFn(d3.select(ref.current));
+ return () => { };
+ }, dependencies);
+
+ return ref;
+}
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/index.html b/sdnr/wt/odlux/apps/lineOfSightApp/src/index.html
new file mode 100644
index 000000000..6c1478e42
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/index.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
+ <!-- <link rel="stylesheet" href="./vendor.css" > -->
+ <title>LineOfSightApp</title>
+</head>
+
+<body>
+ <div id="app"></div>
+ <script type="text/javascript" src="./require.js"></script>
+ <script type="text/javascript" src="./config.js"></script>
+
+ <script>
+ // run the application
+ require(["app","connectApp","faultApp", "networkMapApp", "lineOfSightApp", "linkCalculationApp"], function (app, connectApp, faultApp, networkMapApp, lineOfSightApp, linkCalculationApp) {
+ connectApp.register();
+ faultApp.register();
+ //configurationApp.register();
+ //linkCalculationApp.register();
+ networkMapApp.register();
+ lineOfSightApp.register();
+ app("./app.tsx").runApplication();
+ });
+ </script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/model/GPSProfileResult.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/GPSProfileResult.ts
new file mode 100644
index 000000000..567946bc6
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/GPSProfileResult.ts
@@ -0,0 +1,19 @@
+/**
+ * ============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==========================================================================
+ */
+
+export type GPSProfileResult = { height: number, gps: { latitude: number, longitude: number }, band: string, zone: number, easting: number, northing: number }; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/model/Height.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/Height.tsx
new file mode 100644
index 000000000..7014095cb
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/Height.tsx
@@ -0,0 +1,22 @@
+/**
+ * ============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==========================================================================
+ */
+
+export type Height = {
+ amsl: number;
+ antennaHeight: number;
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/model/LatLon.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/LatLon.ts
new file mode 100644
index 000000000..a447aa52a
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/model/LatLon.ts
@@ -0,0 +1,22 @@
+/**
+ * ============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==========================================================================
+ */
+
+export type LatLon ={
+ latitude: number,
+ longitude: number
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/pluginLineOfSight.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/pluginLineOfSight.tsx
new file mode 100644
index 000000000..b193cfb22
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/pluginLineOfSight.tsx
@@ -0,0 +1,138 @@
+/**
+ * ============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==========================================================================
+ */
+
+// app configuration and main entry point for the app
+
+import * as React from "react";
+import { faRoute } from '@fortawesome/free-solid-svg-icons'; // select app icon
+import applicationManager from '../../../framework/src/services/applicationManager';
+
+
+import { lineofSightRootHandler } from './handlers/rootHandler';
+import MainView from "./views/main";
+import applicationApi from "../../../framework/src/services/applicationApi";
+
+import { Redirect, Route, RouteComponentProps, Switch, useLocation, withRouter } from "react-router-dom";
+import connect, { Connect, IDispatcher } from "../../../framework/src/flux/connect";
+import { IApplicationStoreState } from "../../../framework/src/store/applicationStore";
+import { SetPassedInValues, SetReachableAction } from "./actions/commonActions";
+import { TERRAIN_URL, TILE_URL } from "./config";
+import { isNumber } from "./utils/math";
+
+const mapProps = (state: IApplicationStoreState) => ({
+});
+
+const mapDisp = (dispatcher: IDispatcher) => ({
+ setPassedInValues: (values: (string | null)[]) => dispatcher.dispatch(SetPassedInValues(values)),
+ setReachable: (reachable: boolean) => dispatcher.dispatch(new SetReachableAction(reachable))
+
+});
+
+let lastSearch = "";
+
+const useQuery = () => {
+ return new URLSearchParams(useLocation().search);
+}
+
+
+const LineOfSightApplicationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComponentProps<{ mountId?: string }> & Connect<typeof mapProps, typeof mapDisp>) => {
+
+ let query = useQuery();
+
+ // called when component finshed mounting
+ React.useEffect(() => {
+ extractAndDispatchUrlValues(props.location.search);
+
+ //check tiles/terrain connectivity
+ tryCheckConnection();
+
+ }, []);
+
+
+ const extractAndDispatchUrlValues = (url: string) => {
+
+ if (lastSearch !== url) {
+ lastSearch = url;
+
+ //if mandatory values aren't there, do nothing
+ if (areMandatoryParamsPresent(query)) {
+ const values = extractValuesFromURL(query);
+ props.setPassedInValues(values);
+ }
+ }
+ }
+
+ const tryCheckConnection =() =>{
+ const terrain = fetch(`${TERRAIN_URL}/`);
+ const tiles = fetch(`${TILE_URL}/10/0/0.png`);
+
+ Promise.all([terrain, tiles])
+ .then((result) => {
+ props.setReachable(true);
+
+ })
+ .catch(error=>{
+ console.error("services not reachable.");
+ console.error(error);
+ props.setReachable(false);
+
+ })
+
+ }
+
+ /***
+ *
+ * Checks if lat1, lon1, lat2, lon2 were passed in as url parameters
+ */
+ const areMandatoryParamsPresent = (query: URLSearchParams) => {
+
+ return isNumber(query.get("lat1")) && isNumber(query.get("lon1")) && isNumber(query.get("lat2")) && isNumber(query.get("lon2"))
+
+ }
+
+ const extractValuesFromURL = (query: URLSearchParams) => {
+
+ return [query.get("lat1"), query.get("lon1"), query.get("lat2"), query.get("lon2"), query.get("amslA"), query.get("antennaHeightA"), query.get("amslB"), query.get("antennaHeightB")]
+ }
+
+ return (
+ <MainView />
+ );
+});
+
+
+const LoSRouterApp = withRouter(connect(mapProps, mapDisp)((props: RouteComponentProps & Connect<typeof mapProps, typeof mapDisp>) => {
+
+ return (
+ <Switch>
+ <Route path={`${props.match.path}`} component={LineOfSightApplicationRouteAdapter} />
+ <Redirect to={`${props.match.path}`} />
+ </Switch>
+ )
+}));
+
+export function register() {
+ applicationManager.registerApplication({
+ name: "lineOfSight", // used as name of state as well
+ icon: faRoute,
+ rootActionHandler: lineofSightRootHandler,
+ rootComponent: LoSRouterApp,
+ menuEntry: "Line of Sight"
+ });
+}
+
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/services/heightService.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/services/heightService.ts
new file mode 100644
index 000000000..8b0535881
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/services/heightService.ts
@@ -0,0 +1,62 @@
+/**
+ * ============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 { GPSProfileResult } from "../model/GPSProfileResult";
+import { TERRAIN_URL } from "../config";
+import { LatLon } from "../model/LatLon";
+
+export const apiUrlBase="api/Query";
+
+export const getGPSProfile = async (start: LatLon, end: LatLon) => {
+ const url = `${TERRAIN_URL}/${apiUrlBase}/GPSProfileRecords`;
+
+ const result = await fetch(url, {
+ method: "POST",
+ body: JSON.stringify({ start, end }),
+ headers: {
+ 'Content-Type': 'application/json'
+ // 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+ if (result.ok) {
+ const data = await result.json() as GPSProfileResult[];
+ return data;
+ }
+
+ return Number.NaN;
+}
+
+export const getGPSHeight = async (gpsCoord: LatLon) => {
+ const url = `${TERRAIN_URL}/${apiUrlBase}/GPSHeight`;
+
+ const result = await fetch(url, {
+ method: "POST",
+ body: JSON.stringify(gpsCoord),
+ headers: {
+ 'Content-Type': 'application/json'
+ // 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+ if (result.ok) {
+ const data = await result.json() as { height: number };
+ return data.height;
+ }else{
+ return undefined;
+ }
+}
+
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/index.css b/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/index.css
new file mode 100644
index 000000000..ec2585e8c
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/mapbox-gl.css b/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/mapbox-gl.css
new file mode 100644
index 000000000..03c479af9
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/styles/mapbox-gl.css
@@ -0,0 +1 @@
+.mapboxgl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgba(0,0,0,0);text-align:left}.mapboxgl-map:-webkit-full-screen{width:100%;height:100%}.mapboxgl-canary{background-color:salmon}.mapboxgl-canvas-container.mapboxgl-interactive,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer{cursor:pointer}.mapboxgl-canvas-container.mapboxgl-interactive:active,.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active{cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas{touch-action:pan-x pan-y}.mapboxgl-canvas-container.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:pinch-zoom}.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas{touch-action:none}.mapboxgl-ctrl-bottom-left,.mapboxgl-ctrl-bottom-right,.mapboxgl-ctrl-top-left,.mapboxgl-ctrl-top-right{position:absolute;pointer-events:none;z-index:2}.mapboxgl-ctrl-top-left{top:0;left:0}.mapboxgl-ctrl-top-right{top:0;right:0}.mapboxgl-ctrl-bottom-left{bottom:0;left:0}.mapboxgl-ctrl-bottom-right{right:0;bottom:0}.mapboxgl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.mapboxgl-ctrl-top-left .mapboxgl-ctrl{margin:10px 0 0 10px;float:left}.mapboxgl-ctrl-top-right .mapboxgl-ctrl{margin:10px 10px 0 0;float:right}.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl{margin:0 0 10px 10px;float:left}.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl{margin:0 10px 10px 0;float:right}.mapboxgl-ctrl-group{border-radius:4px;background:#fff}.mapboxgl-ctrl-group:not(:empty){-moz-box-shadow:0 0 2px rgba(0,0,0,.1);-webkit-box-shadow:0 0 2px rgba(0,0,0,.1);box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (-ms-high-contrast:active){.mapboxgl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.mapboxgl-ctrl-group button{width:29px;height:29px;display:block;padding:0;outline:none;border:0;box-sizing:border-box;background-color:transparent;cursor:pointer}.mapboxgl-ctrl-group button+button{border-top:1px solid #ddd}.mapboxgl-ctrl button .mapboxgl-ctrl-icon{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:50%}@media (-ms-high-contrast:active){.mapboxgl-ctrl-icon{background-color:transparent}.mapboxgl-ctrl-group button+button{border-top:1px solid ButtonText}}.mapboxgl-ctrl button::-moz-focus-inner{border:0;padding:0}.mapboxgl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl button:disabled{cursor:not-allowed}.mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon{opacity:.25}.mapboxgl-ctrl button:not(:disabled):hover{background-color:rgba(0,0,0,.05)}.mapboxgl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.mapboxgl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.mapboxgl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.mapboxgl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.mapboxgl-ctrl-group button:focus:only-child{border-radius:inherit}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23999'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")}}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting .mapboxgl-ctrl-icon{-webkit-animation:mapboxgl-spin 2s linear infinite;-moz-animation:mapboxgl-spin 2s infinite linear;-o-animation:mapboxgl-spin 2s infinite linear;-ms-animation:mapboxgl-spin 2s infinite linear;animation:mapboxgl-spin 2s linear infinite}@media (-ms-high-contrast:active){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23999'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23666'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 005.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 009 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 003.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0011 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 110 7 3.5 3.5 0 110-7z'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath d='M14 5l1 1-9 9-1-1 9-9z' fill='red'/%3E%3C/svg%3E")}}@-webkit-keyframes mapboxgl-spin{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@-moz-keyframes mapboxgl-spin{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(1turn)}}@-o-keyframes mapboxgl-spin{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(1turn)}}@-ms-keyframes mapboxgl-spin{0%{-ms-transform:rotate(0deg)}to{-ms-transform:rotate(1turn)}}@keyframes mapboxgl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.mapboxgl-ctrl-logo{width:88px;height:23px;margin:0 0 -4px -4px;display:block;background-repeat:no-repeat;cursor:pointer;overflow:hidden;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg opacity='.3' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cg opacity='.9' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/g%3E%3C/svg%3E")}a.mapboxgl-ctrl-logo.mapboxgl-compact{width:23px}@media (-ms-high-contrast:active){a.mapboxgl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cg fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/g%3E%3C/svg%3E")}}@media (-ms-high-contrast:black-on-white){a.mapboxgl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='88' height='23' viewBox='0 0 88 23' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd'%3E%3Cdefs%3E%3Cpath id='a' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='b' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='c'%3E%3Crect width='100%25' height='100%25' fill='%23fff'/%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/mask%3E%3Cg stroke='%23fff' stroke-width='3' fill='%23fff'%3E%3Ccircle mask='url(%23c)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23b' mask='url(%23c)'/%3E%3C/g%3E%3Cuse xlink:href='%23a'/%3E%3Cuse xlink:href='%23b'/%3E%3C/svg%3E")}}.mapboxgl-ctrl.mapboxgl-ctrl-attrib{padding:0 5px;background-color:hsla(0,0%,100%,.5);margin:0}@media screen{.mapboxgl-ctrl-attrib.mapboxgl-compact{min-height:20px;padding:0;margin:10px;position:relative;background-color:#fff;border-radius:3px 12px 12px 3px}.mapboxgl-ctrl-attrib.mapboxgl-compact:hover{padding:2px 24px 2px 4px;visibility:visible;margin-top:6px}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:hover,.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:hover{padding:2px 4px 2px 24px;border-radius:12px 3px 3px 12px}.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-inner{display:none}.mapboxgl-ctrl-attrib.mapboxgl-compact:hover .mapboxgl-ctrl-attrib-inner{display:block}.mapboxgl-ctrl-attrib.mapboxgl-compact:after{content:"";cursor:pointer;position:absolute;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E");background-color:hsla(0,0%,100%,.5);width:24px;height:24px;box-sizing:border-box;border-radius:12px}.mapboxgl-ctrl-bottom-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;right:0}.mapboxgl-ctrl-top-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{top:0;right:0}.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{top:0;left:0}.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after{bottom:0;left:0}}@media screen and (-ms-high-contrast:active){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' fill='%23fff'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E")}}@media screen and (-ms-high-contrast:black-on-white){.mapboxgl-ctrl-attrib.mapboxgl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='24' height='24' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1012 0 6 6 0 10-12 0m5-3a1 1 0 102 0 1 1 0 10-2 0m0 3a1 1 0 112 0v3a1 1 0 11-2 0'/%3E%3C/svg%3E")}}.mapboxgl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.mapboxgl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.mapboxgl-ctrl-attrib .mapbox-improve-map{font-weight:700;margin-left:2px}.mapboxgl-attrib-empty{display:none}.mapboxgl-ctrl-scale{background-color:hsla(0,0%,100%,.75);font-size:10px;border:2px solid #333;border-top:#333;padding:0 5px;color:#333;box-sizing:border-box}.mapboxgl-popup{position:absolute;top:0;left:0;display:-webkit-flex;display:flex;will-change:transform;pointer-events:none}.mapboxgl-popup-anchor-top,.mapboxgl-popup-anchor-top-left,.mapboxgl-popup-anchor-top-right{-webkit-flex-direction:column;flex-direction:column}.mapboxgl-popup-anchor-bottom,.mapboxgl-popup-anchor-bottom-left,.mapboxgl-popup-anchor-bottom-right{-webkit-flex-direction:column-reverse;flex-direction:column-reverse}.mapboxgl-popup-anchor-left{-webkit-flex-direction:row;flex-direction:row}.mapboxgl-popup-anchor-right{-webkit-flex-direction:row-reverse;flex-direction:row-reverse}.mapboxgl-popup-tip{width:0;height:0;border:10px solid transparent;z-index:1}.mapboxgl-popup-anchor-top .mapboxgl-popup-tip{-webkit-align-self:center;align-self:center;border-top:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip{-webkit-align-self:flex-start;align-self:flex-start;border-top:none;border-left:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip{-webkit-align-self:flex-end;align-self:flex-end;border-top:none;border-right:none;border-bottom-color:#fff}.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip{-webkit-align-self:center;align-self:center;border-bottom:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip{-webkit-align-self:flex-start;align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip{-webkit-align-self:flex-end;align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.mapboxgl-popup-anchor-left .mapboxgl-popup-tip{-webkit-align-self:center;align-self:center;border-left:none;border-right-color:#fff}.mapboxgl-popup-anchor-right .mapboxgl-popup-tip{-webkit-align-self:center;align-self:center;border-right:none;border-left-color:#fff}.mapboxgl-popup-close-button{position:absolute;right:0;top:0;border:0;border-radius:0 3px 0 0;cursor:pointer;background-color:transparent}.mapboxgl-popup-close-button:hover{background-color:rgba(0,0,0,.05)}.mapboxgl-popup-content{position:relative;background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:10px 10px 15px;pointer-events:auto}.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content{border-top-left-radius:0}.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content{border-top-right-radius:0}.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content{border-bottom-left-radius:0}.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content{border-bottom-right-radius:0}.mapboxgl-popup-track-pointer{display:none}.mapboxgl-popup-track-pointer *{pointer-events:none;user-select:none}.mapboxgl-map:hover .mapboxgl-popup-track-pointer{display:flex}.mapboxgl-map:active .mapboxgl-popup-track-pointer{display:none}.mapboxgl-marker{position:absolute;top:0;left:0;will-change:transform}.mapboxgl-user-location-dot,.mapboxgl-user-location-dot:before{background-color:#1da1f2;width:15px;height:15px;border-radius:50%}.mapboxgl-user-location-dot:before{content:"";position:absolute;-webkit-animation:mapboxgl-user-location-dot-pulse 2s infinite;-moz-animation:mapboxgl-user-location-dot-pulse 2s infinite;-ms-animation:mapboxgl-user-location-dot-pulse 2s infinite;animation:mapboxgl-user-location-dot-pulse 2s infinite}.mapboxgl-user-location-dot:after{border-radius:50%;border:2px solid #fff;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px;box-sizing:border-box;box-shadow:0 0 3px rgba(0,0,0,.35)}@-webkit-keyframes mapboxgl-user-location-dot-pulse{0%{-webkit-transform:scale(1);opacity:1}70%{-webkit-transform:scale(3);opacity:0}to{-webkit-transform:scale(1);opacity:0}}@-ms-keyframes mapboxgl-user-location-dot-pulse{0%{-ms-transform:scale(1);opacity:1}70%{-ms-transform:scale(3);opacity:0}to{-ms-transform:scale(1);opacity:0}}@keyframes mapboxgl-user-location-dot-pulse{0%{transform:scale(1);opacity:1}70%{transform:scale(3);opacity:0}to{transform:scale(1);opacity:0}}.mapboxgl-user-location-dot-stale{background-color:#aaa}.mapboxgl-user-location-dot-stale:after{display:none}.mapboxgl-user-location-accuracy-circle{background-color:rgba(29,161,242,.2);width:1px;height:1px;border-radius:100%}.mapboxgl-crosshair,.mapboxgl-crosshair .mapboxgl-interactive,.mapboxgl-crosshair .mapboxgl-interactive:active{cursor:crosshair}.mapboxgl-boxzoom{position:absolute;top:0;left:0;width:0;height:0;background:#fff;border:2px dotted #202020;opacity:.5}@media print{.mapbox-improve-map{display:none}} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/map.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/map.ts
new file mode 100644
index 000000000..abdd27ed2
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/map.ts
@@ -0,0 +1,103 @@
+/**
+ * ============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 mapboxgl from "mapbox-gl";
+import { LatLon } from "../model/LatLon";
+
+
+export const addBaseSource = (map : mapboxgl.Map, name: string) =>{
+
+ if(!map.getSource(name))
+
+ map.addSource(name, {
+ type: 'geojson',
+ data: { type: "FeatureCollection", features: [] }
+ });
+
+}
+
+export const addPoint = (map : mapboxgl.Map, point: LatLon) =>{
+ const json = `{
+ "type": "Feature",
+ "properties": {},
+ "geometry": {
+ "type": "Point",
+ "coordinates":
+ [${point.longitude}, ${point.latitude}]
+ }
+ }`;
+
+
+ (map.getSource("route") as mapboxgl.GeoJSONSource).setData(JSON.parse(json));
+}
+
+export const addBaseLayer = (map: mapboxgl.Map, sourceName: string) =>{
+
+ if(!map.getLayer('line'))
+ map.addLayer({
+ 'id': 'line',
+ 'type': 'line',
+ 'source': sourceName,
+ 'layout': {
+ 'line-join': 'round',
+ 'line-cap': 'round'
+ },
+ 'paint': {
+ 'line-color': '#88A',
+ 'line-width': 6,
+ 'line-opacity': 0.75
+ }
+ });
+
+ if(!map.getLayer('points'))
+ map.addLayer({
+ id: 'points',
+ type: 'circle',
+ source: sourceName,
+ paint: {
+ 'circle-radius': 5,
+ 'circle-color': '#223b53',
+ 'circle-stroke-color': '#225ba3',
+ 'circle-stroke-width': 3,
+ 'circle-opacity': 0.5
+ }
+ });
+}
+
+export const calculateDistanceInMeter = (lat1: number, lon1: number, lat2: number, lon2: number) => {
+ const lonRad1 = toRad(lon1);
+ const latRad1 = toRad(lat1);
+ const lonRad2 = toRad(lon2);
+ const latRad2 = toRad(lat2);
+
+ const dLon = lonRad2 - lonRad1;
+ const dLat = latRad2 - latRad1;
+ const a = Math.pow(Math.sin(dLat / 2), 2) +
+ Math.cos(latRad1) * Math.cos(latRad2) *
+ Math.pow(Math.sin(dLon / 2), 2);
+
+ const c = 2 * Math.asin(Math.sqrt(a));
+
+ return 6378 * c;
+
+ }
+
+ function toRad(value: number) {
+ return (value * Math.PI) / 180;
+ }
+ \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/math.ts b/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/math.ts
new file mode 100644
index 000000000..9e0447b97
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/utils/math.ts
@@ -0,0 +1,30 @@
+/**
+ * ============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==========================================================================
+ */
+
+export const max = <T,>(a: T[], p: (v: T) => Number) => a.reduce<T>((m, x) => p(m) > p(x) ? m : x, a[0]);
+export const min = <T,>(a: T[], p: (v: T) => Number) => a.reduce<T>((m, x) => p(m) < p(x) ? m : x, a[0]);
+
+export const isNumber = (value: string|null) =>{
+
+ if(!value){
+ return false;
+ }else{
+ const num = Number(value);
+ return !isNaN(num);
+ }
+ } \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src/views/main.tsx b/sdnr/wt/odlux/apps/lineOfSightApp/src/views/main.tsx
new file mode 100644
index 000000000..bbe6f34e6
--- /dev/null
+++ b/sdnr/wt/odlux/apps/lineOfSightApp/src/views/main.tsx
@@ -0,0 +1,30 @@
+/**
+ * ============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 Map from '../components/map';
+
+function MainView() {
+ return (
+ <div className="App" style={{height:"100%"}}>
+ <Map />
+ </div>
+ );
+}
+
+export default MainView; \ No newline at end of file