diff options
author | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-08-05 08:50:16 +0200 |
---|---|---|
committer | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-08-05 08:50:16 +0200 |
commit | 3ba5eb125ac8890968e4437b098e39195d699434 (patch) | |
tree | ac58a39013fc9ab2cd468da83e4c41cd2d62b600 /sdnr/wt/odlux/apps | |
parent | 437f67407aece6f7aed8e989638b0d64075f0c0a (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')
42 files changed, 2401 insertions, 148 deletions
diff --git a/sdnr/wt/odlux/apps/app-feature/pom.xml b/sdnr/wt/odlux/apps/app-feature/pom.xml index 738ac6964..17c0dab6e 100644 --- a/sdnr/wt/odlux/apps/app-feature/pom.xml +++ b/sdnr/wt/odlux/apps/app-feature/pom.xml @@ -106,6 +106,11 @@ <artifactId>sdnr-wt-odlux-app-linkCalculationApp</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sdnr-wt-odlux-app-lineOfSightApp</artifactId> + <version>${project.version}</version> + </dependency> diff --git a/sdnr/wt/odlux/apps/app-installer/pom.xml b/sdnr/wt/odlux/apps/app-installer/pom.xml index 1ac88593f..721d56488 100755 --- a/sdnr/wt/odlux/apps/app-installer/pom.xml +++ b/sdnr/wt/odlux/apps/app-installer/pom.xml @@ -132,6 +132,11 @@ <artifactId>sdnr-wt-odlux-app-linkCalculationApp</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sdnr-wt-odlux-app-lineOfSightApp</artifactId> + <version>${project.version}</version> + </dependency> </dependencies> diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx index 5d0757ab2..4a7a0d269 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx @@ -194,11 +194,12 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement <NetworkElementTable stickyHeader tableId="network-element-table" customActionButtons={[refreshNetworkElementsAction, ...canAdd ? [addRequireNetworkElementAction] : []]} columns={[ { property: "nodeId", title: "Node Name", type: ColumnType.text }, { property: "isRequired", title: "Required", type: ColumnType.boolean }, - { property: "status", title: "Connection Status", type: ColumnType.text }, + { property: "status", title: "Connection Status", type: ColumnType.text, width:'15%' }, { property: "host", title: "Host", type: ColumnType.text }, { property: "port", title: "Port", type: ColumnType.numeric }, { property: "coreModelCapability", title: "Core Model", type: ColumnType.text }, - { property: "deviceType", title: "Type", type: ColumnType.text }, + { property: "deviceType", title: "Device Type", type: ColumnType.text }, + { property: "deviceFunction", title: "Device Function", type: ColumnType.text, width: '15%' } ]} idProperty="id" {...this.props.networkElementsActions} {...this.props.networkElementsProperties} asynchronus createContextMenu={rowData => { return this.getContextMenu(rowData); diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts index 89070ab39..bc15afb31 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts @@ -30,6 +30,7 @@ export type NetworkElementConnection = { status?: "Connected" | "mounted" | "unmounted" | "Connecting" | "Disconnected" | "idle"; coreModelCapability?: string; deviceType?: string; + deviceFunction?: string; nodeDetails?: { availableCapabilites: { capabilityOrigin: string; diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/.babelrc b/sdnr/wt/odlux/apps/lineOfSightApp/.babelrc new file mode 100644 index 000000000..3d8cd1260 --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + ["@babel/preset-react"], + ["@babel/preset-env", { + "targets": { + "chrome": "66" + }, + "spec": true, + "loose": false, + "modules": false, + "debug": false, + "useBuiltIns": "usage", + "forceAllTransforms": true + }] + ], + "plugins": [] +} diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/package.json b/sdnr/wt/odlux/apps/lineOfSightApp/package.json new file mode 100644 index 000000000..dbba7ecfb --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/package.json @@ -0,0 +1,47 @@ +{ + "name": "@odlux/line-of-sight-app", + "version": "0.1.0", + "description": "A react based modular UI to display event log from a database.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --env debug", + "build": "webpack --env release --config webpack.config.js", + "build:dev": "webpack --env debug --config webpack.config.js" + }, + "repository": { + "type": "git", + "url": "https://git.mfico.de/highstreet-technologies/odlux.git" + }, + "keywords": [ + "reactjs", + "redux", + "ui", + "framework" + ], + "author": "Aijana Schumann", + "license": "Apache-2.0", + "dependencies": { + "@odlux/framework": "*", + "@types/d3": "^6.7.0", + "@types/mapbox-gl": "^1.10.2", + "@types/node": "^12.0.0", + "d3": "^7.0.0", + "d3-polygon": "^3.0.1", + "mapbox-gl": "^1.11.0", + "object.values": "^1.1.1" + }, + "peerDependencies": { + "@material-ui/core": "4.11.4", + "@material-ui/icons": "4.11.2", + "@types/classnames": "2.2.6", + "@types/flux": "3.1.8", + "@types/jquery": "3.3.10", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", + "jquery": "3.3.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" + } +} diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/pom.xml b/sdnr/wt/odlux/apps/lineOfSightApp/pom.xml new file mode 100644 index 000000000..29184a78b --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/pom.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ ============LICENSE_START======================================================= + ~ ONAP : ccsdk features + ~ ================================================================================ + ~ Copyright (C) 2020 AT&T 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======================================================= + ~ + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.onap.ccsdk.parent</groupId> + <artifactId>binding-parent</artifactId> + <version>2.2.0-SNAPSHOT</version> + <relativePath/> + </parent> + + <groupId>org.onap.ccsdk.features.sdnr.wt</groupId> + <artifactId>sdnr-wt-odlux-app-lineOfSightApp</artifactId> + <version>1.2.0-SNAPSHOT</version> + <packaging>bundle</packaging> + + <name>ccsdk-features :: ${project.artifactId}</name> + <licenses> + <license> + <name>Apache License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0</url> + </license> + </licenses> + + <properties> + <maven.javadoc.skip>true</maven.javadoc.skip> + </properties> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sdnr-wt-odlux-core-model</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sdnr-wt-odlux-core-provider</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <sourceDirectory>src2/main/java</sourceDirectory> + <resources> + <resource> + <directory>dist</directory> + <targetPath>odlux</targetPath> + </resource> + <resource> + <directory>src2/main/resources</directory> + </resource> + <resource> + <directory>src2/test/resources</directory> + </resource> + </resources> + <plugins> + <plugin> + <artifactId>maven-clean-plugin</artifactId> + <configuration> + <filesets> + <fileset> + <directory>dist</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>node</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>node_modules</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>../node_modules</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <!-- eclipse bug build bin folder in basedir --> + <fileset> + <directory>bin</directory> + <followSymlinks>false</followSymlinks> + </fileset> + </filesets> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>build-helper-maven-plugin</artifactId> + <executions> + <execution> + <id>add-test-source</id> + <phase>generate-test-sources</phase> + <goals> + <goal>add-test-source</goal> + </goals> + <configuration> + <sources> + <source>src2/test/java</source> + </sources> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>de.jacks-it-lab</groupId> + <artifactId>frontend-maven-plugin</artifactId> + <version>1.7.2</version> + <executions> + <execution> + <id>install node and yarn</id> + <goals> + <goal>install-node-and-yarn</goal> + </goals> + <!-- optional: default phase is "generate-resources" --> + <phase>initialize</phase> + <configuration> + <nodeVersion>v12.13.0</nodeVersion> + <yarnVersion>v1.22.10</yarnVersion> + </configuration> + </execution> + <execution> + <id>yarn build</id> + <goals> + <goal>yarn</goal> + </goals> + <configuration> + <arguments>run build</arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> 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': + '© <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 diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java b/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java new file mode 100644 index 000000000..43b072c4b --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java @@ -0,0 +1,68 @@ +/* + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt + * ================================================================================================= + * Copyright (C) 2019 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========================================================================== + */ +package org.onap.ccsdk.features.sdnr.wt.odlux.bundles; + +import org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundle; +import org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundleLoader; + +public class MyOdluxBundle extends OdluxBundle { + + @Override + public void initialize() { + super.initialize(); + } + + @Override + public void clean() { + super.clean(); + } + + @Override + public String getResourceFileContent(String filename) { + return super.getResourceFileContent(filename); + } + + @Override + public boolean hasResource(String filename) { + return super.hasResource(filename); + } + + @Override + public void setBundleName(String bundleName) { + super.setBundleName(bundleName); + } + + @Override + public void setLoader(OdluxBundleLoader loader) { + super.setLoader(loader); + } + + @Override + public String getBundleName() { + return super.getBundleName(); + } + + @Override + public OdluxBundleLoader getLoader() { + return super.getLoader(); + } + + public MyOdluxBundle() { + super(); + } +} diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml b/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml new file mode 100644 index 000000000..982379dda --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -0,0 +1,9 @@ +<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"> + <reference id="loadersvc" availability="mandatory" activation="eager" interface="org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundleLoader"/> + + <bean id="bundle" init-method="initialize" destroy-method="clean" class="org.onap.ccsdk.features.sdnr.wt.odlux.bundles.MyOdluxBundle"> + <property name="loader" ref="loadersvc"/> + <property name="bundleName" value="lineOfSightApp"/> + <property name="index" value="130"/> + </bean> +</blueprint>
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java b/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java new file mode 100644 index 000000000..c319bb189 --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java @@ -0,0 +1,46 @@ +/* + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt + * ================================================================================================= + * Copyright (C) 2019 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========================================================================== + */ +package org.onap.ccsdk.features.sdnr.wt.odlux.bundles.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.onap.ccsdk.features.sdnr.wt.odlux.OdluxBundleLoaderImpl; +import org.onap.ccsdk.features.sdnr.wt.odlux.bundles.MyOdluxBundle; + +public class TestBundleRes { + + @Test + public void test() { + OdluxBundleLoaderImpl loader = OdluxBundleLoaderImpl.getInstance(); + MyOdluxBundle b = new MyOdluxBundle(); + b.setLoader(loader); + b.setIndex(0); + b.setBundleName("abc"); + b.initialize(); + assertTrue(loader.getNumberOfBundles()==1); + assertNotNull(b.getLoader()); + assertEquals("abc",b.getBundleName()); + assertTrue(b.hasResource("test.js")); + assertNotNull(b.getResourceFileContent("test.js")); + b.clean(); + assertTrue(loader.getNumberOfBundles()==0); + } + +} diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/resources/test.js b/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/resources/test.js new file mode 100644 index 000000000..b47fdc39f --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/src2/test/resources/test.js @@ -0,0 +1,5 @@ +asdac sad +as +d +sad + sadfa
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/tsconfig.json b/sdnr/wt/odlux/apps/lineOfSightApp/tsconfig.json new file mode 100644 index 000000000..a66b5d828 --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist", + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": false, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "strictNullChecks": true, + "pretty": true, + "newLine": "LF", + "module": "es2015", + "target": "es2016", + "moduleResolution": "node", + "experimentalDecorators": true, + "jsx": "preserve", + "lib": [ + "dom", + "es2015", + "es2016" + ], + "types": [ + "prop-types", + "react", + "react-dom" + ] + }, + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/sdnr/wt/odlux/apps/lineOfSightApp/webpack.config.js b/sdnr/wt/odlux/apps/lineOfSightApp/webpack.config.js new file mode 100644 index 000000000..54ba1b499 --- /dev/null +++ b/sdnr/wt/odlux/apps/lineOfSightApp/webpack.config.js @@ -0,0 +1,211 @@ +/** + * Webpack 4 configuration file + * see https://webpack.js.org/configuration/ + * see https://webpack.js.org/configuration/dev-server/ + */ + +"use strict"; + +const path = require("path"); +const webpack = require("webpack"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const TerserPlugin = require('terser-webpack-plugin'); + +// const __dirname = (path => path.replace(/^([a-z]\:)/, c => c.toUpperCase()))(process.__dirname()); + +module.exports = (env) => { + const distPath = path.resolve(__dirname, env === "release" ? "." : "../..", "dist"); + const frameworkPath = path.resolve(__dirname, env === "release" ? "../../framework" : "../..", "dist"); + return [{ + name: "App", + + mode: "none", //disable default behavior + + target: "web", + + context: path.resolve(__dirname, "src"), + + entry: { + lineOfSightApp: ["./pluginLineOfSight.tsx"] + }, + + devtool: env === "release" ? false : "source-map", + + resolve: { + extensions: [".ts", ".tsx", ".js", ".jsx"] + }, + + output: { + path: distPath, + filename: "[name].js", + library: "[name]", + libraryTarget: "umd2", + chunkFilename: "[name].js" + }, + module: { + rules: [{ + test: /\.tsx?$/, + exclude: /node_modules/, + use: [{ + loader: "babel-loader" + }, { + loader: "ts-loader" + }] + }, { + test: /\.jsx?$/, + exclude: /node_modules/, + use: [{ + loader: "babel-loader" + }] + }, + { + test: /\.(png|gif|jpg|svg)$/, + use: [{ + loader: 'url-loader', + options: { + limit: 10000, + name: './icons/[hash].[ext]' + } + }] + }, + { + test: /\.css$/i, + use: ["style-loader", "css-loader"], + }, + ] + }, + + optimization: { + noEmitOnErrors: true, + namedModules: env !== "release", + minimize: env === "release", + minimizer: env !== "release" ? [] : [new TerserPlugin({ + terserOptions: { + warnings: false, // false, true, "verbose" + compress: { + drop_console: true, + drop_debugger: true, + } + } + })], + }, + + plugins: [ + new webpack.DllReferencePlugin({ + context: path.resolve(__dirname, "../../framework/src"), + manifest: require(path.resolve(frameworkPath, "vendor-manifest.json")), + sourceType: "umd2" + }), + new webpack.DllReferencePlugin({ + context: path.resolve(__dirname, "../../framework/src"), + manifest: require(path.resolve(frameworkPath, "app-manifest.json")), + sourceType: "umd2" + }), + ...(env === "release") ? [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: "'production'", + VERSION: JSON.stringify(require("./package.json").version) + } + }), + ] : [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: "'development'", + VERSION: JSON.stringify(require("./package.json").version) + } + }), + new CopyWebpackPlugin([{ + from: 'index.html', + to: distPath + }]), + ] + ], + + devServer: { + public: "http://localhost:3100", + contentBase: frameworkPath, + + compress: true, + headers: { + "Access-Control-Allow-Origin": "*" + }, + host: "0.0.0.0", + port: 3100, + disableHostCheck: true, + historyApiFallback: true, + inline: true, + hot: false, + quiet: false, + stats: { + colors: true + }, + proxy: { + "/yang-schema/": { + target: "http://sdnr:8181", + secure: false + }, + "/userdata": { + target: "http://sdnr:8181", + secure: false + }, + "/userdata/": { + target: "http://sdnr:8181", + secure: false + }, + "/oauth2/": { + target: "http://sdnr:8181", + secure: false + }, + "/database/": { + target: "http://sdnr:8181", + secure: false + }, + "/restconf/": { + target: "http://sdnr:8181", + secure: false + }, + "/rests/": { + target: "http://sdnr:8181", + secure: false + }, + "/topology/": { + target: "http://localhost:3002", + secure: false + }, + "/sitedoc/": { + target: "http://10.20.35.184:3002", + secure: false, + pathRewrite(pathname) { + return pathname.replace(/^\/sitedoc/, '/topology/stadok') + } + }, + "/terrain/": { + target: "http://10.20.11.163:5200", + secure: false, + pathRewrite(pathname) { + return pathname.replace(/^\/terrain/, '/') + } + }, + "/tiles/": { + target: "http://tile.openstreetmap.org", + secure: false, + pathRewrite(pathname) { + return pathname.replace(/^\/tiles/, '') + } + }, + "/help/": { + target: "http://sdnr:8181", + secure: false + }, + "/websocket": { + target: "http://sdnr:8181", + ws: true, + changeOrigin: true, + secure: false + } + } + + } + }]; +} diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/actions/commonLinkCalculationActions.ts b/sdnr/wt/odlux/apps/linkCalculationApp/src/actions/commonLinkCalculationActions.ts index 0849058dc..d499ec209 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/actions/commonLinkCalculationActions.ts +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/actions/commonLinkCalculationActions.ts @@ -116,60 +116,45 @@ export class UpdateWorstMonthRainAction extends Action { } } -export class UpdateEIRPAction extends Action { - constructor(public eirpA: number,public eirpB: number) { + +export class UpdateAntennaGainAction extends Action { + constructor(public antennaGainA: number, public antennaGainB: number) { super(); } } -export class UpdateAntennaGainAction extends Action { - constructor(public antennaGainList: string[]) { +export class updateAntennaNameAction extends Action { + constructor(public antennaNameA: string, public antennaNameB: string) { super(); } } -export class UpdateAntennaListAction extends Action { - constructor(public antennaList: string[]) { +export class UpdateTxPowerAction extends Action { + constructor(public txPowerA: string | null, public txPowerB: string | null) { super(); } } -export class UpdateAntennaAction extends Action { - constructor(public antennaA: string | null, public antennaB : string | null) { +export class UpdateRxSensitivityAction extends Action { + constructor(public rxSensitivityA: string | null, public rxSensitivityB: string | null) { super(); } } -export class UpdateRadioAttributesAction extends Action { - constructor(public som: number , public eirpA : number, public eirpB : number) { +export class UpdateWaveguideLossAction extends Action { + constructor(public waveguideLossA: number, public waveguideLossB: number) { super(); } } -export class UpdateTxPowerAction extends Action { - constructor(public txPowerA: string | null , public txPowerB : string | null) { + +export class UpdateEIRPAction extends Action { + constructor(public eirpA: number, public eirpB: number) { super(); } } -export class UpdateRxSensitivityAction extends Action { - constructor(public rxSensitivityA: string | null , public rxSensitivityB : string | null) { +export class UpdateRxPowerAction extends Action { + constructor(public rxPowerA: number, public rxPowerB: number) { super(); } } - - -export const updateAntennaList = (frequency: number) => async (dispatcher: Dispatch, getState: () => IApplicationStoreState) => { - let antennaList: string[] = [] - let antennaDiameterList: string[] = [] - let antennaGainList :string[] =[] - //switch case here frequency = 26? antennaList.push - switch (frequency) { - case 7: antennaList.push('ANDREW VHLPX2.5-7W', 'ANDREW VHLPX3-7W', 'ANDREW VHLPX4-7W', 'ANDREW VHLPX6-7W' ), antennaDiameterList.push('0.6','0.9','1.2','1.8'), antennaGainList.push('33.90','35.50','37.30','40.61'); break - case 11: antennaList.push('ANDREW VHLPX2-11W', 'ANDREW VHLPX3-11W', 'ANDREW VHLPX4-11W'), antennaDiameterList.push('0.6','0.9','1.2'), antennaGainList.push('34.50','38.4','40.70');break - case 15: antennaList.push('ANDREW VHLPX1-15', 'ANDREW VHLPX2-15', 'ANDREW VHLPX3-15', 'ANDREW VHLPX4-15'), antennaDiameterList.push('0.3','0.6','0.9','1.2'), antennaGainList.push('32.00','36.80','41.11','42.90');break - case 23: antennaList.push('ANDREW VHLPX1-23', 'ANDREW VHLPX2-23', 'ANDREW VHLPX3-23', 'ANDREW VHLPX4-23'), antennaDiameterList.push('0.3','0.6','0.9','1.2'), antennaGainList.push('35.30','40.21','44.80','46.71');break - case 26: antennaList.push('ANDREW VHLPX1-26', 'ANDREW VHLPX2-26', 'ANDREW VHLPX3-26'), antennaDiameterList.push('0.3','0.6','0.9'), antennaGainList.push('36.61','40.21','41.21','45.80');break - case 28: antennaList.push('ANDREW VHLPX1-28', 'ANDREW VHLPX2-28'), antennaDiameterList.push('0.3','0.6'), antennaGainList.push('38.11','42.21');break - case 38: antennaList.push('ANDREW VHLPX1-38', 'ANDREW VHLPX2-38'), antennaDiameterList.push('0.3','0.6'), antennaGainList.push('40.11','45.21');break - case 42: antennaList.push('ANDREW VHLPX1-42-XXX/D', 'ANDREW VHLPX2-42-XXX/A'), antennaDiameterList.push('0.3','0.6'), antennaGainList.push('40.80','46.00');break - case 80: antennaList.push('Radio Waves HPCPE-80', 'Radio Waves HPLP2-80'), antennaDiameterList.push('0.3','0.6'), antennaGainList.push('43.80','50.80');break - } - dispatcher(new UpdateAntennaListAction(antennaList)) - dispatcher(new UpdateAntennaGainAction(antennaGainList)) +export class UpdateSomAction extends Action { + constructor(public somA: number, public somB:number) { + super(); + } } - diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/handlers/linkCalculationAppRootHandler.ts b/sdnr/wt/odlux/apps/linkCalculationApp/src/handlers/linkCalculationAppRootHandler.ts index edfad052a..01512eb92 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/handlers/linkCalculationAppRootHandler.ts +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/handlers/linkCalculationAppRootHandler.ts @@ -21,7 +21,7 @@ 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 { UpdateLinkIdAction, UpdateFrequencyAction , UpdateLatLonAction, UpdateRainAttAction, UpdateRainValAction, updateHideForm, UpdateFslCalculation, UpdateSiteAction, UpdateDistanceAction, isCalculationServerReachableAction, UpdatePolAction, updateAltitudeAction, UpdateAbsorptionLossAction, UpdateWorstMonthRainAction, UpdateEIRPAction, UpdateAntennaAction, UpdateAntennaListAction, UpdateAntennaGainAction, UpdateTxPowerAction, UpdateRxSensitivityAction} from '../actions/commonLinkCalculationActions'; +import { UpdateLinkIdAction, UpdateFrequencyAction , UpdateLatLonAction, UpdateRainAttAction, UpdateRainValAction, updateHideForm, UpdateFslCalculation, UpdateSiteAction, UpdateDistanceAction, isCalculationServerReachableAction, UpdatePolAction, updateAltitudeAction, UpdateAbsorptionLossAction, UpdateWorstMonthRainAction, UpdateEIRPAction, UpdateAntennaGainAction, UpdateTxPowerAction, UpdateRxSensitivityAction, updateAntennaNameAction, UpdateWaveguideLossAction, UpdateRxPowerAction, UpdateSomAction} from '../actions/commonLinkCalculationActions'; declare module '../../../../framework/src/store/applicationStore' { interface IApplicationStoreState { @@ -60,15 +60,18 @@ export type ILinkCalculationAppStateState= { eirpB: number, antennaGainA: number, antennaGainB :number, - antennaList:string[], - antennaGainList:string[], - antennaA: string, - antennaB:string, - systemOperatingMargin : number, + antennaNameA: string, + antennaNameB:string, + systemOperatingMarginA : number, + systemOperatingMarginB : number, txPowerA : string, txPowerB: string, rxSensitivityA : string, - rxSensitivityB: string + rxSensitivityB: string, + waveguideLossA : number, + waveguideLossB: number, + rxPowerA :number, + rxPowerB: number } const initialState: ILinkCalculationAppStateState ={ @@ -98,19 +101,20 @@ const initialState: ILinkCalculationAppStateState ={ eirpB: 0, antennaGainA :0, antennaGainB :0, - antennaList:[], - antennaGainList:[], - antennaA: '0', - antennaB:'0', - systemOperatingMargin : 0, + antennaNameA: '', + antennaNameB:'', + systemOperatingMarginA : 0, + systemOperatingMarginB : 0, txPowerA : '0', txPowerB: '0', rxSensitivityA: '0', - rxSensitivityB: '0' + rxSensitivityB: '0', + waveguideLossA : 0, + waveguideLossB: 0, + rxPowerA : 0, + rxPowerB: 0 } - - export const LinkCalculationHandler: IActionHandler<ILinkCalculationAppStateState> = (state=initialState, action) => { if(action instanceof UpdateLinkIdAction){ @@ -156,17 +160,12 @@ export const LinkCalculationHandler: IActionHandler<ILinkCalculationAppStateStat else if (action instanceof UpdateWorstMonthRainAction){ state = Object.assign({}, state, {month:action.month}) } - else if (action instanceof UpdateEIRPAction){ - state = Object.assign({}, state, {eirpA:action.eirpA, eirpB:action.eirpB}) - } + else if (action instanceof UpdateAntennaGainAction){ - state = Object.assign({}, state, {antennaGainList:action.antennaGainList}) - } - else if (action instanceof UpdateAntennaListAction){ - state = Object.assign({}, state, {antennaList:action.antennaList}) + state = Object.assign({}, state, {antennaGainA:action.antennaGainA,antennaGainB:action.antennaGainB}) } - else if (action instanceof UpdateAntennaAction){ - state = Object.assign({}, state, {antennaA:action.antennaA == null ? state.antennaA : action.antennaA , antennaB: action.antennaB == null? state.antennaB : action.antennaB}) + else if (action instanceof updateAntennaNameAction){ + state = Object.assign({}, state, {antennaNameA:action.antennaNameA, antennaNameB: action.antennaNameB}) } else if (action instanceof UpdateTxPowerAction){ state = Object.assign({}, state, {txPowerA:action.txPowerA == null ? state.txPowerA : action.txPowerA , txPowerB: action.txPowerB == null? state.txPowerB : action.txPowerB}) @@ -174,6 +173,19 @@ export const LinkCalculationHandler: IActionHandler<ILinkCalculationAppStateStat else if (action instanceof UpdateRxSensitivityAction){ state = Object.assign({}, state, {rxSensitivityA:action.rxSensitivityA == null ? state.rxSensitivityA : action.rxSensitivityA , rxSensitivityB: action.rxSensitivityB == null? state.rxSensitivityB : action.rxSensitivityB}) } + else if (action instanceof UpdateWaveguideLossAction){ + state = Object.assign({}, state, {waveguideLossA:action.waveguideLossA, waveguideLossB: action.waveguideLossB}) + } + else if (action instanceof UpdateEIRPAction){ + state = Object.assign({}, state, {eirpA:action.eirpA, eirpB:action.eirpB}) + } + else if (action instanceof UpdateRxPowerAction){ + state = Object.assign({}, state, {rxPowerA:action.rxPowerA, rxPowerB:action.rxPowerB}) + } + else if (action instanceof UpdateSomAction){ + state = Object.assign({}, state, {systemOperatingMarginA:action.somA , systemOperatingMarginB :action.somB}) + } + return state } diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/index.html b/sdnr/wt/odlux/apps/linkCalculationApp/src/index.html index 2023ae365..edcbd2514 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/index.html +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/index.html @@ -15,10 +15,11 @@ <script type="text/javascript" src="./config.js"></script> <script> // run the application - require(["app","connectApp", "linkCalculationApp", "networkMapApp"], function (app, connectApp, linkCalculationApp,networkMapApp) { + require(["app","connectApp", "linkCalculationApp", "networkMapApp" , "lineOfSightApp"], function (app, connectApp, linkCalculationApp,networkMapApp, lineOfSightApp) { connectApp.register(); linkCalculationApp.register(); networkMapApp.register(); + lineOfSightApp.register(); app("./app.tsx").runApplication(); }); </script> diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/pluginLinkCalculation.tsx b/sdnr/wt/odlux/apps/linkCalculationApp/src/pluginLinkCalculation.tsx index f86b22a5c..a15bf033d 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/pluginLinkCalculation.tsx +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/pluginLinkCalculation.tsx @@ -24,11 +24,11 @@ import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react- import { faBookOpen } from '@fortawesome/free-solid-svg-icons'; // select app icon import applicationManager from '../../../framework/src/services/applicationManager'; -import LinkCalculation from './views/linkCalculationComponent'; +import LinkCalculation from './views/linkCalculationComponent'; import LinkCalculationAppRootHandler from './handlers/linkCalculationAppRootHandler'; import connect, { Connect, IDispatcher } from '../../../framework/src/flux/connect'; import { IApplicationStoreState } from "../../../framework/src/store/applicationStore"; -import { UpdateLinkIdAction, UpdateLatLonAction, updateHideForm, UpdateSiteAction, UpdateDistanceAction, isCalculationServerReachableAction, updateAltitudeAction } from "./actions/commonLinkCalculationActions"; +import { UpdateLinkIdAction, UpdateLatLonAction, updateHideForm, UpdateSiteAction, UpdateDistanceAction, isCalculationServerReachableAction, updateAltitudeAction, updateAntennaNameAction, UpdateAntennaGainAction, UpdateWaveguideLossAction } from "./actions/commonLinkCalculationActions"; let currentLinkId: string | null = null; @@ -41,23 +41,29 @@ const mapProps = (state: IApplicationStoreState) => ({ const mapDisp = (dispatcher: IDispatcher) => ({ updateLinkId: (mountId: string) => dispatcher.dispatch(new UpdateLinkIdAction(mountId)), - updateSiteName: (siteNameA?:any, siteNameB?:any)=>{ + updateSiteName: (siteNameA?: any, siteNameB?: any) => { dispatcher.dispatch(new UpdateSiteAction(siteNameA, siteNameB)) }, - updateDistance :(distance:number) =>{ + updateDistance: (distance: number) => { dispatcher.dispatch(new UpdateDistanceAction(distance)) }, - updateLatLon : (Lat1:number, Lon1:number, Lat2:number, Lon2:number)=> { + updateLatLon: (Lat1: number, Lon1: number, Lat2: number, Lon2: number) => { dispatcher.dispatch(new UpdateLatLonAction(Lat1, Lon1, Lat2, Lon2)) - dispatcher.dispatch(new updateHideForm (true)) + dispatcher.dispatch(new updateHideForm(true)) }, - updateAltitude : (amslA:number, aglA:number, amslB:number, aglB:number) => { - dispatcher.dispatch(new updateAltitudeAction(amslA,aglA,amslB,aglB)) + updateAltitude: (amslA: number, aglA: number, amslB: number, aglB: number) => { + dispatcher.dispatch(new updateAltitudeAction(amslA, aglA, amslB, aglB)) + }, + updateAntennaName: (antennaNameA: string, antennaNameB: string) => { + dispatcher.dispatch(new updateAntennaNameAction(antennaNameA, antennaNameB)) + }, + updateAntennaGainAction: (antennaGainA: number, antennaGainB: number) => { + dispatcher.dispatch(new UpdateAntennaGainAction(antennaGainA, antennaGainB)) + }, + updateWaveguideLossAction: (waveguideLossA: number, waveguideLossB: number) => { + dispatcher.dispatch(new UpdateWaveguideLossAction(waveguideLossA, waveguideLossB)) } - // UpdateConectivity : (reachable:boolean) => { - // dispatcher.dispatch (new isCalculationServerReachableAction (reachable)) - // } }); @@ -70,45 +76,68 @@ const LinkCalculationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComp lastUrl = props.location.pathname; linkId = getLinkId(lastUrl); - const data= props.location.search + const data = props.location.search + - - if (data !== undefined && data.length>0){ + if (data !== undefined && data.length > 0) { - - const lat1 = data.split('&')[0].split('=')[1] - const lon1 = data.split('&')[1].split('=')[1] - const lat2 = data.split('&')[2].split('=')[1] - const lon2 = data.split('&')[3].split('=')[1] - const siteNameA = data.split('&')[4].split('=')[1] - const siteNameB = data.split('&')[5].split('=')[1] + const lat1 = data.split('&')[0].split('=')[1] + const lon1 = data.split('&')[1].split('=')[1] + const lat2 = data.split('&')[2].split('=')[1] + const lon2 = data.split('&')[3].split('=')[1] - const distance = data.split('&')[8].split('=')[1] + const siteNameA = data.split('&')[4].split('=')[1] + const siteNameB = data.split('&')[5].split('=')[1] - const amslA = data.split('&')[9].split('=')[1] - const aglA = data.split('&')[10].split('=')[1] + const distance = data.split('&')[8].split('=')[1] - const amslB = data.split('&')[11].split('=')[1] - const aglB = data.split('&')[12].split('=')[1] + const amslA = data.split('&')[9].split('=')[1] + const aglA = data.split('&')[10].split('=')[1] + const amslB = data.split('&')[11].split('=')[1] + const aglB = data.split('&')[12].split('=')[1] - props.updateSiteName(String(siteNameA), String(siteNameB)) + const antennaNameA = data.split('&')[13].split('=')[1].replace("%20", " ") + const antennaGainA = data.split('&')[14].split('=')[1] + const waveguideLossA = data.split('&')[15].split('=')[1] + const antennaNameB = data.split('&')[16].split('=')[1].replace("%20", " ") + const antennaGainB = data.split('&')[17].split('=')[1] + const waveguideLossB = data.split('&')[18].split('=')[1] - props.updateDistance(Number(distance)) - props.updateLatLon(Number(lat1),Number(lon1),Number(lat2),Number(lon2)) + if (siteNameA !== null && siteNameB !== null) { + props.updateSiteName(String(siteNameA), String(siteNameB)) + } - props.updateAltitude (Number(amslA), Number(aglA), Number(amslB), Number(aglB)) - + if (Number(distance) !== null) { + props.updateDistance(Number(distance)) + } + if (Number(lat1) >= -90 && Number(lat2) >= -90 && Number(lat1) <= 90 && Number(lat2) <= 90 && Number(lon1) >= -180 && Number(lon2) >= -180 && Number(lon1) <= 180 && Number(lon2) <= 180) { + props.updateLatLon(Number(lat1), Number(lon1), Number(lat2), Number(lon2)) + } + if (Number(amslA)> 0 && Number(amslB)> 0) { + if (Number(aglA)>= Number(amslA) && Number(aglB)>= Number(amslB)) { + props.updateAltitude(Number(amslA), Number(aglA), Number(amslB), Number(aglB)) + } + } + if (antennaNameA && antennaNameB.length) { + props.updateAntennaName(String(antennaNameA), String(antennaNameB)) + } + if (Number(antennaGainA) > 0 && Number(antennaGainA) > 0) { + props.updateAntennaGainAction(Number(antennaGainA), Number(antennaGainB)) + } + if(Number(waveguideLossA) !== null, Number(waveguideLossB) !== null){ + props.updateWaveguideLossAction(Number(waveguideLossA),Number(waveguideLossB) ) + } } - + if (currentLinkId !== linkId) { // new element is loaded currentLinkId = linkId; props.updateLinkId(currentLinkId); - } + } }, []); // called when component gets updated @@ -126,7 +155,7 @@ const LinkCalculationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComp const getLinkId = (lastUrl: string) => { let index = lastUrl.lastIndexOf("linkCalculation/"); if (index >= 0) { - linkId = lastUrl.substr(index+16); + linkId = lastUrl.substr(index + 16); } else { linkId = ""; } @@ -134,7 +163,7 @@ const LinkCalculationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComp return linkId; } - + return ( <LinkCalculation /> ); diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx b/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx index 063926269..e3eaa6ba0 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx @@ -24,7 +24,7 @@ import { TextField, Tabs, Tab, Typography, AppBar, Button, Tooltip, Checkbox, Ta import './Style.scss' import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { UpdateFrequencyAction, UpdateLatLonAction, UpdateRainAttAction, UpdateRainValAction, UpdateFslCalculation, isCalculationServerReachableAction, UpdatePolAction, UpdateDistanceAction, updateAltitudeAction, UpdateAbsorptionLossAction, UpdateWorstMonthRainAction, updateAntennaList, UpdateAntennaAction, UpdateRadioAttributesAction, UpdateTxPowerAction, UpdateRxSensitivityAction } from "../actions/commonLinkCalculationActions"; +import { UpdateFrequencyAction, UpdateLatLonAction, UpdateRainAttAction, UpdateRainValAction, UpdateFslCalculation, isCalculationServerReachableAction, UpdatePolAction, UpdateDistanceAction, updateAltitudeAction, UpdateAbsorptionLossAction, UpdateWorstMonthRainAction, UpdateTxPowerAction, UpdateRxSensitivityAction, UpdateEIRPAction, UpdateRxPowerAction, UpdateSomAction } from "../actions/commonLinkCalculationActions"; import { faPlaneArrival, faAlignCenter } from "@fortawesome/free-solid-svg-icons"; import ConnectionInfo from '../components/connectionInfo' import { red } from "@material-ui/core/colors"; @@ -55,15 +55,22 @@ const mapProps = (state: IApplicationStoreState) => ({ absorptionOxygen: state.linkCalculation.calculations.absorptionOxygen, absorptionWater: state.linkCalculation.calculations.absorptionWater, month: state.linkCalculation.calculations.month, - eirpSiteA: state.linkCalculation.calculations.eirpA, - eirpSiteB: state.linkCalculation.calculations.eirpB, + eirpA: state.linkCalculation.calculations.eirpA, + eirpB: state.linkCalculation.calculations.eirpB, antennaGainA: state.linkCalculation.calculations.antennaGainA, antennaGainB: state.linkCalculation.calculations.antennaGainB, - antennaList: state.linkCalculation.calculations.antennaList, - antennaGainList: state.linkCalculation.calculations.antennaGainList, - antennaA: state.linkCalculation.calculations.antennaA, - antennaB: state.linkCalculation.calculations.antennaB, - systemOperatingMargin : state.linkCalculation.calculations.systemOperatingMargin + antennaNameA: state.linkCalculation.calculations.antennaNameA, + antennaNameB: state.linkCalculation.calculations.antennaNameB, + systemOperatingMarginA: state.linkCalculation.calculations.systemOperatingMarginA, + systemOperatingMarginB: state.linkCalculation.calculations.systemOperatingMarginB, + waveguideLossA: state.linkCalculation.calculations.waveguideLossA, + waveguideLossB: state.linkCalculation.calculations.waveguideLossB, + rxPowerA: state.linkCalculation.calculations.rxPowerA, + rxPowerB: state.linkCalculation.calculations.rxPowerB, + txPowerA: state.linkCalculation.calculations.txPowerA, + txPowerB: state.linkCalculation.calculations.txPowerB, + rxSensitivityA: state.linkCalculation.calculations.rxSensitivityA, + rxSensitivityB: state.linkCalculation.calculations.rxSensitivityB }); @@ -74,7 +81,6 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ updateFrequency: (frequency: number) => { dispatcher.dispatch(new UpdateFrequencyAction(frequency)) - dispatcher.dispatch(updateAntennaList(frequency)) }, updateLatLon: (Lat1: number, Lon1: number, Lat2: number, Lon2: number) => { dispatcher.dispatch(new UpdateLatLonAction(Lat1, Lon1, Lat2, Lon2)) @@ -114,16 +120,20 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ UpdateWorstMonthRain: (month: string) => { dispatcher.dispatch(new UpdateWorstMonthRainAction(month)) }, - UpdateAntenas: (antennaA: string | null, antennaB: string | null) => { - dispatcher.dispatch(new UpdateAntennaAction(antennaA, antennaB)) + UpdateEIRP: (eirpA: number, eirpB: number) => { + dispatcher.dispatch(new UpdateEIRPAction(eirpA, eirpB)) }, - UpdateRadioAttributes :(som: number, eirpA: number, eirpB: number)=>{ - dispatcher.dispatch(new UpdateRadioAttributesAction(som,eirpA, eirpB)) + UpdateRxPower: (rxPowerA: number, rxPowerB: number) => { + dispatcher.dispatch(new UpdateRxPowerAction(rxPowerA, rxPowerB)) }, - UpdateTxPower :(txPowerA: string | null, txPowerB: string | null)=>{ + UpdateSom: (somA: number, somB: number) => { + dispatcher.dispatch(new UpdateSomAction(somA, somB)) + }, + + UpdateTxPower: (txPowerA: string | null, txPowerB: string | null) => { dispatcher.dispatch(new UpdateTxPowerAction(txPowerA, txPowerB)) - }, - UpdateRxSensitivity :(rxSensitivityA : string | null, rxSensitivityB : string | null)=>{ + }, + UpdateRxSensitivity: (rxSensitivityA: string | null, rxSensitivityB: string | null) => { dispatcher.dispatch(new UpdateRxSensitivityAction(rxSensitivityA, rxSensitivityB)) } }); @@ -145,6 +155,7 @@ interface initialState { attenuationMethodError: string, worstmonth: boolean, showWM: string, + rainMethodErrorState: string } class LinkCalculation extends React.Component<linkCalculationProps, initialState> { @@ -164,6 +175,7 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState antennaTypeError: '', worstmonth: false, showWM: '', + rainMethodErrorState: '0' }; } @@ -246,10 +258,10 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState } } - linkBudget = (antennaA: string, antennaB: string, transmissionPowerA: number, transmissionPowerB: number) => { - fetch(BASE_URL + '/linkbudget/' + antennaA + '/' + antennaB + '/' + transmissionPowerA + '/' + transmissionPowerB) - .then(res=>res.json()) - .then(result => {this.props.UpdateRadioAttributes(result.systemOperatingMargin, result.eirpA, result.eirpB)}) + linkBudget = (lat1: number, lon1: number, lat2: number, lon2: number, distance: number, frequency: number, absorptionMethod: string, polarization: string, antennaGainA: number, antennaGainB: number, waveguideLossA: number, waveguideLossB: number, transmissionPowerA: number, transmissionPowerB: number, rxSensitivityA: number, rxSensitivityB: number) => { + fetch(BASE_URL + '/linkbudget/' + lat1 + ',' + lon1 + ',' + lat2 + ',' + lon2 + '/' + distance + '/' + frequency + '/' + absorptionMethod + '/' + polarization.toUpperCase() + '/' + antennaGainA + '/' + antennaGainB + '/' + waveguideLossA + '/' + waveguideLossB + '/' + transmissionPowerA + '/' + transmissionPowerB + '/' + rxSensitivityA + '/' + rxSensitivityB) + .then(res => res.json()) + .then(result => { this.props.UpdateEIRP(result.eirpA, result.eirpB); this.props.UpdateRxPower(result.receivedPowerA, result.receivedPowerB); this.props.UpdateSom(result.systemOperatingMarginA, result.systemOperatingMarginA) }) } formValid = () => { @@ -260,7 +272,10 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState this.props.lon2 === 0 ? this.setState({ longitude2Error: 'Enter a number between -180 to 180' }) : null this.props.frequency === 0 ? this.setState({ frequencyError: 'Select a frequency' }) : this.setState({ frequencyError: '' }) - this.state.rainMethodDisplay === null && this.props.rainVal === 0 ? this.setState({ rainMethodError: 'Select the rain method' }) : this.setState({ rainMethodError: '' }) + // this.state.rainMethodDisplay === null && this.props.rainVal === 0 ? this.setState({ rainMethodError: 'Select the rain method' }) : this.setState({ rainMethodError: '' }) + + this.state.rainMethodErrorState === '0' ? this.setState({ rainMethodError: 'Select the rain method' }) : this.setState({ rainMethodError: '' }) + this.state.absorptionMethod === '0' ? this.setState({ attenuationMethodError: 'Select the attenuation method' }) : this.setState({ attenuationMethodError: '' }) console.log(this.state); console.log(this.props.lat1 !== 0 && this.props.lat2 !== 0 && this.props.lon1 !== 0 && this.props.lon2 !== 0 && this.props.frequency !== 0 && this.state.rainMethodError === '' && this.state.attenuationMethodError === ''); @@ -282,21 +297,18 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState this.setState({ showWM: ' ' }) this.props.UpdateWorstMonthRain('') this.AbsorptionAtt(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.props.distance, this.props.frequency, this.state.worstmonth, this.state.absorptionMethod) + this.linkBudget(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.props.distance, this.props.frequency, this.state.absorptionMethod, this.props.polarization!, this.props.antennaGainA, this.props.antennaGainB, this.props.waveguideLossA, this.props.waveguideLossB, Number(this.props.txPowerA), Number(this.props.txPowerB), Number(this.props.rxSensitivityA), Number(this.props.rxSensitivityB)) if (this.state.rainMethodDisplay === true) { this.manualRain(this.props.rainVal, this.props.frequency, this.props.distance, this.props.polarization!); } else { - // this.updateRainValue(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.state.worstmonth) this.rainAttCal(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.props.frequency, this.props.distance, this.props.polarization!, this.state.worstmonth); } } else { this.AbsorptionAtt(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.props.distance, this.props.frequency, this.state.worstmonth, this.state.absorptionMethod) - - // this.updateRainValue(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.state.worstmonth) - this.rainAttCal(this.props.lat1, this.props.lon1, this.props.lat2, this.props.lon2, this.props.frequency, this.props.distance, this.props.polarization!, this.state.worstmonth); } } @@ -432,7 +444,7 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState </div> <div> <div style={{ marginTop: 5 }}>Frequency</div> - <div style={{ marginTop: 5 }}> {<select aria-label="select-frequency-in-ghz" className={this.state.frequencyError.length > 0 ? 'error' : 'input'} onChange={(e) => { this.props.updateFrequency(Number(e.target.value)); e.target.value === '0' ? this.setState({ frequencyError: 'select a frequency' }) : this.setState({ frequencyError: '' }); this.props.UpdateAntenas('0', '0') }}> + <div style={{ marginTop: 5 }}> {<select aria-label="select-frequency-in-ghz" className={this.state.frequencyError.length > 0 ? 'error' : 'input'} onChange={(e) => { this.props.updateFrequency(Number(e.target.value)); e.target.value === '0' ? this.setState({ frequencyError: 'select a frequency' }) : this.setState({ frequencyError: '' }) }}> <option value='0' aria-label="none-value" >Select Freq</option> <option value='7' aria-label="7" >7 GHz</option> @@ -452,7 +464,7 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState </div> <div> <div>Rain Model</div> - <div> {<select aria-label="select-rain-method" className={this.state.rainMethodError.length > 0 ? 'error' : 'input'} onChange={(e) => { e.target.value === 'itu' ? this.setState({ rainMethodDisplay: false }) : this.setState({ rainMethodDisplay: true }); e.target.value === '0' ? this.setState({ rainMethodError: 'select a Rain model' }) : this.setState({ rainMethodError: '' }) }}> + <div> {<select aria-label="select-rain-method" className={this.state.rainMethodError.length > 0 ? 'error' : 'input'} onChange={(e) => {if (e.target.value !== '') { this.setState({ rainMethodErrorState: e.target.value, rainMethodError: '' }) }; e.target.value === 'itu' ? this.setState({ rainMethodDisplay: false }) : this.setState({ rainMethodDisplay: true }) }}> <option value='0' aria-label="none-value" >Select Rain Method</option> <option value='itu' aria-label="itur8377">ITU-R P.837-7</option> <option value='manual' aria-label="manual-entry">Specific Rain</option> @@ -471,7 +483,7 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState </div> <div> <div>Absorption Model</div> - <div> {<select aria-label="select-absorption-method" className={this.state.attenuationMethodError.length > 0 ? 'error' : 'input'} onChange={(e) => { if (e.target.value !== '') { this.setState({ absorptionMethod: e.target.value }); this.setState({ attenuationMethodError: '' }) } }}> + <div> {<select aria-label="select-absorption-method" className={this.state.attenuationMethodError.length > 0 ? 'error' : 'input'} onChange={(e) => { if (e.target.value !== '') { this.setState({ absorptionMethod: e.target.value, attenuationMethodError: '' }) } }}> <option value='0' aria-label="none-value" >Select Absorption Method</option> <option value='ITURP67612' aria-label="iturp67612" >ITU-R P.676-12</option> <option value='ITURP67611' aria-label="iturp67611" >ITU-R P.676-11</option> @@ -489,14 +501,15 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState </div> <div> <div>System Operating Margin</div> - <div aria-label="system-operating-margin">{this.props.systemOperatingMargin} dB</div> + <div aria-label="system-operating-margin">{this.props.systemOperatingMarginA.toFixed(3)} dB</div> + <div aria-label="system-operating-margin">{this.props.systemOperatingMarginB.toFixed(3)} dB</div> </div> <div> - <div>Radio Transmitted Power</div> - <div> {<form><input aria-label="site-a-transmitted-power" type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => {if (e.target.value !== '') this.props.UpdateTxPower(e.target.value,null) }} + <div>Radio Transmitted Power</div> + <div> {<form><input aria-label="site-a-transmitted-power" type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => { if (e.target.value !== '') this.props.UpdateTxPower(e.target.value, null) }} > </input> dBm </form>} </div> - <div> {<form><input aria-label="site-b-transmitted-power" type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => { if (e.target.value !== '') this.props.UpdateTxPower(null,e.target.value) }} + <div> {<form><input aria-label="site-b-transmitted-power" type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => { if (e.target.value !== '') this.props.UpdateTxPower(null, e.target.value) }} > </input> dBm </form>} </div> </div> @@ -509,41 +522,43 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState > </input> dBm </form>} </div> </div> + <div> + <div>Rx power</div> + <div aria-label="site-a-effective-isotropic-radiated-power">{this.props.rxPowerA.toFixed(3)} dBm</div> + <div aria-label="site-b-effective-isotropic-radiated-power">{this.props.rxPowerB.toFixed(3)} dBm</div> + </div> </div> <div className='antennaContainer'> <div> <div></div> <div className='antennaFont'>Antenna Settings</div> </div> + <div> <div>Antenna</div> + <div aria-label="site-a-amsl">{this.props.antennaNameA} </div> + <div aria-label="site-b-amsl">{this.props.antennaNameB}</div> + </div> - <div> {<select aria-label="site-a-select-antenna" value={this.props.antennaA} style={{ width: 160, height: 22, fontSize: 13 }} className={this.state.antennaTypeError.length > 0 ? 'error' : 'input'} onChange={(e) => { if (e.target.value !== '') { this.props.UpdateAntenas(e.target.value, null); this.setState({ antennaTypeError: '' }) } }}> - <option value='0' aria-label="none-value" >Select Antenna</option> - {this.props.antennaList.map(antenna => <option value={antenna}>{antenna}</option>)} - - </select>} <div style={{ fontSize: 12, color: 'red' }}>{this.state.antennaTypeError}</div> - </div> - <div> {<select aria-label="site-b-select-antenna" value={this.props.antennaB} style={{ width: 160, height: 22, fontSize: 13 }} className={this.state.antennaTypeError.length > 0 ? 'error' : 'input'} onChange={(e) => { if (e.target.value !== '') { this.props.UpdateAntenas(null, e.target.value); this.setState({ antennaTypeError: '' }) } }}> - <option value='0' aria-label="none-value" >Select Antenna</option> - {this.props.antennaList.map(antenna => <option value={antenna}>{antenna}</option>)} - </select>} <div style={{ fontSize: 12, color: 'red' }}>{this.state.antennaTypeError}</div> - </div> - </div> <div> <div>EIRP</div> - <div aria-label="site-a-effective-isotropic-radiated-power">{this.props.eirpSiteA} dBm</div> - <div aria-label="site-b-effective-isotropic-radiated-power">{this.props.eirpSiteB} dBm</div> + <div aria-label="site-a-effective-isotropic-radiated-power">{this.props.eirpA.toFixed(3)} dBm</div> + <div aria-label="site-b-effective-isotropic-radiated-power">{this.props.eirpB.toFixed(3)} dBm</div> </div> - + <div> <div>Gain</div> - <div aria-label="site-a-antenna-gain" > {this.props.antennaGainList[this.props.antennaList.indexOf(this.props.antennaA)]} dBi</div> - <div aria-label="site-b-antenna-gain">{this.props.antennaGainList[this.props.antennaList.indexOf(this.props.antennaB)]} dBi</div> - + <div aria-label="site-a-antenna-gain" > {this.props.antennaGainA} dBi</div> + <div aria-label="site-b-antenna-gain">{this.props.antennaGainB} dBi</div> </div> <div> + <div>Waveguide Loss</div> + <div aria-label="site-a-waveguide-loss" > {this.props.waveguideLossA} dB</div> + <div aria-label="site-b-waveguide-loss">{this.props.waveguideLossB} dB</div> + </div> + + <div> <div></div> <div>{<button aria-label="calculate-button" style={{ color: '#222', fontFamily: 'Arial', boxAlign: 'center', display: 'inline-block', insetInlineStart: '20', alignSelf: 'center' }} onClick={(e) => this.buttonHandler()} >Calculate</button>} </div> diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/webpack.config.js b/sdnr/wt/odlux/apps/linkCalculationApp/webpack.config.js index 515c5826b..55d98b4d7 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/webpack.config.js +++ b/sdnr/wt/odlux/apps/linkCalculationApp/webpack.config.js @@ -165,6 +165,20 @@ module.exports = (env) => { ws: true, changeOrigin: true, secure: false + }, + "/terrain": { + target: "http://10.20.11.163:5200", + secure: false, + pathRewrite(pathname) { + return pathname.replace(/^\/terrain/, '') + } + }, + "/terrain/": { + target: "http://10.20.11.163:5200", + secure: false, + pathRewrite(pathname) { + return pathname.replace(/^\/terrain/, '/') + } } } diff --git a/sdnr/wt/odlux/apps/performanceHistoryApp/src/components/ltpSelection.tsx b/sdnr/wt/odlux/apps/performanceHistoryApp/src/components/ltpSelection.tsx index 61b781384..b6c14a9ce 100644 --- a/sdnr/wt/odlux/apps/performanceHistoryApp/src/components/ltpSelection.tsx +++ b/sdnr/wt/odlux/apps/performanceHistoryApp/src/components/ltpSelection.tsx @@ -34,7 +34,7 @@ const useStyles = makeStyles(theme => ({ border: "1px solid #ced4da", fontSize: 16, width: "auto", - padding: "5px 26px 5px 12px", + padding: "5px 5px 5px 5px", transition: theme.transitions.create(["border-color", "box-shadow"]), }, center: { |