Add networkMap 79/111879/1
authorAijana Schumann <aijana.schumann@highstreet-technologies.com>
Mon, 31 Aug 2020 11:24:43 +0000 (13:24 +0200)
committerAijana Schumann <aijana.schumann@highstreet-technologies.com>
Mon, 31 Aug 2020 11:24:43 +0000 (13:24 +0200)
Add NetworkMap to odlux

Issue-ID: CCSDK-2560
Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com>
Change-Id: I204bcace9d12f8a26edfa347ee9b7d292c52f030

51 files changed:
sdnr/wt/odlux/apps/app-feature/pom.xml
sdnr/wt/odlux/apps/app-installer/pom.xml
sdnr/wt/odlux/apps/networkMapApp/.babelrc [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/README.md [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png.d.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png.d.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png.d.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/package.json [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/pom.xml [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/App.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/actions/connectivityAction.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/actions/detailsAction.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/actions/mapActions.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/actions/popupActions.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/config.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/handlers/connectivityReducer.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/handlers/detailsReducer.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/handlers/popupReducer.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/handlers/rootReducer.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/index.html [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/model/Feature.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/model/count.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/model/historyEntry.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/model/link.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/model/site.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/pluginTransport.tsx [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/styles/index.css [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/styles/mapbox-gl.css [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/utils/mapLayers.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/utils/mapUtils.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src/utils/utils.ts [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/src2/test/resources/test.js [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/tsconfig.json [new file with mode: 0644]
sdnr/wt/odlux/apps/networkMapApp/webpack.config.js [new file with mode: 0644]
sdnr/wt/odlux/framework/pom.xml
sdnr/wt/odlux/installer/pom.xml
sdnr/wt/odlux/pom.xml

index 2aaf573..e5575a5 100644 (file)
             <groupId>${project.groupId}</groupId>
             <artifactId>sdnr-wt-odlux-app-configurationApp</artifactId>
             <version>${project.version}</version>
+        </dependency>
+         <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sdnr-wt-odlux-app-networkMapApp</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>sdnr-wt-odlux-app-linkCalculationApp</artifactId>
             <version>${project.version}</version>
-        </dependency>   
+        </dependency>
+
+        
+        
     </dependencies>
 </project>
index 1bf55ed..dc919f0 100755 (executable)
             <artifactId>sdnr-wt-odlux-app-configurationApp</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>sdnr-wt-odlux-app-networkMapApp</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>sdnr-wt-odlux-app-linkCalculationApp</artifactId>
diff --git a/sdnr/wt/odlux/apps/networkMapApp/.babelrc b/sdnr/wt/odlux/apps/networkMapApp/.babelrc
new file mode 100644 (file)
index 0000000..3d8cd12
--- /dev/null
@@ -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/networkMapApp/icons/README.md b/sdnr/wt/odlux/apps/networkMapApp/icons/README.md
new file mode 100644 (file)
index 0000000..b26fbc2
--- /dev/null
@@ -0,0 +1,29 @@
+Copyright of icons is as followes:
+
+<!--
+ * ============LICENSE_START========================================================================
+ * apartment.png - Material Icons
+ * =================================================================================================
+ * Copyright (C) 2020 Google. 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==========================================================================
+ */
+ -->
+datacenter.png and lamp.png 
+
+Taken from MS Word
+
+According to https://support.microsoft.com/en-us/office/insert-icons-in-microsoft-office-e2459f17-3996-4795-996e-b9a13486fa79 (date: October 9th, 2019)
+"These icons are free to use; there's no royalty or copyright."
+
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png b/sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png
new file mode 100644 (file)
index 0000000..d4a1c5e
Binary files /dev/null and b/sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png differ
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png.d.ts b/sdnr/wt/odlux/apps/networkMapApp/icons/apartment.png.d.ts
new file mode 100644 (file)
index 0000000..bf398f5
--- /dev/null
@@ -0,0 +1,2 @@
+declare const apartment: string;
+export default apartment;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png b/sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png
new file mode 100644 (file)
index 0000000..eb2a627
Binary files /dev/null and b/sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png differ
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png.d.ts b/sdnr/wt/odlux/apps/networkMapApp/icons/datacenter.png.d.ts
new file mode 100644 (file)
index 0000000..a58a9f5
--- /dev/null
@@ -0,0 +1,2 @@
+declare const datacenter: string;
+export default datacenter;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png b/sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png
new file mode 100644 (file)
index 0000000..f5ea001
Binary files /dev/null and b/sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png differ
diff --git a/sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png.d.ts b/sdnr/wt/odlux/apps/networkMapApp/icons/lamp.png.d.ts
new file mode 100644 (file)
index 0000000..9634b12
--- /dev/null
@@ -0,0 +1,2 @@
+declare const lamp: string;
+export default lamp;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/package.json b/sdnr/wt/odlux/apps/networkMapApp/package.json
new file mode 100644 (file)
index 0000000..0f05ffb
--- /dev/null
@@ -0,0 +1,44 @@
+{
+  "name": "@odlux/networkmap-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/mapbox-gl": "^1.10.2",
+    "mapbox-gl": "^1.11.0",
+    "object.values": "^1.1.1"
+  },
+  "peerDependencies": {
+    "@types/react": "16.9.19",
+    "@types/react-dom": "16.9.5",
+    "@types/react-router-dom": "4.3.1",
+    "@material-ui/core": "4.9.0",
+    "@material-ui/icons": "4.5.1",
+    "@types/classnames": "2.2.6",
+    "@types/flux": "3.1.8",
+    "@types/jquery": "3.3.10",
+    "jquery": "3.3.1",
+    "react": "16.12.0",
+    "react-dom": "16.12.0",
+    "react-router-dom": "4.3.1"
+    
+  }
+}
diff --git a/sdnr/wt/odlux/apps/networkMapApp/pom.xml b/sdnr/wt/odlux/apps/networkMapApp/pom.xml
new file mode 100644 (file)
index 0000000..285bb70
--- /dev/null
@@ -0,0 +1,176 @@
+<?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>odlparent</artifactId>
+        <version>2.0.1-SNAPSHOT</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>org.onap.ccsdk.features.sdnr.wt</groupId>
+    <artifactId>sdnr-wt-odlux-app-networkMapApp</artifactId>
+    <version>1.0.1-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>
+        <checkstyle.skip>true</checkstyle.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>v10.16.3</nodeVersion>
+                            <yarnVersion>v1.19.0</yarnVersion>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>yarn build</id>
+                        <goals>
+                            <goal>yarn</goal>
+                        </goals>
+                        <configuration>
+                            <arguments>run build</arguments>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Import-Package>org.onap.ccsdk.features.sdnr.wt.odlux.model.*,com.opensymphony.*</Import-Package>
+                        <Private-Package/>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/App.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/App.tsx
new file mode 100644 (file)
index 0000000..6caab51
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+import  Map from './components/map'
+import Details from './components/details/details'
+
+function MainView() {
+  return (
+    <div className="App" style={{display: 'flex', flexDirection:'row', flexGrow:1, height:"100%"}}>
+     <Map />
+     <Details />
+    </div>
+  );
+}
+
+export default MainView;
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/actions/connectivityAction.ts b/sdnr/wt/odlux/apps/networkMapApp/src/actions/connectivityAction.ts
new file mode 100644 (file)
index 0000000..448ae83
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { Action } from "../../../../framework/src/flux/action";
+import { Dispatch } from "../../../../framework/src/flux/store";
+import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
+
+
+export class IsTopologyServerReachableAction extends Action{
+    constructor(public reachable: boolean){
+        super();
+    }
+}
+
+export class IsTileServerReachableAction extends Action{
+    constructor(public reachable: boolean){
+        super();
+    }
+}
+
+export const verifyResponse = (response: Response) =>{
+
+    if(response.ok){
+        return response
+    }else{
+        throw Error(`Connection Error: ${response.status} | ${response.statusText} | ${response.url}`)
+    }
+}
+
+export const handleConnectionError = (error: Error) => (dispatcher: Dispatch, getState: () => IApplicationStoreState)=>{
+    const {network:{connectivity: {isToplogyServerAvailable}}} = getState();
+    if(isToplogyServerAvailable){
+       dispatcher(new IsTopologyServerReachableAction(false))
+    }
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/actions/detailsAction.ts b/sdnr/wt/odlux/apps/networkMapApp/src/actions/detailsAction.ts
new file mode 100644 (file)
index 0000000..8a005bc
--- /dev/null
@@ -0,0 +1,164 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { Action } from '../../../../framework/src/flux/action';
+import { requestRest } from '../../../../framework/src/services/restService';
+
+
+import { site, Device } from "../model/site";
+import { link } from '../model/link';
+import { HistoryEntry } from "../model/historyEntry";
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+import { Dispatch } from '../../../../framework/src/flux/store';
+
+export class SelectSiteAction extends Action {
+  constructor(public site: site){
+    super()
+  }
+}
+
+export class SelectLinkAction extends Action {
+ constructor(public link: link){
+   super();
+ }
+}
+
+export class ClearDetailsAction extends Action{
+  constructor(){
+    super();
+  }
+}
+
+export class AddToHistoryAction extends Action {
+ constructor(public entry: HistoryEntry){
+   super();
+ }
+}
+
+export class ClearHistoryAction extends Action {
+  constructor(){
+    super();
+  }
+}
+
+export class IsBusyCheckingDeviceListAction extends Action{
+  constructor(public isBusy: boolean){
+    super();
+  }
+}
+
+export class FinishedLoadingDeviceListAction extends Action{
+  constructor(public devices: Device[]){
+    super();
+  }
+}
+
+export class ClearLoadedDevicesAction extends Action{
+  constructor(){
+    super();
+  }
+}
+
+let running=false;
+
+export const UpdateDetailsView = (nodeId: string) =>(dispatcher: Dispatch, getState: () => IApplicationStoreState) =>{
+  const {network:{details:{checkedDevices}}} = getState();
+  if(checkedDevices!==null){
+    const index = checkedDevices.findIndex(item=>item.name===nodeId)
+    if(index!==-1)
+     requestRest<any>("/rests/operational/network-topology:network-topology/topology/topology-netconf/node/"+nodeId, { method: "GET" })
+     .then(result =>{
+      if(result!==null){
+        checkedDevices[index].status = result.node[0]["netconf-node-topology:connection-status"];
+
+      }else{
+        checkedDevices[index].status = "Not connected";
+      }
+      dispatcher(new FinishedLoadingDeviceListAction(checkedDevices));
+
+     });
+
+  }
+}
+
+export const CheckDeviceList = (list: Device[]) => async (dispatcher: Dispatch, getState: () => IApplicationStoreState) =>{
+if(running) return;
+running=true;
+  dispatcher(new IsBusyCheckingDeviceListAction(true));
+
+  const promises = list.map((device)=>{
+    if(device.simulatorId){
+      return requestRest<any>("/rests/operational/network-topology:network-topology/topology/topology-netconf/node/"+device.simulatorId, { method: "GET" })
+
+    }else{
+      return requestRest<any>("/rests/operational/network-topology:network-topology/topology/topology-netconf/node/"+device.name, { method: "GET" })
+
+    }
+
+  })
+
+  Promise.all(promises).then((result)=>{
+    running=false;
+    
+
+    result.forEach((res: any, index)=>{
+      console.log("value")
+     console.log(res);
+     if(res !==null && res.node!==null){
+
+      list[index].status = res.node[0]["netconf-node-topology:connection-status"];
+     }else{
+      list[index].status = "Not connected";
+     }
+    });
+
+    dispatcher(new FinishedLoadingDeviceListAction(list));
+    dispatcher(new IsBusyCheckingDeviceListAction(false));
+
+  })
+  .catch(err=>{
+    console.error(err);
+
+  dispatcher(new IsBusyCheckingDeviceListAction(false));
+
+  });
+
+  /*  result.forEach((res: Promise<any>, index)=>{
+      console.log("value")
+     console.log(res);
+     console.log(res.value);
+     if(res.value!==null){
+      list[index].status = res.value.node[0]["netconf-node-topology:connection-status"];
+     }else{
+      list[index].status = "Not connected";
+     }*/
+    
+  
+
+
+
+
+  //get devices
+  //wait on all to finish
+  //update array
+
+
+
+
+
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/actions/mapActions.ts b/sdnr/wt/odlux/apps/networkMapApp/src/actions/mapActions.ts
new file mode 100644 (file)
index 0000000..b8af40b
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { Action } from '../../../../framework/src/flux/action';
+import { Dispatch } from '../../../../framework/src/flux/store';
+
+
+import { link } from "../model/link";
+import { site } from "../model/site";
+import { Feature } from '../model/Feature';
+import { URL_API } from '../config';
+
+
+export class HighlightLinkAction extends Action{
+    constructor(public link: link){
+        super();
+    }
+}
+
+export class HighlightSiteAction extends Action{
+    constructor(public site: site){
+        super();
+    }
+}
+
+export class RemoveHighlightingAction extends Action {
+   constructor(){
+       super();
+   }
+}
+
+export class ZoomToSearchResultAction extends Action{
+    constructor(public lat: number, public lon: number){
+        super();
+    }
+}
+
+export class AddAlarmAction extends Action{
+    constructor(public element: Feature){
+        super();
+    }
+}
+
+export class SetCoordinatesAction extends Action{
+    constructor(public lat: number, public lon: number, public zoom: number){
+        super();
+    }
+}
+
+export class SetStatistics extends Action{
+    constructor(public siteCount: string, public linkCount: string){
+        super();
+    }
+}
+
+export class SetIconSwitchAction extends Action{
+    constructor(public enable:boolean){
+        super();
+    }
+}
+
+export const findSiteToAlarm = (alarmedNodeId: string) => (dispatcher: Dispatch) =>{
+    fetch(URL_API+"/site/geojson/device/"+alarmedNodeId)
+    .then(res => res.json())
+    .then(result=>{
+        dispatcher(new AddAlarmAction(result));
+    });
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/actions/popupActions.ts b/sdnr/wt/odlux/apps/networkMapApp/src/actions/popupActions.ts
new file mode 100644 (file)
index 0000000..ff8d079
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { Action } from '../../../../framework/src/flux/action';
+
+export class SetPopupPositionAction extends Action {
+    constructor(public top: number, public left: number){
+        super()
+    }
+}
+
+export class SelectMultipleLinksAction extends Action {
+    constructor(public ids: string[]) {
+        super();
+    }
+}
+
+
+
+export class SelectMultipleSitesAction extends Action {
+    constructor(public ids: string[]) {
+       super();
+    }
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/connectionInfo.tsx
new file mode 100644 (file)
index 0000000..d1e2d97
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react'
+
+import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
+import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect";
+import { Paper, Typography } from "@material-ui/core";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+
+
+type props = Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+const ConnectionInfo: React.FunctionComponent<props> = (props) => {
+
+    return ((props.isTopoServerReachable === false || props.isTileServerReachable === false )?  <Paper style={{padding:5, position: 'absolute', top: 160, width: 230, left:"40%"}}>
+        <div style={{display: 'flex', flexDirection: 'column'}}>
+        <div style={{'alignSelf': 'center', marginBottom:5}}> <Typography> <FontAwesomeIcon icon={faExclamationTriangle} /> Connection Error</Typography></div>
+        {props.isTileServerReachable === false && <Typography> Tile data can't be loaded.</Typography>}
+        {props.isTopoServerReachable === false && <Typography > Network data can't be loaded.</Typography>}
+        </div>
+    </Paper> : null
+)
+
+}
+
+const mapStateToProps = (state: IApplicationStoreState) => ({
+    isTopoServerReachable: state.network.connectivity.isToplogyServerAvailable,
+    isTileServerReachable: state.network.connectivity.isTileServerAvailable
+
+});
+
+
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({
+
+    //zoomToSearchResult: (lat: number, lon: number) => dispatcher.dispatch(new ZoomToSearchResultAction(lat, lon))
+
+});;
+
+
+export default connect(mapStateToProps,mapDispatchToProps)(ConnectionInfo)
+
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/denseTable.tsx
new file mode 100644 (file)
index 0000000..9846a22
--- /dev/null
@@ -0,0 +1,121 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TableContainer from '@material-ui/core/TableContainer';
+import TableHead from '@material-ui/core/TableHead';
+import TableRow from '@material-ui/core/TableRow';
+import Paper from '@material-ui/core/Paper';
+import { makeStyles, Button, Tooltip } from '@material-ui/core';
+
+type props = { headers: string[], height:number,  navigate?(applicationName: string, path?: string):void, onLinkClick?(id: string): void, data: any[], hover: boolean, onClick?(id: string): void, actions?:boolean  };
+
+
+const styles = makeStyles({
+    container: {
+        overflow:"auto"
+    },
+    button: {
+        margin: 0,
+        padding: "6px 6px",
+        minWidth: 'unset'
+      }
+    
+  });
+  
+
+const DenseTable: React.FunctionComponent<props> = (props) => {
+
+    const classes = styles();
+
+    const handleClick = (event: any, id: string) =>{
+        event.preventDefault();
+        props.onClick !== undefined && props.onClick(id);
+
+    }
+
+    const handleHover = (event: any, id: string) =>{
+        event.preventDefault();
+
+    }
+
+    return (
+        <Paper style={{borderRadius:"0px"}}>
+       <div style={{ height:props.height, overflow:"auto"}}>
+            <Table stickyHeader size="small" aria-label="a dense table" >
+                <TableHead>
+                    <TableRow>
+                        {
+                            props.headers.map((data) => {
+                                return <TableCell>{data}</TableCell>
+                            })
+                        }
+                    </TableRow>
+                </TableHead>
+                <TableBody>
+                    {props.data.map((row, index) => {
+
+                        
+                        var filteredRows = Object.keys(row).filter(function(e) { if(e!=="simulatorId") return row });
+                     
+                        //var filteredRows = Object.keys(row).filter(function(e) { if(e!=="simulatorId") return row[e] });
+                        var values = Object.keys(row).map(function(e) {  if(e!=="simulatorId"){ return row[e];} else return undefined });
+                       
+
+                        return (
+                            <TableRow key={index} hover={props.hover} onMouseOver={e => handleHover(e,row.name)} onClick={ e =>  handleClick(e, row.name)}>
+
+                                {
+                                    values.map((data:any) => {
+                                       
+                                        if(data!== undefined)
+                                        return <TableCell >  {data} </TableCell>
+                                        else
+                                        return null;
+                                    })
+                                }
+                                {
+
+                                    props.actions && <TableCell >  
+<div style={{display:"flex"}}>                                           
+    <Tooltip title="Configure">
+    <Button className={classes.button} disabled={row.status!=="connected"} onClick={(e: any) =>{ e.preventDefault(); e.stopPropagation(); props.navigate && props.navigate("configuration", row.simulatorId ? row.simulatorId : row.name)}}>C</Button>
+    </Tooltip>
+    <Tooltip title="Fault">
+    <Button className={classes.button} onClick={(e: any) =>{ e.preventDefault(); e.stopPropagation(); props.navigate && props.navigate("fault", row.simulatorId ? row.simulatorId : row.name)}}>F</Button>
+    </Tooltip>
+    </div> 
+    </TableCell>
+    
+                                }
+                            </TableRow>)
+                    })
+                    }
+
+                </TableBody>
+            </Table>
+            </div>
+        </Paper>
+    );
+
+}
+
+export default DenseTable;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/details.tsx
new file mode 100644 (file)
index 0000000..a2e51d3
--- /dev/null
@@ -0,0 +1,205 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react'
+
+import connect, { IDispatcher, Connect } from '../../../../../framework/src/flux/connect';
+
+import { site, Device } from '../../model/site';
+import Typography from '@material-ui/core/Typography';
+import { link } from '../../model/link';
+import { Breadcrumbs, Link, Paper } from '@material-ui/core';
+import SiteDetails from './siteDetails';
+import LinkDetails from './linkDetails';
+import { URL_API, URL_BASEPATH } from '../../config';
+import { SelectSiteAction, SelectLinkAction, AddToHistoryAction, ClearHistoryAction, CheckDeviceList, ClearDetailsAction } from '../../actions/detailsAction';
+import { HistoryEntry } from '../../model/historyEntry';
+import { HighlightLinkAction, HighlightSiteAction, RemoveHighlightingAction } from '../../actions/mapActions';
+import { isSite } from '../../utils/utils';
+import { IApplicationStoreState } from '../../../../../framework/src/store/applicationStore';
+import { NavigateToApplication } from '../../../../../framework/src/actions/navigationActions';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+
+
+const Details: React.FunctionComponent<porps> = (props) => {
+
+    const [message, setMessage] = React.useState("No data selected.");
+
+
+    //on mount
+    React.useEffect(() => {
+        const detailsId = getDetailsIdFromUrl();
+        if (detailsId !== null && props.data?.name !== detailsId) {
+            loadDetailsData(detailsId)
+        }
+
+    }, []);
+
+    // if url changed
+    React.useEffect(() => {
+        const detailsId = getDetailsIdFromUrl();
+        console.log(detailsId)
+        if (detailsId !== null && props.data?.name !== detailsId) {
+            loadDetailsData(detailsId)
+        }
+        else if(detailsId===null){
+            setMessage("No data selected.");
+            props.clearDetails();
+            props.undoMapSelection();
+        }
+
+    }, [props.location.pathname]);
+
+    //update url if new element loaded
+    React.useEffect(() => {
+        if (props.data !== null) {
+            const currentUrl = window.location.href;
+            const parts = currentUrl.split(URL_BASEPATH);
+            const detailsPath = parts[1].split("/details/");
+            props.history.replace(`/${URL_BASEPATH}${detailsPath[0]}/details/${props.data.name}`)
+        }
+
+    }, [props.data])
+
+    const onLinkClick = async (id: string) => {
+        const result = await fetch(`${URL_API}/link/${id}`);
+        if(result.ok){
+            const resultAsJson = await result.json();
+            const link = resultAsJson as link;
+            props.selectLink(link);
+            props.addHistory({ id: props.data!.name, data: props.data! });
+            props.highlightLink(link);
+
+        }
+    }
+
+    const backClick = (e: any) => {
+        if (isSite(props.breadcrumbs[0].data)) {
+            props.selectSite(props.breadcrumbs[0].data)
+            props.highlightSite(props.breadcrumbs[0].data);
+
+        } else {
+            props.selectLink(props.breadcrumbs[0].data);
+            props.highlightLink(props.breadcrumbs[0].data);
+
+        }
+
+        props.clearHistory();
+        e.preventDefault();
+    }
+
+    const createDetailPanel = (data: site | link) => {
+
+        if (isSite(data)) {
+            return <SiteDetails navigate={props.navigateToApplication} updatedDevices={props.updatedDevices} loadDevices={props.loadDevices} site={data} onLinkClick={onLinkClick} />
+        } else {
+            return <LinkDetails link={data} />
+        }
+    }
+
+    const getDetailsIdFromUrl = () =>{
+        const currentUrl = window.location.href;
+        const parts = currentUrl.split(URL_BASEPATH);
+        const detailsPath = parts[1].split("/details/")
+        return detailsPath[1] ? detailsPath[1] : null;
+    }
+
+    const loadDetailsData = (id: string) =>{
+
+        fetch(`${URL_API}/link/${id}`)
+                .then(res => {
+                    if (res.ok)
+                        return res.json()
+                    else
+                        return Promise.reject()
+
+                })
+                .then(result => {
+                    props.selectLink(result)
+                    props.highlightLink(result);
+
+                })
+                .catch(error => {
+
+                    fetch(`${URL_API}/site/${id}`)
+                        .then(res => {
+                            if (res.ok)
+                                return res.json()
+                            else return Promise.reject();
+                        })
+                        .then(result => { 
+                            props.selectSite(result); 
+                            props.highlightSite(result);
+                         })
+                        .catch(error =>{
+                            setMessage("No element with name " + id + " found");
+                            props.clearDetails();
+                            props.undoMapSelection();
+                        });
+                })
+    }
+
+
+    return (<div style={{ width: '30%', background: "#bbbdbf", padding: "20px", alignSelf:"stretch" }}>
+        <Paper style={{ height:"100%"}} id="site-details-panel"  >
+            {
+                props.breadcrumbs.length > 0 &&
+                <Breadcrumbs style={{ marginLeft: "15px", marginTop: "5px" }} aria-label="breadcrumb">
+                    <Link color="inherit" href="/" onClick={backClick}>
+                        {props.breadcrumbs[0].id}
+                    </Link>
+                    <Link>
+                        {props.data?.name}
+                    </Link>
+                </Breadcrumbs>
+            }
+            {
+                props.data !== null ?
+                    createDetailPanel(props.data)
+                    : <Typography style={{ marginTop: "5px" }} align="center" variant="body1">{message}</Typography>
+
+            }
+        </Paper>
+    </div>)
+}
+
+type porps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+//select always via details?
+const mapStateToProps = (state: IApplicationStoreState) => ({
+    data: state.network.details?.data,
+    breadcrumbs: state.network.details.history,
+    updatedDevices: state.network.details.checkedDevices
+});
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({
+    selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)),
+    selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)),
+    clearDetails: () => dispatcher.dispatch(new ClearDetailsAction()),
+    addHistory: (newEntry: HistoryEntry) => dispatcher.dispatch(new AddToHistoryAction(newEntry)),
+    clearHistory: () => dispatcher.dispatch(new ClearHistoryAction()),
+    highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)),
+    highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)),
+    loadDevices: async (networkElements: Device[]) => { await dispatcher.dispatch(CheckDeviceList(networkElements)) },
+    navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path, "test3")),
+    undoMapSelection: () => dispatcher.dispatch(new RemoveHighlightingAction())
+
+})
+
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Details));
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx
new file mode 100644 (file)
index 0000000..de1bf6b
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+
+import { link } from '../../model/link';
+import { TextField, Tabs, Tab, Typography, AppBar, Button, Link } from '@material-ui/core';
+import DenseTable from '../denseTable';
+import { LatLonToDMS } from '../../utils/mapUtils';
+
+type panelId = "siteA" | "siteB";
+type props = { link: link };
+
+const LinkDetails: React.FunctionComponent<props> = (props) => {
+
+    const [value, setValue] = React.useState<panelId>("siteA");
+    const [height, setHeight] = React.useState(330);
+
+    const handleResize = () =>{
+        console.log("resize")
+        const el = document.getElementById('site-details-panel')?.getBoundingClientRect();
+        const el2 = document.getElementById('site-tabs')?.getBoundingClientRect();
+
+        if(el && el2){
+            if(props.link.type==="microwave")
+              setHeight(el!.height - el2!.y -30);
+            else
+              setHeight(el!.height - el2!.y +20);
+
+        }
+    }
+
+    //on mount
+    React.useEffect(()=>{
+        handleResize();
+
+        //window.addEventListener("resize", handleResize);
+    },[]);
+
+    React.useEffect(()=>{
+        handleResize();
+    }, [props.link])
+
+    const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => {
+        setValue(newValue);
+    }
+
+    const onCalculateLinkClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) =>{
+       e.preventDefault();
+       const siteA= props.link.locationA;
+       const siteB =props.link.locationB;
+       const nameA = props.link.siteA;
+       const nameB = props.link.siteB;
+       const distance = props.link.length > 0 ? props.link.length : props.link.calculatedLength;
+       const azimuthA = props.link.azimuthA;
+       const azimuthB = props.link.azimuthB;
+       window.open(`/#/linkCalculation?lat1=${siteA.lat}&lon1=${siteA.lon}&lat2=${siteB.lat}&lon2=${siteB.lon}&siteA=${nameA}&siteB=${nameB}&azimuthA=${azimuthA}&azimuthB=${azimuthB}&distance=${distance}`)
+
+    }
+
+    const data = [
+
+   {name:"Site Name", val1: props.link.siteA, val2: props.link.siteB},
+    {name:"Latitude", val1: LatLonToDMS(props.link.locationA.lat), val2: LatLonToDMS(props.link.locationB.lat)},
+    {name:"Longitude", val1: LatLonToDMS(props.link.locationA.lon, true), val2: LatLonToDMS(props.link.locationB.lon, true)},
+    {name:"Azimuth in °", val1: props.link.azimuthA.toFixed(2), val2: props.link.azimuthB.toFixed(2)}
+];
+
+    return (<div style={{ paddingLeft: "15px", paddingRight: "15px", paddingTop: "0px", display: 'flex', flexDirection: 'column' }}>
+        <h2>{props.link.name}</h2>
+        <TextField disabled style={{ marginTop: "5px" }} value="Unkown" label="Operator" />
+        <TextField disabled style={{ marginTop: "5px" }} value={props.link.type} label="Type" />
+        <TextField disabled style={{ marginTop: "5px" }} value={props.link.length.toFixed(2)} label="Distance planned in km" />
+        <TextField disabled style={{ marginTop: "5px" }} value={props.link.calculatedLength.toFixed(2)} label="Distance calculated in km" />
+
+        <AppBar position="static" id="site-tabs" style={{ marginTop: "20px", background: '#2E3B55' }}>
+            <Typography style={{ margin:"5px"}}>SITE DETAILS</Typography>
+        </AppBar>
+        <DenseTable height={height} hover={false} headers={["", "Site A", "Site B"]} data={data} />
+        {
+            props.link.type==="microwave" && <Button style={{marginTop:20}} fullWidth variant="contained" color="primary" onClick={onCalculateLinkClick}>Calculate link</Button>
+        }
+    </div>)
+}
+
+export default LinkDetails;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/siteDetails.tsx
new file mode 100644 (file)
index 0000000..a95666e
--- /dev/null
@@ -0,0 +1,153 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+import { TextField, Tabs, Tab, Typography, AppBar, Button, Tooltip } from '@material-ui/core';
+
+
+import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../../framework/src/components/material-table";
+
+
+import { site, Device } from '../../model/site';
+import DenseTable from '../denseTable';
+import { LatLonToDMS } from '../../utils/mapUtils';
+
+type minLinks = { name: string, azimuth: string}
+
+const FaultAlarmNotificationTable = MaterialTable as MaterialTableCtorType<minLinks>;
+
+
+type panelId="links" | "nodes";
+type props = { site: site, updatedDevices: Device[]|null, navigate(applicationName: string, path?: string):void, onLinkClick(id: string): void, loadDevices(devices:Device[]): void };
+
+const SiteDetails: React.FunctionComponent<props> = (props) => {
+
+    const [value, setValue] = React.useState<panelId>("links");
+    const [height, setHeight] = React.useState(330);
+
+    const handleResize = () =>{
+        //console.log("resize")
+        const el = document.getElementById('site-details-panel')?.getBoundingClientRect();
+        const el2 = document.getElementById('site-tabs')?.getBoundingClientRect();
+
+        if(el && el2){
+            setHeight(el!.height - el2!.y +20);
+        }
+        
+    }
+
+    //on mount
+    React.useEffect(()=>{
+        handleResize();
+
+        window.addEventListener("resize", ()=>{console.log("really got resized.")});
+    },[]);
+
+    // on update
+    React.useEffect(()=>{
+
+        props.loadDevices(props.site.devices);
+        handleResize();
+
+    }, [props.site])
+
+    const onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: panelId) => {
+        setValue(newValue);
+    }
+
+    const linkRows: minLinks[] = props.site.links.map(link=> 
+        { 
+            return {name: link.name, azimuth: link.azimuthB.toFixed(2) }   
+        });
+
+
+
+    return (<div  style={{ padding: '15px', display: "flex", flexDirection:"column", minWidth:0, minHeight:0 }}>
+        <h2 >{props.site.name}</h2>
+        {
+            props.site.operator !== '' && props.site.operator !== null ?
+                <TextField disabled={true} value={props.site.operator} label="Operator" /> :
+                <TextField disabled={true} value="Unkown" label="Operator" style={{ marginTop: "5px" }} />
+        }
+        {
+            props.site.type !== undefined && props.site.type.length > 0 &&
+            <TextField disabled={true} value={props.site.type} label="Type" style={{ marginTop: "5px" }} />
+        }
+        {
+            props.site.address !== undefined && props.site.address.length > 0 &&
+            <TextField disabled={true} value={props.site.address} label="Adress" style={{ marginTop: "5px" }} />
+        }
+        {
+            props.site.heighAGLInMeters !== undefined && props.site.heighAGLInMeters > 0 &&
+            <TextField disabled={true} value={props.site.heighAGLInMeters} label="AMSL in meters" style={{ marginTop: "5px" }} />
+        }
+        {
+            props.site.antennaHeightAGLInMeters !== undefined && props.site.antennaHeightAGLInMeters > 0 &&
+            <TextField disabled={true} value={props.site.antennaHeightAGLInMeters} label="Atenna above ground in meters" style={{ marginTop: "5px" }} />
+        }
+         
+        <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lat)} label="Latitude" />
+        <TextField style={{ marginTop: "5px" }} disabled={true} value={LatLonToDMS(props.site.geoLocation.lon, true)} label="Longitude" />
+
+        <AppBar position="static" style={{ marginTop: "5px", background: '#2E3B55' }}>
+            <Tabs id="site-tabs" value={value} onChange={onHandleTabChange} aria-label="simple tabs example">
+                <Tab label="Links" value="links" />
+                <Tab label="Nodes" value="nodes" />
+            </Tabs>
+        </AppBar>
+        {
+            value === "links" &&
+            <>
+                {
+                    props.site.links.length === 0 &&
+                    <Typography variant="body1" style={{ marginTop: '10px' }}>No links available.</Typography>
+                }
+               
+                {
+                    props.site.links.length > 0 &&
+                    <DenseTable height={height} hover={true} headers={["Link Name", "Azimuth in °"]}  data={linkRows} onClick={props.onLinkClick}  ></DenseTable>
+               /**
+                * 
+                * */
+                
+               
+               }
+
+            </>
+
+        }
+        {
+            value === "nodes" &&
+            <>
+                {
+                    props.site.devices.length === 0 &&
+                    <Typography variant="body1" style={{ marginTop: '10px' }}>No nodes available.</Typography>
+                }
+
+                {
+                    props.site.devices.length>0 && props.updatedDevices !== null &&
+                    <DenseTable navigate={props.navigate} height={height} hover={false} headers={["ID","Name","Type", "Manufacturer","Owner","Status", "Ports", "Actions"]} actions={true} data={props.updatedDevices!} />
+                }
+            </>
+        }
+    </div>
+    )
+
+}
+
+export default SiteDetails;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/map.tsx
new file mode 100644 (file)
index 0000000..1aabb92
--- /dev/null
@@ -0,0 +1,606 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react'
+import * as mapboxgl from 'mapbox-gl';
+import InfoIcon from '@material-ui/icons/Info';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+
+
+import { site } from '../model/site';
+import { SelectSiteAction, ClearHistoryAction, SelectLinkAction } from '../actions/detailsAction';
+import { OSM_STYLE, URL_API, URL_BASEPATH, URL_TILE_API } from '../config';
+import { link } from '../model/link';
+import MapPopup from './mapPopup';
+import { SetPopupPositionAction, SelectMultipleLinksAction, SelectMultipleSitesAction } from '../actions/popupActions';
+import { Feature } from '../model/Feature';
+import { HighlightLinkAction, HighlightSiteAction, SetCoordinatesAction, SetStatistics } from '../actions/mapActions';
+import { addDistance, getUniqueFeatures } from '../utils/mapUtils';
+import { location } from '../handlers/mapReducer'
+import { Typography, Paper, Tooltip } from '@material-ui/core';
+import { elementCount } from '../model/count';
+import lamp from '../../icons/lamp.png';
+import apartment from '../../icons/apartment.png';
+import datacenter from '../../icons/datacenter.png';
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect';
+import { verifyResponse, IsTileServerReachableAction, handleConnectionError } from '../actions/connectivityAction';
+import ConnectionInfo from './connectionInfo'
+import { ApplicationStore } from '../../../../framework/src/store/applicationStore';
+import { showIconLayers, addBaseLayers, swapLayersBack } from '../utils/mapLayers';
+
+
+
+
+
+type coordinates = { lat: number, lon: number, zoom: number }
+
+let alarmElements: Feature[] = [];
+let map: mapboxgl.Map;
+let isLoadingInProgress = false;
+let notLoadedBoundingBoxes: mapboxgl.LngLatBounds[] = [];
+
+let lastBoundingBox: mapboxgl.LngLatBounds | null = null;
+let myRef = React.createRef<HTMLDivElement>();
+
+
+class Map extends React.Component<mapProps, { isPopupOpen: boolean }> {
+
+    constructor(props: mapProps) {
+        super(props);
+        //any state stuff
+        this.state = { isPopupOpen: false }
+
+    }
+
+    componentDidMount() {
+
+        window.addEventListener("menu-resized", this.handleResize);
+
+        fetch(URL_TILE_API + '/10/0/0.png')
+            .then(res => {
+                if (res.ok) {
+                    this.setupMap();
+                } else {
+                    this.props.setTileServerLoaded(false);
+                    console.error("tileserver " + URL_TILE_API + "can't be reached.")
+                }
+            })
+            .catch(err => {
+                this.props.setTileServerLoaded(false);
+                console.error("tileserver " + URL_TILE_API + "can't be reached.")
+            });
+
+        fetch(URL_API + "/info")
+            .then(result => verifyResponse(result))
+            .catch(error => this.props.handleConnectionError(error));
+    }
+
+    setupMap = () => {
+
+        let lat = this.props.lat;
+        let lon = this.props.lon;
+        let zoom = this.props.zoom;
+
+        const coordinates = this.extractCoordinatesFromUrl();
+        // override lat/lon/zoom with coordinates from url, if available
+        if (this.areCoordinatesValid(coordinates)) {
+            lat = coordinates.lat;
+            lon = coordinates.lon;
+            zoom = !Number.isNaN(coordinates.zoom) ? coordinates.zoom : zoom;
+        }
+
+        map = new mapboxgl.Map({
+            container: myRef.current!,
+            style: OSM_STYLE as any,
+            center: [lon, lat],
+            zoom: zoom,
+            accessToken: ''
+        });
+
+        map.on('load', (ev) => {
+
+            addBaseLayers(map, this.props.selectedSite, this.props.selectedLink);
+            map.loadImage(
+                lamp,
+                function (error: any, image: any) {
+                    if (error) throw error;
+                    map.addImage('lamp', image);
+                });
+
+            map.loadImage(
+                datacenter,
+                function (error: any, image: any) {
+                    if (error) throw error;
+                    map.addImage('data-center', image);
+                });
+
+            map.loadImage(
+                apartment,
+                function (error: any, image: any) {
+                    if (error) throw error;
+                    map.addImage('house', image);
+                });
+
+            const boundingBox = map.getBounds();
+
+
+            fetch(`${URL_API}/links/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
+                .then(result => verifyResponse(result))
+                .then(result => result.json())
+                .then(features => {
+                    if (map.getLayer('lines')) {
+                        (map.getSource('lines') as mapboxgl.GeoJSONSource).setData(features);
+                    }
+                })
+                .catch(error => this.props.handleConnectionError(error));
+
+
+            fetch(`${URL_API}/sites/geoJson/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
+                .then(result => verifyResponse(result))
+                .then(result => result.json())
+                .then(features => {
+                    if (map.getLayer('points')) {
+                        (map.getSource('points') as mapboxgl.GeoJSONSource).setData(features);
+                    }
+                })
+                .catch(error => this.props.handleConnectionError(error));;
+
+        });
+
+        map.on('click', (e: any) => {
+
+            if (map.getLayer('points')) { // data is shown as points
+
+                var clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5],
+                [e.point.x + 5, e.point.y + 5]], {
+                    layers: ['lines']
+                }), "id");
+
+                const clickedPoints = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['points'] }), "id");
+                const alarmedSites = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['alarmedPoints'] }), "id");
+
+                if (clickedPoints.length != 0) {
+
+
+                    if (alarmedSites.length > 0) {
+                        alarmedSites.forEach(alarm => {
+                            const index = clickedPoints.findIndex(item => item.properties!.id === alarm.properties!.id);
+                            console.log(index);
+
+                            if (index !== -1) {
+                                clickedPoints[index].properties!.alarmed = true;
+                                clickedPoints[index].properties!.type = "alarmed";
+                            }
+                        });
+                        console.log(clickedPoints);
+                    }
+
+                    this.showSitePopup(clickedPoints, e.point.x, e.point.y);
+                } else if (clickedLines.length != 0) {
+                    this.showLinkPopup(clickedLines, e.point.x, e.point.y);
+                }
+
+
+            } else { // data is shown as icons
+
+                const clickedLamps = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-lamps'] }), "id");
+                const buildings = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-building'] }), "id");
+                const houses = getUniqueFeatures(map.queryRenderedFeatures(e.point, { layers: ['point-data-center'] }), "id");
+
+                const combinedFeatures = [...clickedLamps, ...buildings, ...houses];
+
+                const clickedLines = getUniqueFeatures(map.queryRenderedFeatures([[e.point.x - 5, e.point.y - 5],
+                [e.point.x + 5, e.point.y + 5]], {
+                    layers: ['lines']
+                }), "id");
+
+                if (combinedFeatures.length > 0)
+                    this.showSitePopup(combinedFeatures, e.point.x, e.point.y);
+                else if (clickedLines.length != 0) {
+                    this.showLinkPopup(clickedLines, e.point.x, e.point.y);
+                }
+            }
+
+        });
+
+        map.on('moveend', () => {
+
+            const mapZoom = Number(map.getZoom().toFixed(2));
+            const lat = Number(map.getCenter().lat.toFixed(4));
+            const lon = Number(map.getCenter().lng.toFixed(4));
+
+
+            if (this.props.lat !== lat || this.props.lon !== lon || this.props.zoom !== mapZoom) {
+                this.props.updateMapPosition(lat, lon, mapZoom)
+            }
+
+            const currentUrl = window.location.href;
+            const parts = currentUrl.split(URL_BASEPATH);
+            const detailsPath = parts[1].split("/details/");
+
+            if (detailsPath[1] !== undefined && detailsPath[1].length > 0) {
+                this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}/details/${detailsPath[1]}`)
+            }
+            else {
+                this.props.history.replace(`/${URL_BASEPATH}/${map.getCenter().lat.toFixed(4)},${map.getCenter().lng.toFixed(4)},${mapZoom.toFixed(2)}`)
+            }
+
+            const boundingBox = map.getBounds();
+
+            showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id);
+
+            fetch(`${URL_API}/info/count/${boundingBox.getWest()},${boundingBox.getSouth()},${boundingBox.getEast()},${boundingBox.getNorth()}`)
+                .then(result => verifyResponse(result))
+                .then(res => res.json())
+                .then(result => {
+                    console.log(result);
+                    if (result.links !== this.props.linkCount || result.sites !== this.props.siteCount) {
+                        this.props.setStatistics(result.links, result.sites);
+                    }
+                })
+                .catch(error => this.props.handleConnectionError(error));;
+        })
+
+        map.on('move', () => {
+            const mapZoom = map.getZoom();
+
+            const boundingBox = map.getBounds();
+
+            this.loadNetworkData(boundingBox);
+            if (mapZoom > 9) {
+
+                if (map.getLayer('points')) {
+                    map.setLayoutProperty('selectedPoints', 'visibility', 'visible');
+                    map.setPaintProperty('points', 'circle-radius', 7);
+                }
+            } else {
+
+                // reduce size of points / lines if zoomed out
+                map.setPaintProperty('points', 'circle-radius', 2);
+                map.setLayoutProperty('selectedPoints', 'visibility', 'none');
+
+                if (mapZoom <= 4) {
+                    map.setPaintProperty('lines', 'line-width', 1);
+                } else {
+                    map.setPaintProperty('lines', 'line-width', 2);
+                }
+            }
+        });
+    }
+
+    componentDidUpdate(prevProps: mapProps, prevState: {}) {
+
+        if (map !== undefined) {
+            if (prevProps.selectedSite?.properties.id !== this.props.selectedSite?.properties.id) {
+
+                if (this.props.selectedSite != null) {
+                    if (map.getSource("selectedLine") !== undefined) {
+                        (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
+                        (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedSite] });
+                    }
+
+
+                    if (map.getLayer('point-lamps') !== undefined) {
+
+                        map.setFilter('point-lamps', ['==', 'type', 'street lamp']);
+                        map.setFilter('point-data-center', ['==', 'type', 'data center']);
+                        map.setFilter('point-building', ['==', 'type', 'high rise building'])
+
+                        if (this.props.selectedSite?.properties.type !== undefined) {
+                            switch (this.props.selectedSite?.properties.type) {
+                                case 'street lamp':
+                                    map.setFilter('point-lamps', ["all", ['==', 'type', 'street lamp'], ['!=', 'id', this.props.selectedSite.properties.id]]);
+                                    break;
+                                case 'data center':
+                                    map.setFilter('point-data-center', ["all", ['==', 'type', 'data center'], ['!=', 'id', this.props.selectedSite.properties.id]]);
+                                    break;
+                                case 'high rise building':
+                                    map.setFilter('point-building', ["all", ['==', 'type', 'high rise building'], ['!=', 'id', this.props.selectedSite.properties.id]])
+
+                                    break;
+                            }
+                        }
+                    }
+
+
+                }
+                else 
+                {
+                    if (map.getSource("selectedPoints") !== undefined)
+                        (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
+
+                }
+            }
+
+            if (prevProps.selectedLink !== this.props.selectedLink) {
+                if (this.props.selectedLink != null) {
+
+                    if (map.getLayer('point-lamps') !== undefined) {
+                        map.setFilter('point-lamps', ['==', 'type', 'street lamp']);
+                        map.setFilter('point-data-center', ['==', 'type', 'data center']);
+                        map.setFilter('point-building', ['==', 'type', 'high rise building']);
+                    }
+
+                    if (map.getSource("selectedLine") !== undefined) {
+                        (map.getSource("selectedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
+                        (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [this.props.selectedLink] });
+                    }
+                }
+                else 
+                {
+                    if (map.getSource("selectedLine") !== undefined)
+                        (map.getSource("selectedLine") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: [] });
+                }
+            }
+
+            if (prevProps.location.pathname !== this.props.location.pathname) {
+                if (map) {
+                    const coordinates = this.extractCoordinatesFromUrl();
+                    this.moveMapToCoordinates(coordinates);
+                }
+            }
+
+            if (prevProps.alarmlement !== this.props.alarmlement) {
+                if (this.props.alarmlement !== null && !alarmElements.includes(this.props.alarmlement)) {
+                    if (map.getSource("alarmedPoints"))
+                        (map.getSource("alarmedPoints") as mapboxgl.GeoJSONSource).setData({ type: "FeatureCollection", features: alarmElements });
+                    alarmElements.push(this.props.alarmlement)
+                }
+            }
+
+            if (prevProps.showIcons !== this.props.showIcons) {
+                if (map && map.getZoom() > 11) {
+                    console.log(this.props.showIcons);
+                    showIconLayers(map, this.props.showIcons, this.props.selectedSite?.properties.id);
+                }
+            }
+
+            if (prevProps.zoomToElement !== this.props.zoomToElement) {
+                if (this.props.zoomToElement !== null) {
+                    const currentZoom = map?.getZoom();
+
+                    map.flyTo({
+                        center: [
+                            this.props.zoomToElement.lon,
+                            this.props.zoomToElement.lat
+                        ], zoom: currentZoom < 10 ? 10 : currentZoom,
+                        essential: true
+                    });
+                }
+            }
+        }
+    }
+
+    handleResize = () => {
+        if (map) {
+            // wait a moment until resizing actually happened
+            window.setTimeout(() => map.resize(), 500);
+        }
+    }
+
+    extractCoordinatesFromUrl = (): coordinates => {
+        const currentUrl = window.location.href;
+        const mainPathParts = currentUrl.split(URL_BASEPATH);
+        const coordinatePathPart = mainPathParts[1].split("/details/"); // split by details if present
+        const allCoordinates = coordinatePathPart[0].replace("/", "");
+        const coordinates = allCoordinates.split(",");
+        return { lat: Number(coordinates[0]), lon: Number(coordinates[1]), zoom: Number(coordinates[2]) }
+    }
+
+    areCoordinatesValid = (coordinates: coordinates) => {
+
+        if ((!Number.isNaN(coordinates.lat)) && (!Number.isNaN(coordinates.lon))) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    moveMapToCoordinates = (coordinates: coordinates) => {
+
+        if (this.areCoordinatesValid(coordinates)) {
+            let zoom = -1;
+
+            if (!Number.isNaN(coordinates.zoom)) {
+                zoom = coordinates.zoom;
+            }
+
+            map.flyTo({
+                center: [
+                    coordinates.lon,
+                    coordinates.lat
+                ], zoom: zoom !== -1 ? zoom : this.props.zoom,
+                essential: true
+            })
+        }
+    }
+
+
+    //TODO: how to handle if too much data gets loaded? (1 mio points...?)
+    // data might have gotten collected, reload if necessary!
+    //always save count, if count and current view count differ -> reload last boundingbox
+    loadNetworkData = async (bbox: mapboxgl.LngLatBounds) => {
+        if (!isLoadingInProgress) { // only load data if loading not in progress
+            isLoadingInProgress = true;
+
+            if (lastBoundingBox == null) {
+                lastBoundingBox = bbox;
+                await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
+                await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
+            } else {
+
+                // new bbox is bigger than old one
+                if (bbox.contains(lastBoundingBox.getNorthEast()) && bbox.contains(lastBoundingBox.getSouthWest()) && lastBoundingBox !== bbox) {  //if new bb is bigger than old one
+
+                    lastBoundingBox = bbox;
+
+                    const distance = map.getCenter().distanceTo(bbox.getNorthEast()); // radius of visible area (center -> corner) (in meters)
+
+                    //calculate new boundingBox
+                    const increasedBoundingBox = addDistance(bbox.getSouth(), bbox.getWest(), bbox.getNorth(), bbox.getEast(), (distance / 1000) / 2)
+
+                    await this.draw('lines', `${URL_API}/links/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`);
+                    await this.draw('points', `${URL_API}/sites/geoJson/${increasedBoundingBox.west},${increasedBoundingBox.south},${increasedBoundingBox.east},${increasedBoundingBox.north}`);
+                    console.log("bbox is bigger");
+
+                } else if (lastBoundingBox.contains(bbox.getNorthEast()) && lastBoundingBox.contains(bbox.getSouthWest())) { // last one contains new one
+                    // bbox is contained in last one, do nothing
+                    isLoadingInProgress = false;
+
+                } else { // bbox is not fully contained in old one, extend 
+
+                    lastBoundingBox.extend(bbox);
+
+                    await this.draw('lines', `${URL_API}/links/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
+                    await this.draw('points', `${URL_API}/sites/geoJson/${lastBoundingBox.getWest()},${lastBoundingBox.getSouth()},${lastBoundingBox.getEast()},${lastBoundingBox.getNorth()}`);
+                }
+
+            }
+
+
+            if (notLoadedBoundingBoxes.length > 0) { // load last not loaded boundingbox
+                this.loadNetworkData(notLoadedBoundingBoxes.pop()!)
+                notLoadedBoundingBoxes = [];
+            }
+
+        } else {
+            notLoadedBoundingBoxes.push(bbox);
+        }
+    }
+
+    showSitePopup = (sites: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => {
+        if (sites.length > 1) {
+            const ids = sites.map(feature => feature.properties!.id);
+
+            this.props.setPopupPosition(top, left);
+            this.props.selectMultipleSites(ids);
+            this.setState({ isPopupOpen: true });
+
+        } else {
+            const id = sites[0].properties!.id;
+
+            fetch(`${URL_API}/site/${id}`)
+                .then(result => verifyResponse(result))
+                .then(res => res.json() as Promise<site>)
+                .then(result => {
+                    this.props.selectSite(result);
+                    this.props.highlightSite(result);
+                    this.props.clearDetailsHistory();
+                })
+                .catch(error => this.props.handleConnectionError(error));;
+        }
+
+    }
+
+
+
+    showLinkPopup = (links: mapboxgl.MapboxGeoJSONFeature[], top: number, left: number) => {
+
+        if (links.length > 1) {
+
+            const ids = links.map(feature => feature.properties!.id as string);
+
+            this.props.setPopupPosition(top, left);
+            this.props.selectMultipleLinks(ids);
+            this.setState({ isPopupOpen: true });
+
+        } else {
+            var id = links[0].properties!.id;
+
+            fetch(`${URL_API}/link/${id}`)
+                .then(result => verifyResponse(result))
+                .then(res => res.json() as Promise<link>)
+                .then(result => {
+                    this.props.selectLink(result);
+                    this.props.highlightLink(result);
+
+                    this.props.clearDetailsHistory();
+                })
+                .catch(error => this.props.handleConnectionError(error));;
+        }
+    }
+
+    draw = async (layer: string, url: string) => {
+
+        fetch(url)
+            .then(result => verifyResponse(result))
+            .then(res => res.json())
+            .then(result => {
+                isLoadingInProgress = false;
+                if (map.getSource(layer)) {
+                    (map.getSource(layer) as mapboxgl.GeoJSONSource).setData(result);
+                }
+            })
+            .catch(error => this.props.handleConnectionError(error));;
+    }
+
+    render() {
+
+        const reachabe = this.props.isTopoServerReachable && this.props.isTileServerReachable;
+
+        return <>
+
+            <div id="map" style={{ width: "70%", position: 'relative' }} ref={myRef} >
+                {
+                    this.state.isPopupOpen &&
+                    <MapPopup onClose={() => { this.setState({ isPopupOpen: false }); }} />
+                }
+                <ConnectionInfo />
+            </div>
+        </>
+    }
+
+}
+
+type mapProps = RouteComponentProps & Connect<typeof mapStateToProps, typeof mapDispatchToProps>;
+
+const mapStateToProps = (state: IApplicationStoreState) => ({
+    selectedLink: state.network.map.selectedLink,
+    selectedSite: state.network.map.selectedSite,
+    zoomToElement: state.network.map.zoomToElement,
+    alarmlement: state.network.map.alarmlement,
+    lat: state.network.map.lat,
+    lon: state.network.map.lon,
+    zoom: state.network.map.zoom,
+    linkCount: state.network.map.statistics.links,
+    siteCount: state.network.map.statistics.sites,
+    isTopoServerReachable: state.network.connectivity.isToplogyServerAvailable,
+    isTileServerReachable: state.network.connectivity.isTileServerAvailable,
+    showIcons: state.network.map.allowIconSwitch
+
+
+
+});
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({
+    selectSite: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)),
+    selectLink: (link: link) => dispatcher.dispatch(new SelectLinkAction(link)),
+    clearDetailsHistory: () => dispatcher.dispatch(new ClearHistoryAction()),
+    selectMultipleLinks: (ids: string[]) => dispatcher.dispatch(new SelectMultipleLinksAction(ids)),
+    selectMultipleSites: (ids: string[]) => dispatcher.dispatch(new SelectMultipleSitesAction(ids)),
+    setPopupPosition: (x: number, y: number) => dispatcher.dispatch(new SetPopupPositionAction(x, y)),
+    highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)),
+    highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)),
+    updateMapPosition: (lat: number, lon: number, zoom: number) => dispatcher.dispatch(new SetCoordinatesAction(lat, lon, zoom)),
+    setStatistics: (linkCount: string, siteCount: string) => dispatcher.dispatch(new SetStatistics(siteCount, linkCount)),
+    setTileServerLoaded: (reachable: boolean) => dispatcher.dispatch(new IsTileServerReachableAction(reachable)),
+    handleConnectionError: (error: Error) => dispatcher.dispatch(handleConnectionError(error))
+})
+
+export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Map));
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/mapPopup.tsx
new file mode 100644 (file)
index 0000000..0400247
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as React from 'react';
+import { Typography, Select, MenuItem, ClickAwayListener, Popper, Paper, FormGroup, Portal, Popover } from '@material-ui/core';
+import { SelectSiteAction, ClearHistoryAction, ClearDetailsAction } from '../actions/detailsAction';
+import { site } from '../model/site';
+import { link } from '../model/link';
+import { URL_API } from '../config';
+import { HighlightLinkAction, HighlightSiteAction } from '../actions/mapActions';
+import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
+import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect';
+import { verifyResponse, handleConnectionError } from '../actions/connectivityAction';
+
+
+
+
+const MapPopup: React.FunctionComponent<props> = (props) => {
+
+    const [value, setValue] = React.useState("");
+
+    const handleChange = (event: any) => {
+        setValue(event.target.value);
+
+        const id = event.target.value;
+
+       
+        fetch(`${URL_API}/${props.type}/${id}`)
+        .then(result => verifyResponse(result))
+        .then(res => res.json())
+        .then(result => {
+            props.clearDetailsHistory();
+            props.selectElement(result);
+            props.type === "link" ?  props.highlightLink(result)  : props.highlightSite(result)
+            props.onClose();
+        })
+        .catch(error => {
+            props.handleConnectionError(error); 
+            props.onClose(); 
+           // props.clearDetails();
+        });
+    };
+
+    return <>
+        <Popover open={true}  anchorEl={undefined} onClose={props.onClose} anchorReference="anchorPosition" anchorPosition={{ top: props.position.left, left: props.position.top }}>
+            <Paper style={{ padding: "15px" }}>
+                <Typography variant="h5">{`Multiple ${props.type.toLowerCase()}s were selected`}</Typography>
+                <Typography variant="body1">Please select one.</Typography>
+                <Select style={{ width: 300 }} onChange={handleChange} value={value} native>
+                    <option value={""} disabled>{props.type} ids</option>
+                    {
+                        props.ids.map(id => <option key={id} value={id}>{id}</option>)
+                    }
+                </Select>
+            </Paper>
+        </Popover>
+    </>
+}
+
+type props = Connect<typeof mapStateToProps, typeof mapDispatchToProps>& { onClose(): void }
+
+const mapStateToProps = (state: IApplicationStoreState) => ({
+    ids: state.network.popup.selectionPendingForIds,
+    type: state.network.popup.pendingDataType,
+    position: state.network.popup.position
+
+});
+
+const mapDispatchToProps = (dispatcher: IDispatcher) => ({ 
+    selectElement: (site: site) => dispatcher.dispatch(new SelectSiteAction(site)),
+    clearDetailsHistory:()=> dispatcher.dispatch(new ClearHistoryAction()),
+    highlightLink: (link: link) => dispatcher.dispatch(new HighlightLinkAction(link)),
+    highlightSite: (site: site) => dispatcher.dispatch(new HighlightSiteAction(site)),
+    handleConnectionError: (error:Error) => dispatcher.dispatch(handleConnectionError(error)),
+    clearDetails: () => dispatcher.dispatch(new ClearDetailsAction()),
+
+});
+
+export default (connect(mapStateToProps, mapDispatchToProps))(MapPopup);
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/config.ts b/sdnr/wt/odlux/apps/networkMapApp/src/config.ts
new file mode 100644 (file)
index 0000000..bdfcd24
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+export const URL_API="/topology"
+export const URL_TILE_API = '/tiles';
+
+
+export const OSM_STYLE = {
+    'version': 8,
+    'sources': {
+        'raster-tiles': {
+            'type': 'raster',
+            'tiles': [
+               URL_TILE_API+'/{z}/{x}/{y}.png'
+
+            ],
+            'tileSize': 256,
+            'attribution':
+                'Data by <a target="_top" rel="noopener" href="http://openstreetmap.org">OpenStreetMap</a>, under <a target="_top" rel="noopener" href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
+        }
+    },
+    'layers': [
+        {
+            'id': 'simple-tiles',
+            'type': 'raster',
+            'source': 'raster-tiles',
+            'minzoom': 0,
+            'maxzoom': 22
+        }
+    ]
+};
+
+export const URL_BASEPATH = "network";
+
+
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/connectivityReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/connectivityReducer.ts
new file mode 100644 (file)
index 0000000..7214705
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { IActionHandler } from "../../../../framework/src/flux/action";
+import { IsTopologyServerReachableAction, IsTileServerReachableAction } from "../actions/connectivityAction";
+
+
+export type connectivityState = {isToplogyServerAvailable: boolean, isTileServerAvailable: boolean };
+
+const initialState: connectivityState = {isToplogyServerAvailable: true, isTileServerAvailable: true};
+
+export const ConnectivityReducer: IActionHandler<connectivityState> =(state=initialState, action)=> {
+
+    if(action instanceof IsTopologyServerReachableAction){
+        state = Object.assign({}, state, { isToplogyServerAvailable: action.reachable });
+    }
+    else if (action instanceof IsTileServerReachableAction){
+        state = Object.assign({}, state, { isTileServerAvailable: action.reachable });
+
+    }
+
+    return state;
+} 
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/detailsReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/detailsReducer.ts
new file mode 100644 (file)
index 0000000..f573009
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { IActionHandler } from '../../../../framework/src/flux/action';
+import { link } from "../model/link";
+import { site, Device } from "../model/site";
+import { HistoryEntry } from "../model/historyEntry";
+import { SelectSiteAction, SelectLinkAction, AddToHistoryAction, ClearHistoryAction, IsBusyCheckingDeviceListAction, FinishedLoadingDeviceListAction, ClearLoadedDevicesAction, ClearDetailsAction } from '../actions/detailsAction';
+
+export type DetailsStoreState={
+    data: site | link | null,
+    history: HistoryEntry[],
+    isBusyCheckingDeviceList: boolean,
+    checkedDevices: Device[] | null
+
+}
+
+const initialState: DetailsStoreState = {
+    data: null,
+    history:[],
+    isBusyCheckingDeviceList: false,
+    checkedDevices: null
+}
+
+export const DetailsReducer:IActionHandler<DetailsStoreState>=(state = initialState, action)=>{
+    
+    if(action instanceof SelectSiteAction){
+        state= Object.assign({}, state, {data: action.site});
+    }
+    else if(action instanceof SelectLinkAction){
+        state =  Object.assign({}, state, {data: action.link});
+    }else if(action instanceof ClearDetailsAction){
+        state =  Object.assign({}, state, {data: null});
+    }
+    else if(action instanceof AddToHistoryAction){
+        state = Object.assign({}, state, {history: [...state.history, action.entry]})
+
+    }else if(action instanceof ClearHistoryAction){
+        state = Object.assign({}, state, {history: []});
+
+    }else if(action instanceof IsBusyCheckingDeviceListAction){
+        state = Object.assign({}, state, {isBusyCheckingDeviceList: action.isBusy});
+    }else if (action instanceof FinishedLoadingDeviceListAction){
+        state = Object.assign({}, state, {checkedDevices: action.devices});
+        
+    }else if(action instanceof ClearLoadedDevicesAction){
+        state = Object.assign({}, state, {checkedDevices: null});
+
+    }
+
+
+    return state;
+
+}
+
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts
new file mode 100644 (file)
index 0000000..a140e9b
--- /dev/null
@@ -0,0 +1,81 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { IActionHandler } from '../../../../framework/src/flux/action';
+import { Feature } from "../model/Feature";
+import { HighlightLinkAction, HighlightSiteAction, ZoomToSearchResultAction, AddAlarmAction, SetCoordinatesAction, SetStatistics, SetIconSwitchAction, RemoveHighlightingAction } from '../actions/mapActions';
+
+export type location = {lat: number, lon:number}
+
+export type mapState = {
+    selectedLink: Feature | null,
+    selectedSite: Feature | null,
+    zoomToElement: location | null,
+    alarmlement: Feature|null,
+    lat: number,
+    lon: number, 
+    zoom: number,
+    statistics:{links: string, sites: string},
+    allowIconSwitch: boolean
+}
+
+const initialState: mapState ={
+    selectedLink: null,
+    selectedSite: null,
+    zoomToElement: null,
+    alarmlement: null,
+    lat: 52,
+    lon: 13,
+    zoom: 10,
+    statistics:{links:"Not counted yet.", sites: "Not counted yet."},
+    allowIconSwitch: true
+}
+
+export const MapReducer: IActionHandler<mapState> = (state=initialState, action: any) => {
+    
+    if(action instanceof HighlightLinkAction){
+      
+        state = Object.assign({}, state, {selectedSite: null, selectedLink:{type: "Feature", geometry:{type:"LineString", coordinates:[[action.link.locationA.lon,action.link.locationA.lat ],[action.link.locationB.lon,action.link.locationB.lat ]]}}})
+
+
+    }
+    else if(action instanceof HighlightSiteAction){
+       
+    state = Object.assign({}, state, {selectedLink: null, selectedSite:{type: "Feature", properties: {id: action.site.name, type:action.site.type}, geometry:{type:"Point", coordinates:[action.site.geoLocation.lon,action.site.geoLocation.lat ]}}})
+
+    }else if (action instanceof ZoomToSearchResultAction){
+        state = Object.assign({}, state, {zoomToElement:{lat: action.lat, lon: action.lon}});
+    }else if (action instanceof AddAlarmAction){
+        state = Object.assign({}, state, {alarmlement:action.element});
+
+    }else if(action instanceof SetCoordinatesAction){
+        state = Object.assign({}, state, {lat:action.lat, lon: action.lon, zoom:action.zoom});
+        
+    }else if(action instanceof SetStatistics){
+        state = Object.assign({}, state, {statistics:{sites: action.siteCount, links: action.linkCount}});
+
+    }else if (action instanceof SetIconSwitchAction){
+        state = Object.assign({}, state, {allowIconSwitch: action.enable});
+
+    }else if(action instanceof RemoveHighlightingAction){
+        state = Object.assign({}, state, {selectedLink: null, selectedSite:null})
+
+    }
+
+    return state;
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/popupReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/popupReducer.ts
new file mode 100644 (file)
index 0000000..dcac9c9
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { IActionHandler } from '../../../../framework/src/flux/action';
+import { SelectMultipleLinksAction, SelectMultipleSitesAction, SetPopupPositionAction } from "../actions/popupActions";
+
+export type popupStoreState = {
+    selectionPendingForIds: string[],
+    pendingDataType: "link"|"site"| "",
+    position: { top: number, left: number }
+};
+
+const initialState: popupStoreState = {
+    selectionPendingForIds: [],
+    pendingDataType: "",
+    position: { top: 0, left: 0 }
+};
+
+export const PopupsReducer: IActionHandler<popupStoreState> = (state = initialState, action) => {
+   
+    if(action instanceof SelectMultipleLinksAction){
+        state = Object.assign({}, state, { selectionPendingForIds: action.ids, pendingDataType: "link", isSelectionNeeded: true });
+
+    }else if(action instanceof SelectMultipleSitesAction){
+        state = Object.assign({}, state, { selectionPendingForIds: action.ids, pendingDataType: "site", isSelectionNeeded: true });
+
+    }else if(action instanceof SetPopupPositionAction){
+        state= Object.assign({}, state, {position:{top:action.top, left: action.left}})
+
+    }
+       
+
+    return state;
+
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/rootReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/rootReducer.ts
new file mode 100644 (file)
index 0000000..4069ed2
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { combineActionHandler } from '../../../../framework/src/flux/middleware';
+
+import { DetailsReducer, DetailsStoreState } from "./detailsReducer";
+import { PopupsReducer, popupStoreState } from "./popupReducer";
+import { MapReducer, mapState } from "./mapReducer";
+import { connectivityState, ConnectivityReducer } from './connectivityReducer';
+
+export interface INetworkAppStoreState{
+    details: DetailsStoreState,
+    popup: popupStoreState,
+    map: mapState,
+    connectivity: connectivityState
+}
+
+declare module '../../../../framework/src/store/applicationStore' {
+    interface IApplicationStoreState {
+      network: INetworkAppStoreState
+    }
+  }
+
+const appHandler = {
+    details: DetailsReducer, 
+    popup: PopupsReducer, 
+    map: MapReducer, 
+    connectivity: ConnectivityReducer};
+
+export const networkmapRootHandler = combineActionHandler<INetworkAppStoreState>(appHandler)
+
+export default networkmapRootHandler;
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/index.html b/sdnr/wt/odlux/apps/networkMapApp/src/index.html
new file mode 100644 (file)
index 0000000..f68e8c1
--- /dev/null
@@ -0,0 +1,30 @@
+<!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>Networkmap App</title>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="text/javascript" src="./require.js"></script>
+  <script type="text/javascript" src="./config.js"></script>
+  <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.css' rel='stylesheet' />
+
+  <script>
+    // run the application
+    require(["app","connectApp","faultApp", "networkMapApp", "configurationApp"], function (app, connectApp, faultApp, networkMapApp, configurationApp) {
+      connectApp.register();
+      //faultApp.register();
+      configurationApp.register();
+      networkMapApp.register();
+      app("./app.tsx").runApplication();
+    });
+  </script>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/model/Feature.ts b/sdnr/wt/odlux/apps/networkMapApp/src/model/Feature.ts
new file mode 100644 (file)
index 0000000..c4f9ad1
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+export type Feature = {type: "Feature", properties: {id: string, type?: string}, geometry: Geometry}
+
+export type Geometry = Point | LineString;
+
+type Point = {type: "Point", coordinates: number[]}
+
+type LineString ={type: "LineString", coordinates: number[][]}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/model/count.ts b/sdnr/wt/odlux/apps/networkMapApp/src/model/count.ts
new file mode 100644 (file)
index 0000000..726e2ff
--- /dev/null
@@ -0,0 +1,19 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+export type elementCount ={sites: string, links: string}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/model/historyEntry.ts b/sdnr/wt/odlux/apps/networkMapApp/src/model/historyEntry.ts
new file mode 100644 (file)
index 0000000..707ff3d
--- /dev/null
@@ -0,0 +1,22 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { site } from "./site";
+import { link } from "./link";
+
+export type HistoryEntry={id: string, data: site|link};
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/model/link.ts b/sdnr/wt/odlux/apps/networkMapApp/src/model/link.ts
new file mode 100644 (file)
index 0000000..a6ef65c
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+export type link = {id: string, 
+    name: string,
+    length: number,
+    calculatedLength: number,
+    type: string,
+     siteA: string,
+     siteB: string,
+    azimuthA: number,
+    azimuthB: number,
+    locationA:{lon: number, lat: number},
+    locationB:{lon: number, lat: number}
+ };
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/model/site.ts b/sdnr/wt/odlux/apps/networkMapApp/src/model/site.ts
new file mode 100644 (file)
index 0000000..79af653
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { link } from "./link";
+
+export type site = {
+    id: string,
+    name: string,
+    address?: string,
+    heighAGLInMeters?: number, //AboveGroundLevel
+    antennaHeightAGLInMeters?: number,
+    type?: string,
+    operator: string,
+    geoLocation:{lon: number, lat: number},
+    devices: Device[],
+    links: link[]
+}
+
+export type Device = {
+    id: string,
+    type: string,
+    name: string,
+    manufacture: string,
+    owner: string,
+    status?: string,
+    port: number[],
+    simulatorId?: string,
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/pluginTransport.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/pluginTransport.tsx
new file mode 100644 (file)
index 0000000..0ff9418
--- /dev/null
@@ -0,0 +1,101 @@
+/**
+* ============LICENSE_START========================================================================
+* ONAP : ccsdk feature sdnr wt odlux
+* =================================================================================================
+* 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==========================================================================
+*/
+// app configuration and main entry point for the app
+
+import * as React from "react";
+import { faMapMarked } from '@fortawesome/free-solid-svg-icons'; // select app icon
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import applicationManager from '../../../framework/src/services/applicationManager';
+
+
+import { networkmapRootHandler } from './handlers/rootReducer';
+import MainView from "./App";
+import { subscribe, IFormatedMessage } from "../../../framework/src/services/notificationService";
+import applicationApi from "../../../framework/src/services/applicationApi";
+import { UpdateDetailsView } from "./actions/detailsAction";
+import { findSiteToAlarm } from "./actions/mapActions";
+import { URL_BASEPATH } from "./config";
+
+const App : React.SFC = (props) => {
+  return <MainView />
+};
+
+export function register() {
+  applicationManager.registerApplication({
+    name: URL_BASEPATH, // used as name of state as well
+    icon: faMapMarked,
+    rootActionHandler: networkmapRootHandler,
+    rootComponent: App,
+    menuEntry: "Network Map"
+  });
+}
+
+type ObjectNotification = {
+  counter: string;
+  nodeName: string;
+  objectId: string;
+  timeStamp: string;
+}
+
+type FaultAlarmNotification = {
+  id: string;
+  nodeName: string;
+  counter: number;
+  timeStamp: string;
+  objectId: string;
+  problem: string;
+  severity: null | 'Warning' | 'Minor' | 'Major' | 'Critical';
+  type: string;
+  sourceType: string;
+}
+
+// subscribe to the websocket notifications from connect
+subscribe<ObjectNotification & IFormatedMessage>(["ObjectCreationNotification", "ObjectDeletionNotification", "AttributeValueChangedNotification"], (msg => {
+  const store = applicationApi.applicationStore;
+  
+ //store && store.dispatch(UpdateDetailsView(msg.nodeName))
+
+}));
+
+
+subscribe<FaultAlarmNotification & IFormatedMessage>("ProblemNotification", (fault => {
+  const store = applicationApi && applicationApi.applicationStore;
+  if (fault && store) {
+    // store.dispatch(findSiteToAlarm(fault.nodeName));
+
+  
+  }
+}));
+
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/styles/index.css b/sdnr/wt/odlux/apps/networkMapApp/src/styles/index.css
new file mode 100644 (file)
index 0000000..ec2585e
--- /dev/null
@@ -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/networkMapApp/src/styles/mapbox-gl.css b/sdnr/wt/odlux/apps/networkMapApp/src/styles/mapbox-gl.css
new file mode 100644 (file)
index 0000000..03c479a
--- /dev/null
@@ -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/networkMapApp/src/utils/mapLayers.ts b/sdnr/wt/odlux/apps/networkMapApp/src/utils/mapLayers.ts
new file mode 100644 (file)
index 0000000..e11d964
--- /dev/null
@@ -0,0 +1,398 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import * as mapboxgl from 'mapbox-gl';
+import { Feature } from 'model/Feature';
+
+
+export const addBaseLayers = (map: mapboxgl.Map, selectedPoint: Feature|null, selectedLine: Feature|null) => {
+       
+    const boundingBox = map.getBounds();
+    map.addSource('lines', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: [] }
+    });
+
+    const features = selectedLine !== null ? [selectedLine] : [];
+
+    map.addSource('selectedLine', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: features }
+    });
+
+    map.addSource('points', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: [] }
+    });
+
+    const selectedPointFeature = selectedPoint !== null ? [selectedPoint] : [];
+
+
+    map.addSource('selectedPoints', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: selectedPointFeature }
+
+    });
+
+    map.addLayer({
+        'id': 'lines',
+        'type': 'line',
+        'source': 'lines',
+        'layout': {
+            'line-join': 'round',
+            'line-cap': 'round'
+        },
+        'paint': {
+            'line-color': '#888',
+            'line-width': 2
+        }
+    });
+
+    map.addLayer({
+        'id': 'selectedLine',
+        'type': 'line',
+        'source': 'selectedLine',
+        'layout': {
+            'line-join': 'round',
+            'line-cap': 'round'
+        },
+        'paint': {
+            'line-color': '#888',
+            'line-width': 4
+        }
+    });
+
+
+
+    map.addLayer({
+        id: 'points',
+        source: 'points',
+        type: 'circle',
+        paint: {
+            'circle-color': '#11b4da',
+            'circle-radius': 7,
+            'circle-stroke-width': 1,
+            'circle-stroke-color': '#fff'
+        }
+    });
+
+    map.addLayer({
+        id: 'selectedPoints',
+        source: 'selectedPoints',
+        type: 'circle',
+        paint: {
+            'circle-color': '#116bda',
+            'circle-radius': 9,
+            'circle-stroke-width': 1,
+            'circle-stroke-color': '#fff'
+        }
+    });
+
+    map.addSource("alarmedPoints", {
+        type: 'geojson',
+        data: {type:"FeatureCollection", features:[]}
+    })
+
+    map.addLayer({
+        id: 'alarmedPoints',
+        source: 'alarmedPoints',
+        type: 'circle',
+        paint: {
+            'circle-color': '#CC0000',
+            'circle-radius': 9,
+            'circle-stroke-width': 1,
+            'circle-stroke-color': '#fff'
+        }
+    });
+}
+
+export const removeBaseLayers = (map: mapboxgl.Map) => {
+
+    map.removeLayer("points");
+    map.removeLayer("lines");
+    map.removeLayer('selectedPoints');
+    map.removeLayer('selectedLine');
+
+    map.removeSource("points");
+    map.removeSource("lines");
+    map.removeSource('selectedPoints');
+    map.removeSource('selectedLine');
+}
+
+let checkedLayers = false;
+
+const createFilter = (type:'street lamp'|'high rise building'|'data center', selectedSiteId?:string) =>{
+
+    return selectedSiteId === undefined ? ['==', 'type', type] : ["all", ['==', 'type', type], ['!=', 'id', selectedSiteId]]
+}
+
+export const showIconLayers = (map: mapboxgl.Map, show: boolean, selectedSiteId?: string) => {
+
+    const zoom = map.getZoom();
+    
+        if(show){
+
+    if (zoom > 11) {
+
+        const bounds = map.getBounds();
+
+        if(map.getLayer('points')!== undefined && map.getLayer('point-lamps')===undefined && !checkedLayers){
+
+        // if sites don't have a type don't change layers to icons
+        const elements = map.queryRenderedFeatures( undefined,{
+                layers: ['points'], filter:['has', 'type']
+            });
+            checkedLayers=true;
+
+            if(elements.length>0 && elements.length<1000){
+
+        if (map.getLayer('point-lamps') === undefined) {
+            map.removeLayer('points');
+
+            map.setLayoutProperty('selectedPoints', 'visibility', 'none');
+
+            map.addLayer({
+                'id': 'point-lamps',
+                'type': 'symbol',
+                'source': 'points',
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'lamp',
+                    'icon-size': 0.1
+
+                },
+                'filter': createFilter("street lamp", selectedSiteId),
+            });
+
+            map.addLayer({
+                'id': 'point-building',
+                'type': 'symbol',
+                'source': 'points',
+                'filter': createFilter("high rise building", selectedSiteId),
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'house',
+                    'icon-size': 0.1
+                }
+            });
+
+            map.addLayer({
+                'id': 'point-data-center',
+                'type': 'symbol',
+                'source': 'points',
+                'filter': createFilter("data center", selectedSiteId),
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'data-center',
+                    'icon-size': 0.1
+                } });
+
+            map.addLayer({
+                id: 'point-remaining',
+                source: 'points',
+                type: 'circle',
+                'filter': ['==', 'type', ''],
+                paint: {
+                    'circle-color': '#11b4da',
+                    'circle-radius': 7,
+                    'circle-stroke-width': 1,
+                    'circle-stroke-color': '#fff'
+                }
+            });
+
+            map.addLayer({
+                'id': 'select-point-lamps',
+                'type': 'symbol',
+                'source': 'selectedPoints',
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'lamp',
+                    'icon-size': 0.15
+
+                },
+                'filter': ['==', 'type', 'street lamp'],
+            });
+
+            map.addLayer({
+                'id': 'select-point-buildings',
+                'type': 'symbol',
+                'source': 'selectedPoints',
+                'filter': ['==', 'type', 'high rise building'],
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'house',
+                    'icon-size': 0.15
+                }
+            });
+
+            map.addLayer({
+                'id': 'select-point-data-center',
+                'type': 'symbol',
+                'source': 'selectedPoints',
+                'filter': ['==', 'type', 'data center'],
+                'layout': {
+                    'icon-allow-overlap': true,
+                    'icon-image': 'data-center',
+                    'icon-size': 0.15
+                }
+            });
+            }
+        }
+        }
+       
+    } else {
+        swapLayersBack(map);
+    }
+}else{
+    swapLayersBack(map);
+
+}
+    
+}
+
+export const swapLayersBack = (map: mapboxgl.Map) =>{
+    checkedLayers=false;
+    if (map.getLayer('points') === undefined) {
+
+        map.setLayoutProperty('selectedPoints', 'visibility', 'visible');
+
+        map.removeLayer('point-building');
+        map.removeLayer('point-lamps');
+        map.removeLayer('point-data-center');
+        map.removeLayer('point-remaining');
+        map.removeLayer('select-point-data-center');
+        map.removeLayer('select-point-buildings');
+        map.removeLayer('select-point-lamps');
+
+
+
+        map.addLayer({
+            id: 'points',
+            source: 'points',
+            type: 'circle',
+            paint: {
+                'circle-color': '#11b4da',
+                'circle-radius': 7,
+                'circle-stroke-width': 1,
+                'circle-stroke-color': '#fff'
+            }
+        });
+
+        map.moveLayer('points', map.getLayer('selectedPoints').id)
+
+
+    }
+}
+
+const addClusterLayers = (map: mapboxgl.Map, data: any) => {
+    map.addSource('clusters', {
+        type: 'geojson',
+        data: data
+    });
+
+    map.addSource('selectedLine', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: [] }
+    });
+
+    map.addSource('selectedPoints', {
+        type: 'geojson',
+        data: { type: "FeatureCollection", features: [] }
+
+    });
+
+    map.addLayer({
+        id: 'clusters',
+        type: 'circle',
+        source: 'clusters',
+        filter: ['has', 'count'],
+        paint: {
+            'circle-color': [
+                'step',
+                ['get', 'count'],
+                '#51bbd6',
+                100,
+                '#f1f075',
+                750,
+                '#f28cb1'
+            ],
+            'circle-radius': [
+                'step',
+                ['get', 'count'],
+                20,
+                100,
+                30,
+                750,
+                40
+            ]
+        }
+    });
+
+
+    map.addLayer({
+        id: 'cluster-count',
+        type: 'symbol',
+        source: 'clusters',
+        filter: ['has', 'count'],
+        layout: {
+            'text-field': '{count}',
+            'text-font': ['Roboto Bold'],
+            'text-size': 12
+        }
+    });
+
+    map.addLayer({
+        'id': 'selectedLine',
+        'type': 'line',
+        'source': 'selectedLine',
+        'layout': {
+            'line-join': 'round',
+            'line-cap': 'round'
+        },
+        'paint': {
+            'line-color': '#888',
+            'line-width': 4
+        }
+    });
+
+    map.addLayer({
+        id: 'unclustered-points',
+        source: 'clusters',
+        filter: ['!', ['has', 'count'],],
+        type: 'circle',
+        paint: {
+            'circle-color': '#11b4da',
+            'circle-radius': 7,
+            'circle-stroke-width': 1,
+            'circle-stroke-color': '#fff'
+        }
+    });
+
+    map.addLayer({
+        id: 'selectedPoints',
+        source: 'selectedPoints',
+        type: 'circle',
+        paint: {
+            'circle-color': '#116bda',
+            'circle-radius': 9,
+            'circle-stroke-width': 1,
+            'circle-stroke-color': '#fff'
+        }
+    });
+
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/utils/mapUtils.ts b/sdnr/wt/odlux/apps/networkMapApp/src/utils/mapUtils.ts
new file mode 100644 (file)
index 0000000..34cdc06
--- /dev/null
@@ -0,0 +1,135 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+const EARTHRADIUSM = 6378137;
+
+type updatedCoordinates = { south: number, west: number, north: number, east: number };
+
+
+
+
+export const addDistance = (south: number, west: number, north: number, east: number, distanceKm: number): updatedCoordinates => {
+
+    const distanceInM = distanceKm * 1000;
+
+    const dLat = distanceInM / EARTHRADIUSM;
+    const dLon = distanceInM / (EARTHRADIUSM * Math.cos(Math.PI * (north + south) / 360));
+
+    const latOffset = dLat * 180 / Math.PI;
+    const lonOffset = dLon * 180 / Math.PI;
+
+    const newEast = checkLongitude(east + lonOffset);
+    const newWest = checkLongitude(west - lonOffset);
+    const newNorth = checkLatitude(north + latOffset);
+    const newSouth = checkLatitude(south - latOffset);
+
+    return { east: newEast, north: newNorth, south: newSouth, west: newWest };
+
+}
+
+
+//taken from https://www.movable-type.co.uk/scripts/latlong.html
+export 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)];
+}
+
+
+export const LatLonToDMS = (value:number, isLon:boolean=false) =>{
+    const absoluteValue = Math.abs(value);
+    const d = Math.floor(absoluteValue);
+    const m = Math.floor((absoluteValue -d)* 60);
+    const s = (absoluteValue - d - m / 60 ) * 3600;
+    const dms=`${d}° ${m}' ${s.toFixed(2)}"`
+
+    const sign = Math.sign(value);
+
+    if(isLon){
+        return (sign === -1 || sign === -0 ) ? dms + " W" : dms + " E";
+    }else{
+        return (sign === -1 || sign === -0 ) ? dms + " S" : dms + " N";
+    } 
+}
+
+// Because features come from tiled vector data, feature geometries may be split
+// or duplicated across tile boundaries and, as a result, features may appear
+// multiple times in query results.
+
+//taken from https://docs.mapbox.com/mapbox-gl-js/example/filter-features-within-map-view/
+
+export const getUniqueFeatures = (array: mapboxgl.MapboxGeoJSONFeature[], comparatorProperty:string) =>{
+    var existingFeatureKeys: any = {};
+    
+    var uniqueFeatures = array.filter(function(el) {
+    if (existingFeatureKeys[el.properties![comparatorProperty]]) {
+    return false;
+    } else {
+    existingFeatureKeys[el.properties![comparatorProperty]] = true;
+    return true;
+    }
+    });
+     
+    return uniqueFeatures;
+    }
+
+const radians_to_degrees = (radians:number) =>{
+    
+    var pi = Math.PI;
+    return radians * (180/pi);
+}
+
+ const degrees_to_radians = (degrees: number) =>
+    {
+    return degrees * (Math.PI/180);
+    }
+
+
+const checkLatitude = (lat: number) => {
+
+    if (lat > 90)
+        return 90;
+    else if (lat < -90)
+        return -90;
+    else
+        return lat;
+
+}
+
+const checkLongitude = (lon: number) => {
+    if (lon > 180)
+        return 180;
+    else if (lon < -180)
+        return -180;
+    else
+        return lon;
+}
+
+
+
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/utils/utils.ts b/sdnr/wt/odlux/apps/networkMapApp/src/utils/utils.ts
new file mode 100644 (file)
index 0000000..20c4e5a
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2020 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
+import { link } from "../model/link";
+import { site } from "../model/site";
+
+
+export function isSite(data: link | site): data is site {
+    return (data as site).geoLocation !== undefined;
+}
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java b/sdnr/wt/odlux/apps/networkMapApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java
new file mode 100644 (file)
index 0000000..43b072c
--- /dev/null
@@ -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/networkMapApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml b/sdnr/wt/odlux/apps/networkMapApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644 (file)
index 0000000..4ede944
--- /dev/null
@@ -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="networkMapApp"/>
+        <property name="index" value="110"/>
+    </bean>
+</blueprint>
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java b/sdnr/wt/odlux/apps/networkMapApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java
new file mode 100644 (file)
index 0000000..c319bb1
--- /dev/null
@@ -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/networkMapApp/src2/test/resources/test.js b/sdnr/wt/odlux/apps/networkMapApp/src2/test/resources/test.js
new file mode 100644 (file)
index 0000000..b47fdc3
--- /dev/null
@@ -0,0 +1,5 @@
+asdac sad 
+as
+d 
+sad
+ sadfa
\ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/networkMapApp/tsconfig.json b/sdnr/wt/odlux/apps/networkMapApp/tsconfig.json
new file mode 100644 (file)
index 0000000..a66b5d8
--- /dev/null
@@ -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/networkMapApp/webpack.config.js b/sdnr/wt/odlux/apps/networkMapApp/webpack.config.js
new file mode 100644 (file)
index 0000000..5fc67e3
--- /dev/null
@@ -0,0 +1,177 @@
+/**
+ * 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: {
+      networkMapApp: ["./pluginTransport.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]'
+          }
+        }]
+      }]
+    },
+
+    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://10.20.6.29:8181",
+          secure: false
+        },   
+        "/oauth2/": {
+          target: "http://10.20.6.29:8181",
+          secure: false
+        },
+        "/database/": {
+          target: "http://10.20.6.29:8181",
+          secure: false
+        },
+        "/restconf/": {
+          target: "http://10.20.6.29:8181",
+          secure: false
+        },
+        "/rests/": {
+          target: "http://10.20.6.29:8181",
+          secure: false
+        },
+        "/topology/": {
+          target: "http://localhost:3001",
+          secure: false
+        },
+        "/help/": {
+          target: "http://10.20.6.29:8181",
+          secure: false
+        },
+        "/websocket": {
+          target: "http://10.20.6.29:8181",
+          ws: true,
+          changeOrigin: true,
+          secure: false
+        }
+      }
+
+    }
+  }];
+}
index a7c81eb..7145b4a 100644 (file)
@@ -46,7 +46,7 @@
     <properties>
         <buildtime>${maven.build.timestamp}</buildtime>
         <distversion>ONAP Frankfurt (Neon, mdsal ${odl.mdsal.version})</distversion>
-        <buildno>62.ad364be(20/08/21)</buildno>
+        <buildno>64.b032889(20/08/31)</buildno>
         <odlux.version>ONAP SDN-R | ONF Wireless for ${distversion} - Build: ${buildtime} ${buildno} ${project.version}</odlux.version>
     </properties>
 
index 5ff2791..7559b25 100644 (file)
                                     <version>${project.version}</version>
                                     <type>jar</type>
                                     <overWrite>false</overWrite>
+                                </artifactItem>
+                                 <!-- networkMapApp-->
+                                <artifactItem>
+                                    <groupId>${project.groupId}</groupId>
+                                    <artifactId>sdnr-wt-odlux-app-networkMapApp</artifactId>
+                                    <version>${project.version}</version>
+                                    <type>jar</type>
+                                    <overWrite>false</overWrite>
                                 </artifactItem>
                                  <!-- linkCalculationApp-->
                                 <artifactItem>
index a822d3d..c4e1615 100644 (file)
@@ -52,6 +52,7 @@
         <module>apps/performanceHistoryApp</module>
         <module>apps/eventLogApp</module>
         <module>apps/configurationApp</module>
+        <module>apps/networkMapApp</module>
         <module>apps/linkCalculationApp</module>
         <module>apps/app-feature</module>
         <module>apps/app-installer</module>