Initial commit

This commit is contained in:
torsten committed 2026-08-13 18:33:10 +02:00
commit b7d90b9afe
16245 files changed
+3396666

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
# Leaflet map for displaying mounting points
## Table of contents
1. [Usage](#usage)
2. [Programming](#programming)
1. [Basics](#basics)
1. [TypeScript](#typescript)
2. [AngularJs](#angularjs)
3. [Leaflet](#leaflet)
2. [The code](#the-code)
_Note: view this in a program that supports markdown_
_(e.g. VS Code or a browser with plugin) and it will look much better_
---
## Usage
### Connection
When opening index.html you will first see the connection menu.
Here you can choose if you want to connect to localhost:2080, enter
a custom ip or load data from the browser cache.
### Main page
The main page consists of two parts: the map on the left side and the selection
menu on the right side.
The map will display the buildings and mouting points received from the previously
entered websocket. Buildings are currently light blue while mounting points are
highlighted with a typical map marker in black. These markers will open a popup
that displays additional info when clicked.
The selection menu on the right side has three sections, namely "Layers", "Asset
types" and finally "Selected mounting points".
- The layer selection is handled via checkboxes where checked means displayed
- The asset type selection happens with a dropdown menu
- Selected mounting points are displayed in a table with tag/designation and id
The table displaying selected mounting points also features a button for each
point that centers the map on it and opens its popup. It can also be searched
using the text input above it (case sensitive, checks if name or id contain given
text).
---
## Programming
### Basics
This program is a kind of messy combination of several libraries/frameworks that
weren't really made to work with each other soI have to explain some things first:
#### TypeScript
TypeScript (TS) is a superset of JavaScript (JS) which means every JS command is also valid TS.
The big difference is that TS is typed (who would have guessed?) andfeatures object
oriented programming. Concepts such as classes and inheritance are
supported. TS is transpiled into JS. This secures compatibility with most browsers.
A little syntax example of TS:
```typescript
var name: type = value;
function name(parameter: type): returnType {
// ...
}
class name extends inheritedClass {
PropertyOne: type;
PropertyTwo: type = value;
constructor(parameter: type) {
this.PropertyOne = parameter;
}
}
```
The command `tsc --build <file.ts>` can be used to transpile "file.ts",
`tsc --watch <file.ts>` however will stay running and always retranspile when
it detects changes in "file.ts".
#### AngularJs
AngularJs is a kind of library for JS/htm which allows you to build dynamic pages
with really little actual code. There has to be some backing code but most of it
will actually happen wihtin the html file. It's kinda hard to explain but here's
an example:
```html
<li>
<ul ng-repeat="x in [1, 2, 3, 4, 5]">
{{x}}
</ul>
</li>
```
This example doesn't really show anything too useful but you can see two of the most
important concepts of AngularJs: directives and templates.
- The directive `ng-repeat` for example will repeat the tag it is in dynamically
depending on its content, in this case 5 times
- The template `{{x}}` will be dynamically replaced by the value of x, in this
case 1 thorugh 5
When viewed in a browser the example will render as this:
> - 1
> - 2
> - 3
> - 4
> - 5
We can make JS variables visible to AngularJs by attaching them to its `$scope`
object.
This was really short but I can't really teach a whole framework within a
readme. There is a nice (German) [tutorial](https://angular.de/artikel/angularjs-tutorial-deutsch/)
though.
There actually is a newer version of Angular explicitly for TS but it's way more
complicated so I stuck with the old JS version. It was a headache to get that to
work but now it does. Don't touch it.
#### Leaflet
Leaflet is a JS library for creating maps. It's rather simple to understand
and has nice [documentation](https://leafletjs.com/reference-1.4.0.html).
Basically you create a div with a special id and then put stuff like markers
into it via JS. Not much I could explain here. Except maybe I can say a sentence
about function chaining. Leaflet smartly uses return types in its functions that
allow you to chain them like crazy. For example:
```typescript
var marker: L.Marker = L.marker(coordinates)
.bindPopup(popupContent)
.addTo(map);
```
`.bindPopup()` and `.addTo()` both still return the marker. This way you can get
a lot of stuff done with a single statement.
In this example you can also see that Leaflet likes to use factory methods instead
of instance contructors (`new`). Feels weird in the beginning but makes sense when
you keep in mind that this is a TS port of a JS library.
### The code
Let me first tell you how to actually run the code:
1. Transpile: Ctrl+Shift+B in VS Code, choose "build"
2. Browserify: `browserify main.js -o scripts/main.js`
3. Open index.html
The browserify step is needed since this program is basically a nodejs app.
Browserify combines all node modules into a single .js file
so we can actually use the file in a html website.
To clear things up:
- ./main.ts is the source file - _develop here_
- ./main.js is the transpiled file - _don't touch it_
- ./scripts/main.js is the final browerified file - _don't even look at it_
When developing it is way easier to have those commands in watch mode so
you can simply refresh the browser to see the changes you made:
- Ctrl+Shift+B in VS Code, choose watch
- `watchify main.js -o scripts/main.js`
These commands will automatically retranspile and rebrowserify main.ts when you
hit save in an editor of your choice (use VS Code though - it's the best).
tsc, browserify and watchify can be installed via npm.
**Also, don't update utm-latlng!**
utm-latlng is a dependency which features conversion of coordinates from
latitude/longtitude to UTM and vice-versa. There's a bug in it that prevents
the correct conversion from UTM back to lat/lon in the current version. There's
a pull request with a one line fix on GitHub but that was half a year ago and
has never been merged so I copied the line and pasted it in the local installation
of the library. Updating it will probably break it. So don't.
_Actually, it's probably best to not update anything, just to be sure._
---
Now let's get to the actual code. I wont explain every little detail since there
are additional comments in the code but I will highlight _some_ things. For writing
I recommend VS Code.
- The only files needed to display the final page in the end are:
- index.html
- style.css
- The scripts folder and its content
- the img forler and its content
- index.html is rather simple. Contains additional comments as well but here's
some more things:
- leaflet.css has to be before leaflet.min.js apparently
- Everything withing `{{}}` is an AngularJs template and will be filled
dynamically with the code inside
- `ng-click` is basically `onclick`
- I wont even talk about style.css
- main.ts. This is the evil one
- Don't question the angular module and controllers. They are basically magic.
- `module Calc` is a little collection of some math stuff:
- `RadToDeg()` and `DegToRad()` are really just one-liner functions to
convert between degrees and radians
- `RotatePoint()` rotates a point around an angle using fancy trigonometry
I snatched of the internet
- class UTM` is just there so a have a uniform way of representing UTM
coords
- `LatLonToUtm()` and `UtmToLatLon()` are just wrappers to simplify the
respective utm-latlng functions
- `class MountingPoint` is a leaflet marker which stores additional info
about the mounting point it represents
- `class Layer` stores the markers and is basically a representation of
floors but floors with the same name are stored in the same layer
- `IsStringEmpty()` returns `true` when the given string is either empty,
`null` or `undefined`
- Any property of `$scope` within the angular controller is visible to
angular templates within index.html
- `$scope.loadData()` gets the data from the entered websocket and calls
the event `webSocketResponse`
- `$scope.$on("webSocketResponse", /* ... */)`:
This handles the `webSocketResponse`-event. Almost the whole program is
contained wihtin its callback. This is so that nothing tries to access
any data that has not been received yet. It is divided into sections
that handle the different types of objects we want to display, namely
buildings, levels, mounting points and assets. It also sets up the leaflet
map.
- Nested `for (let x of y)`-"loops" are godsent. I mostly use them to get two
objects with matching properties and then do stuff with them. For example:
```typescript
for (let m of mountingPoints) {
for (let b of buildings) {
if (m.objectId == b.id) {
// ...
```
- `(layer.LLayer.getLayers() as unknown[]) as MountingPoint[])`
This thing... Let me break it down.
`layer` is an instance of my `Layer`-class. `LLayer` is the leaflet map
layer it encapsulates.
`.getLayers()` returns the array of markers on that layer. The problem
I faced here was that they lost the properties I added in my
`MountingPoint`-class because it returns `L.Marker`s.
More precisely, the values is still there but not accessible since
`L.Marker` doesn't have the properties.
But I can't directly cast from `L.Marker` to `MountingPoint`.
Therefore I do a cheaty cast via `unknown`.
It's confusing as heck but the only way I can easily get access to the
`MountingPoint`-properties.
- The mean conditional statement at `foundBySearch` within
`$scope.MountingPointFilter()` is a short way of saying
`true` if either
- `$scope.mpSearch` (search text input in index.html) is empty
- currently tested mounting point tag/designation or id contain
`$scope.mpSearch`
- `$scope.SelectionChanged()` is called in the end so that I don't have
to manually fix all the default selections
- `$scope.$apply()` will update the `$scope`-object so that it's values
are actually available in the angular templates within index.html.
It would happen automatically eventually but I want to be on the safe side.
Sometimes it is nessecary to call it, sometimes it thorws an error. Just try
it out. The error doesn't seem to matter.
---
I hope this and the comments in the code are enough to understand it.
If not, you can send me a [mail](mailto:justus.hertel@rheinmetall.com).
If you are lucky I might remember how it works :p
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+194
View File
@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html ng-app="main">
<head>
<meta charset="utf-8" />
<!-- Leaflet Stylesheet -->
<link rel="stylesheet" type="text/css" href="scripts/leaflet.css" />
<!-- Wikitude, only in mobile app -->
<script src="architect://architect.js" type="text/javascript"></script>
<!-- Main Script (includes angular, leaflet and utm-latlng) -->
<script src="scripts/main.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body ng-controller="ctrl">
<div id="parentContainer" ng-hide="arMode">
<!-- Contains everything, needed for css -->
<div id="connectionConatiner" ng-hide="connected">
<!-- Contains the connection menu, only shown in the beginning -->
<h1>
Connection
</h1>
<button
ng-click="loadDataFromWS('ws://localhost:2080')"
ng-disabled="disableInput"
style="width: 100%"
>
Connect to localhost
</button>
<br class="bigBr" />
OR
<br class="bigBr" />
<button
ng-click="loadDataFromWS(websocketIp)"
ng-disabled="disableInput"
>
Connect to
</button>
<input
type="text"
ng-model="websocketIp"
placeholder="ws://192.168.133.140:2080"
ng-disabled="disableInput"
/>
<br class="bigBr" />
OR
<br class="bigBr" />
<button
ng-click="loadDataFromCache()"
ng-disabled="disableInput"
style="width: 100%"
>
Load cached data
</button>
<hr />
<label>
{{ status }}
</label>
<progress
ng-show="disableInput"
max="6"
value="0"
id="progressBar"
>
</progress>
</div>
<div id="mapContainer">
<!-- Contains the leaflet map, needed for css -->
<div id="map">
<!-- This is the actual map, see main.ts -->
</div>
</div>
<div id="guiContainer" ng-hide="loading">
<!-- Contains the selection menu,
disabled when the websocket response is being processed -->
<div ng-show="connected">
<h1>
Layers
</h1>
<table>
<tr ng-repeat="layer in layers">
<td>
<input
type="checkbox"
ng-model="layer.Selected"
ng-change="SelectionChanged()"
/>
{{ layer.Name }}
</td>
<!-- Creates table rows with a checkbox for every layer -->
</tr>
</table>
<hr />
<h1>
Asset type
</h1>
<select
ng-options="asset as asset.designation for asset in assets.availableAssets"
ng-model="assets.selectedAsset"
ng-change="SelectionChanged()"
>
</select>
<!-- Creates a dropdown menu with all available asset types -->
<hr />
<h1>
Selected mounting points:
{{
(
mountingPoints
| filter: MountingPointFilter(mountingPoint)
).length
}}
<!-- Displays the amount of mounting points that
fit the selected criteria -->
</h1>
<input
type="text"
ng-model="mpSearch"
placeholder="Search... (case sensitive)"
/>
<!-- The table below will be filtered according to the entered text,
see main.ts -->
<button ng-click="mpSearch = ''">Clear</button>
<!-- This just clears the text input above -->
<br />
<br />
<div id="mp">
<table
ng-show="
(
mountingPoints
| filter: MountingPointFilter(mountingPoint)
).length > 0
"
>
<!-- Enables the table if theres more than 0 selected mounting points -->
<tr>
<th>
Name
</th>
<th>
ID
</th>
<th>
Position
</th>
</tr>
<tr
ng-repeat="mountingPoint in mountingPoints | filter: MountingPointFilter(mountingPoint)"
>
<!-- Creates table rows for each selected mounting point -->
<td>
{{
mountingPoint.tag != ""
? mountingPoint.tag
: mountingPoint.designation
}}
</td>
<!-- Displays mounting point tag if available,
otherwise designation is displayed -->
<td>
{{ mountingPoint.id }}
</td>
<td>
<button ng-click="ShowMp(mountingPoint)">
Show
</button>
<!-- Opens the marker popup of the according mounting point,
and centers the map on it, see main.ts -->
</td>
</tr>
</table>
</div>
<hr />
</div>
</div>
</div>
<div ng-show="arMode" id="arCtrl">
{{ arMountingPoint }}
<br />
<button onclick="window.Functions.BackFromAr()">
Back
</button>
</div>
</body>
</html>
+503
View File
@@ -0,0 +1,503 @@
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// #region Initialization
console.log("\nBegin initialization ##################################################");
var angular_1 = __importDefault(require("angular"));
angular_1.default != undefined
? console.log("\tLoaded AngularJs.")
: console.error("\tFailed to load AngularJs.");
var leaflet_1 = __importDefault(require("leaflet"));
leaflet_1.default != undefined
? console.log("\tLoaded Leaflet.")
: console.error("\tFailed to load Leaflet.");
var utm_latlng_1 = __importDefault(require("utm-latlng"));
utm_latlng_1.default != undefined
? console.log("\tLoaded utm-latlng.")
: console.error("\tFailed to load utm-latlng.");
var isMobile = /Android/i.test(navigator.userAgent); // Check for mobile browser
console.log("\tDetected " + (isMobile ? "mobile browser." : "desktop browser, disabling AR features."));
// AngularJS
var ngApp = angular_1.default.module("main", []);
console.log("\tInitialized AngularJs.");
console.log("Initialization finished ###############################################");
// #endregion
// #region Helpers
var Calc;
(function (Calc) {
// Converts radians to degrees
function RadToDeg(rad) {
return rad * (180 / Math.PI);
}
Calc.RadToDeg = RadToDeg;
// Converts degrees to radians
function DegToRad(deg) {
return (deg * Math.PI) / 180;
}
Calc.DegToRad = DegToRad;
// Rotates a point around 0,0 by an angle in radians
function RotatePoint(point, angle) {
return {
x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
};
}
Calc.RotatePoint = RotatePoint;
// UTM Coordinates
var UTM = /** @class */ (function () {
function UTM(e, n, zn, zl) {
this.Easting = e;
this.Northing = n;
this.ZoneNumber = zn;
this.ZoneLetter = zl;
}
return UTM;
}());
var _precision = 7; // Precision for LatLon to UTM conversion
var _utm = new utm_latlng_1.default("WGS 84"); // Create UTM/LatLng converter for WGS84 projection
// Wrapper for LatLon to UTM conversion
function LatLonToUtm(lat, lon) {
return _utm.convertLatLngToUtm(lat, lon, _precision);
}
Calc.LatLonToUtm = LatLonToUtm;
// Wrapper for UTM to LatLon conversion
function UtmToLatLon(utm) {
var temp = _utm.convertUtmToLatLng(utm.Easting, utm.Northing, utm.ZoneNumber, utm.ZoneLetter);
return { lat: temp.lat, lon: temp.lng };
}
Calc.UtmToLatLon = UtmToLatLon;
})(Calc || (Calc = {}));
// Leaflet marker with additional info to represent a mounting point
var MountingPoint = /** @class */ (function (_super) {
__extends(MountingPoint, _super);
function MountingPoint() {
var _this = _super !== null && _super.apply(this, arguments) || this;
_this.SupportedAssets = [];
_this.ID = -1;
return _this;
}
return MountingPoint;
}(leaflet_1.default.Marker));
// Represents all levels with the same designation,
// Stores the mounting points of those levels
var Layer = /** @class */ (function () {
function Layer(name) {
this.Name = name;
this.Ids = [];
this.Selected = true;
this.LLayer = leaflet_1.default.layerGroup(undefined);
}
return Layer;
}());
// Returns true if str is undefined, empty or null
function IsStringEmpty(str) {
return str == undefined || str == "" || str == null;
}
// jQuery like shortcut to get html elements
function $(query) {
return document.querySelector(query);
}
// Retrieves the angular $scope from outside a controller
function GetScope() {
return angular_1.default.element($("body")).scope();
}
function Status(str) {
GetScope().status = str;
console.log(str);
$("#progressBar").value++;
}
// #endregion
// Main AngularJs controller
ngApp.controller("ctrl", function ($scope) {
var data = {}; // Will hold the websocket response
var t0 = null; // Measures the time it takes to load and parse
$scope.disableInput = false; // True when loadData kicks in, disables the inputs
$scope.connected = false; // True when websocket response was received
$scope.status = undefined; // String that will be shown while connecting
// Writes the websocket response to data when the load button is clicked
/** @function */ $scope.loadDataFromWS = function (ip) {
$scope.disableInput = true;
console.log("Begin loading data ####################################################");
t0 = performance.now(); // Get start time
// Default value for ip if nothing was entered
ip = IsStringEmpty(ip) ? "ws://192.168.133.140:2080" : ip;
// Catch invalid ips
try {
var ws = new WebSocket(ip); // Connect to the websocket
}
catch (e) {
alert("Invalid IP");
return;
}
Status("\tConnecting to " + ip + "...");
// Connect to the web socket
ws.onopen = function () {
console.log("\tConnected.");
ws.send(JSON.stringify({
type: "getEssentials",
uuid: "UUID"
}));
};
// Handles the response of the websocket
ws.onmessage = function (response) {
console.log("\tFetched " + Math.round(new TextEncoder().encode(response.data).length / 1000) + "kb.");
data = JSON.parse(response.data).data; // Parse websocket response to data
window.localStorage.setItem("data", escape(response.data));
$scope.$broadcast("dataLoaded"); // Call response event to kick in the map and selection GUI
};
// Handle bad websocket responses
ws.onclose = function (event) {
if (event.code > 1000) {
t0 = null;
$scope.status = "";
alert("Error when getting websocket response.\n" + event.code);
$scope.disableInput = false;
}
};
};
/** @function */ $scope.loadDataFromCache = function () {
$scope.disableInput = true;
console.log("Begin loading data ####################################################");
t0 = performance.now();
Status("Loading data from cache...");
try {
if (window.localStorage.getItem("data") == null)
throw new Error();
var _data = unescape(window.localStorage.getItem("data"));
data = JSON.parse(_data).data;
console.log("\tLoaded " + Math.round(new TextEncoder().encode(_data).length / 1000) + "kb from cache.");
$scope.$broadcast("dataLoaded");
}
catch (_a) {
$scope.status = "";
$scope.disableInput = false;
t0 = null;
console.log("\tCould not load data from cache.");
alert("No data cached.");
}
};
// Handles websocket response event
$scope.$on("dataLoaded", function () {
var _data = data;
$scope.connected = true; // Switches the connection menu for the mounting point selection
$scope.loading = true; // Disables the GUI while doing stuff
// #region MAP
console.log("\tCreating map.");
// Create the map, set view and image
var map = new leaflet_1.default.Map("map").setView(new leaflet_1.default.LatLng(53.0528064472226, 8.89412999153137), 8);
var imageBounds = new leaflet_1.default.LatLngBounds([
[53.0528064472226, 8.89412999153137],
[53.056520842139, 8.90030980110168]
]);
var mapImage = leaflet_1.default.imageOverlay("img/RDE2.jpg", imageBounds);
map.addLayer(mapImage);
map.fitBounds(imageBounds);
// #endregion
// #region BUILDINGS
// Get buildings
var buildings = _data.objects;
Status("\tParsing " + buildings.length + " buildings...");
// Create GeoJson from buidling footprints
var footprintPolygons = {
type: "FeatureCollection",
features: []
};
for (var _i = 0, buildings_1 = buildings; _i < buildings_1.length; _i++) {
var currentBuilding = buildings_1[_i];
// Check if current builing has footprint data, if not: skip
if (!(currentBuilding.path[0] == undefined)) {
// Holds footprint coordinates of the current building
var footprintCoords = [[]];
// Parse footprint coordinates from current building
for (var _a = 0, _b = currentBuilding.path; _a < _b.length; _a++) {
var pos = _b[_a];
footprintCoords[0].push([
// Convert from radians to degrees and add to the array
Calc.RadToDeg(pos.lon),
Calc.RadToDeg(pos.lat)
]);
}
// Last point has to be the same as the first point so we add it again
footprintCoords[0].push([
Calc.RadToDeg(currentBuilding.path[0].lon),
Calc.RadToDeg(currentBuilding.path[0].lat)
]);
// Create new GeoJson Feature with building footprint
var currentFootprint = {
type: "Feature",
properties: {
name: currentBuilding.designation
},
geometry: {
type: "Polygon",
coordinates: footprintCoords
}
};
// Add GeoJson feature to feature collection
footprintPolygons.features.push(currentFootprint);
}
}
// Draw building footprints to map, add name popups
var footprintGeoJson = leaflet_1.default.geoJSON(footprintPolygons, {
onEachFeature: function (feature, layer) {
layer.bindPopup("<h2>" + feature.properties.name + "</h2>");
}
}).addTo(map);
// #endregion
// #region LEVELS
var levels = _data.levels; // Levels are the actual floors in the building
var layers = []; // Layers are their representation on the map
Status("\tParsing " + levels.length + " levels...");
for (var _c = 0, levels_1 = levels; _c < levels_1.length; _c++) {
var level = levels_1[_c];
// Create a layer for each level
var add = true;
for (var _d = 0, layers_1 = layers; _d < layers_1.length; _d++) {
var layer = layers_1[_d];
// Levels with duplicate names will be on the same layer
if (level.designation === layer.Name) {
add = false;
layer.Ids.push(level.id);
break;
}
}
if (add) {
// Create new Layer if level is not a duplicate
var newLayer = new Layer(level.designation);
newLayer.Ids.push(level.id);
layers.push(newLayer);
}
}
var outdoorLayer = new Layer("outdoor"); // Create a layer for mounting points outside of buildings
outdoorLayer.Ids = [-1];
var footprintLayer = new Layer("buildings"); // Create a layer for the building footprint
footprintLayer.LLayer = footprintGeoJson;
layers.push(outdoorLayer); // Add outdoor and footprint layers
layers.push(footprintLayer);
$scope.layers = layers; // Add layers to the checkbox menu
// #endregion
// #region MOUNTING POINTS
// Get mounting points
var mountingPoints = _data.mountingPoints;
Status("\tParsing " + mountingPoints.length + " mounting points...");
for (var _e = 0, mountingPoints_1 = mountingPoints; _e < mountingPoints_1.length; _e++) {
var m = mountingPoints_1[_e];
for (var _f = 0, buildings_2 = buildings; _f < buildings_2.length; _f++) {
var b = buildings_2[_f];
if (b.id == m.objectId) {
// Current mounting point is in current buidling?
b.utmRefPoint = Calc.LatLonToUtm(Calc.RadToDeg(b.refPoint.lat), Calc.RadToDeg(b.refPoint.lon)); // Converts refPoint of the building to UTM
m.fixedPosition = Calc.RotatePoint({ x: m.position3d.x, y: m.position3d.y }, Calc.DegToRad(b.rotationDegree)); // Rotates coords of the mounting point by rotationDegree of the building
m.absoluteUtm = b.utmRefPoint;
m.absoluteUtm.Easting += m.fixedPosition.x;
m.absoluteUtm.Northing += m.fixedPosition.y;
m.absoluteLatLon = Calc.UtmToLatLon(m.absoluteUtm);
// Add refPoint to the relative position and convert back to lat/lng
var arButtonHtml = isMobile
? "<button\n\t\t\t\t\t\t\t\tonclick=\"window.Functions.DisplayInAr(" + m.id + ")\"\n\t\t\t\t\t\t\t\tid=\"arButton\"\n\t\t\t\t\t\t >\n\t\t\t\t\t\t\t\tShow in AR\n\t\t\t\t\t\t\t</button>"
: "<i>AR not supported on desktop.</i>";
for (var _g = 0, layers_2 = layers; _g < layers_2.length; _g++) {
var l = layers_2[_g];
for (var _h = 0, _j = l.Ids; _h < _j.length; _h++) {
var i = _j[_h];
if (i == m.levelId) {
// Current mouting point is on current level?
var marker = new MountingPoint(
// Create a marker for the mounting point
new leaflet_1.default.LatLng(m.absoluteLatLon.lat, m.absoluteLatLon.lon), {
icon: leaflet_1.default.icon({
iconUrl: "img/marker.png",
iconSize: [30, 30]
})
})
.bindPopup(
// Create a popup with info about the mounting point
"<h2>\n\t\t\t\t\t\t\t\t\t\t" + (IsStringEmpty(m.tag) ? m.designation : m.tag) + "\n\t\t\t\t\t\t\t\t\t\t</h2>\n\t\t\t\t\t\t\t\t\t\t<table>\n\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>ID</td>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>" + m.id + "</td>\n\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>Object ID</td>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>" + m.objectId + "</td>\n\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>Supported Asset Type IDs</td>\n\t\t\t\t\t\t\t\t\t\t\t\t<td>" + m.supportedAssetTypeIds + "</td>\n\t\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t\t\t\t<br />\n\t\t\t\t\t\t\t\t\t\t" + arButtonHtml + "\n\t\t\t\t\t\t\t\t\t\t")
.addTo(map); // Add it to the map
marker.SupportedAssets = // Add info about supported asset types
m.supportedAssetTypeIds;
marker.ID = m.id; // Add mounting point id
l.LLayer.addLayer(marker); // Add marker to corresponding layer
}
}
}
}
}
}
$scope.mountingPoints = mountingPoints;
// #endregion
// #region ASSETS
// Get asset types
var assets = _data.assetTypes;
Status("\tParsing " + assets.length + " assets...");
// Add asset types to the dropdown menu
$scope.assets = {
availableAssets: assets,
selectedAsset: assets[0]
};
// #endregion
// #region ANGULAR FUNCTIONS
// Called by either the layer or asset selection
/** @function */ $scope.SelectionChanged = function () {
var points = 0;
for (var _i = 0, layers_3 = layers; _i < layers_3.length; _i++) {
var layer = layers_3[_i];
if (layer.Selected) {
map.addLayer(layer.LLayer); // Add selected layers
// Hacky type conversion to get the supported asset types from each mounting point on a layer
// If it's stupid but it works it's not stupid
for (var _a = 0, _b = layer.LLayer.getLayers(); _a < _b.length; _a++) {
var m = _b[_a];
try {
for (var _c = 0, _d = m.SupportedAssets; _c < _d.length; _c++) {
var a = _d[_c];
if (a == $scope.assets.selectedAsset.id) {
m.addTo(map); // Add markers for selected asset type
++points;
}
else {
m.removeFrom(map); // Remove all other markers
}
}
}
catch (_e) {
/** Sometimes theres an error here...
* Doesn't seem to interfere with anything though and
* I don't want to see it in the console
*/
}
}
}
else
map.removeLayer(layer.LLayer); // Remove unselected layers
}
console.log("Selected " + points + " mounting point" + (points == 1 ? "" : "s") + ".");
};
// Filter for selected mounting point table and count
/** @function */ $scope.MountingPointFilter = function () {
return function (mp) {
var correctAssetType = false;
for (var _i = 0, _a = mp.supportedAssetTypeIds; _i < _a.length; _i++) {
var i = _a[_i];
if (i == $scope.assets.selectedAsset.id) {
correctAssetType = true;
break;
}
} // Current mounting point supports selected asset type
var correctLayer = false;
for (var _b = 0, layers_4 = layers; _b < layers_4.length; _b++) {
var layer = layers_4[_b];
for (var _c = 0, _d = layer.Ids; _c < _d.length; _c++) {
var i = _d[_c];
if (layer.Selected && i == mp.levelId) {
correctLayer = true;
break;
}
}
} // Current mounting point is on a selected layer
var foundBySearch = IsStringEmpty($scope.mpSearch)
? true // Automatically true if search input empty
: (IsStringEmpty(mp.tag) ? mp.designation : mp.tag).indexOf($scope.mpSearch) >= 0 || mp.id.toString().indexOf($scope.mpSearch) >= 0;
// true if tag, designation or id contain search input
return correctAssetType && correctLayer && foundBySearch;
};
};
// Function for the Show button
/** @function */ $scope.ShowMp = function (mp) {
for (var _i = 0, layers_5 = layers; _i < layers_5.length; _i++) {
var l = layers_5[_i];
// Not this again...
for (var _a = 0, _b = l.LLayer.getLayers(); _a < _b.length; _a++) {
var m = _b[_a];
if (m.ID == mp.id) {
// Find clicked mounting point
m.openPopup(); // Open corresponding popup
map.panTo(m.getLatLng()); // Center map on the popup
break;
}
}
}
};
// #endregion
// Get final time and format it
var time = t0 != null
? "Finished loading data in " + (performance.now() -
t0) + "ms ########################################"
: "Finished loading data #################################################";
console.log(time);
$scope.SelectionChanged(); // Make initial map state fit the initial menu state
$scope.loading = false; // Done loading
$scope.$apply();
});
});
// #region WIKITUDE FUNCTIONS
var currentPOI = undefined; // Holds current wikitude ar marker
// Shows an augmented reality marker vor the selected mounting point
function _displayInAr(id) {
var $scope = GetScope(); // Retrieve angular scope
var mountingPoint;
for (var _i = 0, _a = $scope.mountingPoints; _i < _a.length; _i++) {
var mp = _a[_i];
if (mp.id == id) {
mountingPoint = mp;
break;
}
} // Get correct mounting point
var name = IsStringEmpty(mountingPoint.tag)
? mountingPoint.designation
: mountingPoint.tag;
$scope.arMountingPoint = name;
// Display mounting point tag/designation
currentPOI = new AR.GeoObject([
new AR.GeoLocation(mountingPoint.absoluteLatLon.lat, mountingPoint.absoluteLatLon.lon)
], {
drawables: {
cam: [
new AR.ImageDrawable(new AR.ImageResource("img/armarker.png"), 2.5, { zOrder: 0, opacity: 1 }),
new AR.Label(name, 1, {
zOrder: 1,
style: { textColor: "#ffffff" }
})
],
indicator: new AR.ImageDrawable(new AR.ImageResource("img/arindicator.png"), 0.1, {
enabled: true,
verticalAnchor: AR.CONST.VERTICAL_ANCHOR.TOP
})
}
}); // Create AR marker
$scope.arMode = true; // Hide map and menu
$scope.$apply();
console.log("Switched to AR mode.");
}
// Goes back to the map view
function _backFromAr() {
var $scope = GetScope(); // Retrieve angular scope
$scope.arMountingPoint = ""; // Clear name display
$scope.arMode = false; // Show map and menu again
currentPOI.destroy();
currentPOI = undefined;
$scope.$apply();
console.log("Switched to map mode.");
}
// Attach functions
window.Functions = window.Functions || {
DisplayInAr: _displayInAr,
BackFromAr: _backFromAr
};
// #endregion
// #endregion
+664
View File
@@ -0,0 +1,664 @@
// #region Initialization
console.log(
"\nBegin initialization ##################################################"
);
import angular from "angular";
angular != undefined
? console.log("\tLoaded AngularJs.")
: console.error("\tFailed to load AngularJs.");
import L, { PopupEvent, Popup } from "leaflet";
L != undefined
? console.log("\tLoaded Leaflet.")
: console.error("\tFailed to load Leaflet.");
import UTMLatLng from "utm-latlng";
UTMLatLng != undefined
? console.log("\tLoaded utm-latlng.")
: console.error("\tFailed to load utm-latlng.");
const isMobile: boolean = /Android/i.test(navigator.userAgent); // Check for mobile browser
console.log(
`\tDetected ${
isMobile ? "mobile browser." : "desktop browser, disabling AR features."
}`
);
// import AR from "scripts/architect"
declare var AR: any; // Is loaded in html from scripts/architect.js
// AngularJS
const ngApp = angular.module("main", []);
console.log("\tInitialized AngularJs.");
console.log(
"Initialization finished ###############################################"
);
// #endregion
// #region Helpers
module Calc {
// Converts radians to degrees
export function RadToDeg(rad: number): number {
return rad * (180 / Math.PI);
}
// Converts degrees to radians
export function DegToRad(deg: number): number {
return (deg * Math.PI) / 180;
}
// Rotates a point around 0,0 by an angle in radians
export function RotatePoint(
point: { x: number; y: number },
angle: number
): { x: number; y: number } {
return {
x: point.x * Math.cos(angle) - point.y * Math.sin(angle),
y: point.x * Math.sin(angle) + point.y * Math.cos(angle)
};
}
// UTM Coordinates
class UTM {
Easting: number;
Northing: number;
ZoneNumber: number;
ZoneLetter: string;
constructor(e: number, n: number, zn: number, zl: string) {
this.Easting = e;
this.Northing = n;
this.ZoneNumber = zn;
this.ZoneLetter = zl;
}
}
const _precision: number = 7; // Precision for LatLon to UTM conversion
const _utm = new UTMLatLng("WGS 84"); // Create UTM/LatLng converter for WGS84 projection
// Wrapper for LatLon to UTM conversion
export function LatLonToUtm(lat: number, lon: number) {
return _utm.convertLatLngToUtm(lat, lon, _precision);
}
// Wrapper for UTM to LatLon conversion
export function UtmToLatLon(utm: UTM): { lat: number; lon: number } {
let temp = _utm.convertUtmToLatLng(
utm.Easting,
utm.Northing,
utm.ZoneNumber,
utm.ZoneLetter
);
return { lat: temp.lat, lon: temp.lng };
}
}
// Leaflet marker with additional info to represent a mounting point
class MountingPoint extends L.Marker {
SupportedAssets: number[] = [];
ID: number = -1;
}
// Represents all levels with the same designation,
// Stores the mounting points of those levels
class Layer {
Name: string;
Ids: number[];
Selected: boolean;
LLayer: L.LayerGroup;
constructor(name) {
this.Name = name;
this.Ids = [];
this.Selected = true;
this.LLayer = L.layerGroup(undefined);
}
}
// Returns true if str is undefined, empty or null
function IsStringEmpty(str: string): boolean {
return str == undefined || str == "" || str == null;
}
// jQuery like shortcut to get html elements
function $(query: string): HTMLElement | null {
return document.querySelector(query);
}
// Retrieves the angular $scope from outside a controller
function GetScope(): any {
return angular.element($("body") as Element).scope() as any;
}
function Status(str: string): void {
GetScope().status = str;
console.log(str);
($("#progressBar") as HTMLProgressElement).value++;
}
// #endregion
// Main AngularJs controller
ngApp.controller("ctrl", $scope => {
let data: any = {}; // Will hold the websocket response
let t0: number | null = null; // Measures the time it takes to load and parse
$scope.disableInput = false; // True when loadData kicks in, disables the inputs
$scope.connected = false; // True when websocket response was received
$scope.status = undefined; // String that will be shown while connecting
// Writes the websocket response to data when the load button is clicked
/** @function */ $scope.loadDataFromWS = (ip: string): void => {
$scope.disableInput = true;
console.log(
"Begin loading data ####################################################"
);
t0 = performance.now(); // Get start time
// Default value for ip if nothing was entered
ip = IsStringEmpty(ip) ? "ws://192.168.133.140:2080" : ip;
// Catch invalid ips
try {
var ws = new WebSocket(ip); // Connect to the websocket
} catch (e) {
alert("Invalid IP");
return;
}
Status(`\tConnecting to ${ip}...`);
// Connect to the web socket
ws.onopen = () => {
console.log("\tConnected.");
ws.send(
JSON.stringify({
type: "getEssentials",
uuid: "UUID"
})
);
};
// Handles the response of the websocket
ws.onmessage = response => {
console.log(
`\tFetched ${Math.round(
new TextEncoder().encode(response.data).length / 1000
)}kb.`
);
data = JSON.parse(response.data).data; // Parse websocket response to data
window.localStorage.setItem("data", escape(response.data));
$scope.$broadcast("dataLoaded"); // Call response event to kick in the map and selection GUI
};
// Handle bad websocket responses
ws.onclose = event => {
if (event.code > 1000) {
t0 = null;
$scope.status = "";
alert(`Error when getting websocket response.\n${event.code}`);
$scope.disableInput = false;
}
};
};
/** @function */ $scope.loadDataFromCache = () => {
$scope.disableInput = true;
console.log(
"Begin loading data ####################################################"
);
t0 = performance.now();
Status("Loading data from cache...");
try {
if (window.localStorage.getItem("data") == null) throw new Error();
let _data = unescape(window.localStorage.getItem("data") as string);
data = JSON.parse(_data).data;
console.log(
`\tLoaded ${Math.round(
new TextEncoder().encode(_data).length / 1000
)}kb from cache.`
);
$scope.$broadcast("dataLoaded");
} catch {
$scope.status = "";
$scope.disableInput = false;
t0 = null;
console.log("\tCould not load data from cache.");
alert("No data cached.");
}
};
// Handles websocket response event
$scope.$on("dataLoaded", () => {
let _data: any = data;
$scope.connected = true; // Switches the connection menu for the mounting point selection
$scope.loading = true; // Disables the GUI while doing stuff
// #region MAP
console.log("\tCreating map.");
// Create the map, set view and image
let map = new L.Map("map").setView(
new L.LatLng(53.0528064472226, 8.89412999153137),
8
);
let imageBounds = new L.LatLngBounds([
[53.0528064472226, 8.89412999153137],
[53.056520842139, 8.90030980110168]
]);
let mapImage = L.imageOverlay("img/RDE2.jpg", imageBounds);
map.addLayer(mapImage);
map.fitBounds(imageBounds);
// #endregion
// #region BUILDINGS
// Get buildings
let buildings: any[] = _data.objects;
Status(`\tParsing ${buildings.length} buildings...`);
// Create GeoJson from buidling footprints
let footprintPolygons: GeoJSON.FeatureCollection = {
type: "FeatureCollection",
features: []
};
for (let currentBuilding of buildings) {
// Check if current builing has footprint data, if not: skip
if (!(currentBuilding.path[0] == undefined)) {
// Holds footprint coordinates of the current building
let footprintCoords: number[][][] = [[]];
// Parse footprint coordinates from current building
for (let pos of currentBuilding.path) {
footprintCoords[0].push([
// Convert from radians to degrees and add to the array
Calc.RadToDeg(pos.lon),
Calc.RadToDeg(pos.lat)
]);
}
// Last point has to be the same as the first point so we add it again
footprintCoords[0].push([
Calc.RadToDeg(currentBuilding.path[0].lon),
Calc.RadToDeg(currentBuilding.path[0].lat)
]);
// Create new GeoJson Feature with building footprint
let currentFootprint: GeoJSON.Feature = {
type: "Feature",
properties: {
name: currentBuilding.designation
},
geometry: {
type: "Polygon",
coordinates: footprintCoords
}
};
// Add GeoJson feature to feature collection
footprintPolygons.features.push(currentFootprint);
}
}
// Draw building footprints to map, add name popups
let footprintGeoJson = L.geoJSON(footprintPolygons, {
onEachFeature: (feature, layer) => {
layer.bindPopup(`<h2>${feature.properties.name}</h2>`);
}
}).addTo(map);
// #endregion
// #region LEVELS
let levels: any[] = _data.levels; // Levels are the actual floors in the building
let layers: Layer[] = []; // Layers are their representation on the map
Status(`\tParsing ${levels.length} levels...`);
for (let level of levels) {
// Create a layer for each level
let add: boolean = true;
for (let layer of layers) {
// Levels with duplicate names will be on the same layer
if (level.designation === layer.Name) {
add = false;
layer.Ids.push(level.id);
break;
}
}
if (add) {
// Create new Layer if level is not a duplicate
let newLayer: Layer = new Layer(level.designation);
newLayer.Ids.push(level.id);
layers.push(newLayer);
}
}
let outdoorLayer: Layer = new Layer("outdoor"); // Create a layer for mounting points outside of buildings
outdoorLayer.Ids = [-1];
let footprintLayer: Layer = new Layer("buildings"); // Create a layer for the building footprint
footprintLayer.LLayer = footprintGeoJson;
layers.push(outdoorLayer); // Add outdoor and footprint layers
layers.push(footprintLayer);
$scope.layers = layers; // Add layers to the checkbox menu
// #endregion
// #region MOUNTING POINTS
// Get mounting points
let mountingPoints: any[] = _data.mountingPoints;
Status(`\tParsing ${mountingPoints.length} mounting points...`);
for (let m of mountingPoints) {
for (let b of buildings) {
if (b.id == m.objectId) {
// Current mounting point is in current buidling?
b.utmRefPoint = Calc.LatLonToUtm(
Calc.RadToDeg(b.refPoint.lat),
Calc.RadToDeg(b.refPoint.lon)
); // Converts refPoint of the building to UTM
m.fixedPosition = Calc.RotatePoint(
{ x: m.position3d.x, y: m.position3d.y },
Calc.DegToRad(b.rotationDegree)
); // Rotates coords of the mounting point by rotationDegree of the building
m.absoluteUtm = b.utmRefPoint;
m.absoluteUtm.Easting += m.fixedPosition.x;
m.absoluteUtm.Northing += m.fixedPosition.y;
m.absoluteLatLon = Calc.UtmToLatLon(m.absoluteUtm);
// Add refPoint to the relative position and convert back to lat/lng
const arButtonHtml = isMobile
? `<button
onclick="window.Functions.DisplayInAr(${m.id})"
id="arButton"
>
Show in AR
</button>`
: "<i>AR not supported on desktop.</i>";
for (let l of layers) {
for (let i of l.Ids) {
if (i == m.levelId) {
// Current mouting point is on current level?
let marker = new MountingPoint(
// Create a marker for the mounting point
new L.LatLng(
m.absoluteLatLon.lat,
m.absoluteLatLon.lon
),
{
icon: L.icon({
iconUrl: "img/marker.png",
iconSize: [30, 30]
})
}
)
.bindPopup(
// Create a popup with info about the mounting point
`<h2>
${IsStringEmpty(m.tag) ? m.designation : m.tag}
</h2>
<table>
<tr>
<td>ID</td>
<td>${m.id}</td>
</tr>
<tr>
<td>Object ID</td>
<td>${m.objectId}</td>
</tr>
<tr>
<td>Supported Asset Type IDs</td>
<td>${m.supportedAssetTypeIds}</td>
</tr>
</table>
<br />
${arButtonHtml}
`
)
.addTo(map); // Add it to the map
marker.SupportedAssets = // Add info about supported asset types
m.supportedAssetTypeIds;
marker.ID = m.id; // Add mounting point id
l.LLayer.addLayer(marker); // Add marker to corresponding layer
}
}
}
}
}
}
$scope.mountingPoints = mountingPoints;
// #endregion
// #region ASSETS
// Get asset types
let assets: any[] = _data.assetTypes;
Status(`\tParsing ${assets.length} assets...`);
// Add asset types to the dropdown menu
$scope.assets = {
availableAssets: assets,
selectedAsset: assets[0]
};
// #endregion
// #region ANGULAR FUNCTIONS
// Called by either the layer or asset selection
/** @function */ $scope.SelectionChanged = () => {
let points: number = 0;
for (let layer of layers) {
if (layer.Selected) {
map.addLayer(layer.LLayer); // Add selected layers
// Hacky type conversion to get the supported asset types from each mounting point on a layer
// If it's stupid but it works it's not stupid
for (let m of (layer.LLayer.getLayers() as unknown[]) as MountingPoint[]) {
try {
for (let a of m.SupportedAssets) {
if (a == $scope.assets.selectedAsset.id) {
m.addTo(map); // Add markers for selected asset type
++points;
} else {
m.removeFrom(map); // Remove all other markers
}
}
} catch {
/** Sometimes theres an error here...
* Doesn't seem to interfere with anything though and
* I don't want to see it in the console
*/
}
}
} else map.removeLayer(layer.LLayer); // Remove unselected layers
}
console.log(
`Selected ${points} mounting point${points == 1 ? "" : "s"}.`
);
};
// Filter for selected mounting point table and count
/** @function */ $scope.MountingPointFilter = () => {
return mp => {
let correctAssetType: boolean = false;
for (let i of mp.supportedAssetTypeIds) {
if (i == $scope.assets.selectedAsset.id) {
correctAssetType = true;
break;
}
} // Current mounting point supports selected asset type
let correctLayer: boolean = false;
for (let layer of layers) {
for (let i of layer.Ids) {
if (layer.Selected && i == mp.levelId) {
correctLayer = true;
break;
}
}
} // Current mounting point is on a selected layer
let foundBySearch: boolean = IsStringEmpty($scope.mpSearch)
? true // Automatically true if search input empty
: (IsStringEmpty(mp.tag) ? mp.designation : mp.tag).indexOf(
$scope.mpSearch
) >= 0 || mp.id.toString().indexOf($scope.mpSearch) >= 0;
// true if tag, designation or id contain search input
return correctAssetType && correctLayer && foundBySearch;
};
};
// Function for the Show button
/** @function */ $scope.ShowMp = mp => {
for (let l of layers) {
// Not this again...
for (let m of (l.LLayer.getLayers() as unknown[]) as MountingPoint[]) {
if (m.ID == mp.id) {
// Find clicked mounting point
m.openPopup(); // Open corresponding popup
map.panTo(m.getLatLng()); // Center map on the popup
break;
}
}
}
};
// #endregion
// Get final time and format it
let time: string =
t0 != null
? `Finished loading data in ${performance.now() -
t0}ms ########################################`
: `Finished loading data #################################################`;
console.log(time);
$scope.SelectionChanged(); // Make initial map state fit the initial menu state
$scope.loading = false; // Done loading
$scope.$apply();
});
});
// #region WIKITUDE FUNCTIONS
var currentPOI: any = undefined; // Holds current wikitude ar marker
// Shows an augmented reality marker vor the selected mounting point
function _displayInAr(id: number): void {
let $scope = GetScope(); // Retrieve angular scope
let mountingPoint: any;
for (let mp of $scope.mountingPoints) {
if (mp.id == id) {
mountingPoint = mp;
break;
}
} // Get correct mounting point
let name = IsStringEmpty(mountingPoint.tag)
? mountingPoint.designation
: mountingPoint.tag;
$scope.arMountingPoint = name;
// Display mounting point tag/designation
currentPOI = new AR.GeoObject(
[
new AR.GeoLocation(
mountingPoint.absoluteLatLon.lat,
mountingPoint.absoluteLatLon.lon
)
],
{
drawables: {
cam: [
new AR.ImageDrawable(
new AR.ImageResource("img/armarker.png"),
2.5,
{ zOrder: 0, opacity: 1 }
),
new AR.Label(name, 1, {
zOrder: 1,
style: { textColor: "#ffffff" }
})
],
indicator: new AR.ImageDrawable(
new AR.ImageResource("img/arindicator.png"),
0.1,
{
enabled: true,
verticalAnchor: AR.CONST.VERTICAL_ANCHOR.TOP
}
)
}
}
); // Create AR marker
$scope.arMode = true; // Hide map and menu
$scope.$apply();
console.log("Switched to AR mode.");
}
// Goes back to the map view
function _backFromAr(): void {
let $scope = GetScope(); // Retrieve angular scope
$scope.arMountingPoint = ""; // Clear name display
$scope.arMode = false; // Show map and menu again
currentPOI.destroy();
currentPOI = undefined;
$scope.$apply();
console.log("Switched to map mode.");
}
// #region TS -> JS
/**
* Hacky way to make typescript functions accessible from the html page
* like in regular javascript
*/
// Add "Functions"-object to window
declare global {
interface Window {
Functions: any;
}
}
// Attach functions
window.Functions = window.Functions || {
DisplayInAr: _displayInAr,
BackFromAr: _backFromAr
};
// #endregion
// #endregion
+39
View File
@@ -0,0 +1,39 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@types/angular": {
"version": "1.6.54",
"resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.54.tgz",
"integrity": "sha512-xA1FuozWXeRQ7FClUbvk8ePL+dydBeDoCWRPFTHU5+8uvVtIIfLGiHA8CMkwsbddFCYnTDVbLxG85a/HBx7LtA=="
},
"@types/geojson": {
"version": "7946.0.5",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.5.tgz",
"integrity": "sha512-rLlMXpd3rdlrp0+xsrda/hFfOpIxgqFcRpk005UKbHtcdFK+QXAjhBAPnvO58qF4O1LdDXrcaiJxMgstCIlcaw=="
},
"@types/leaflet": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.4.2.tgz",
"integrity": "sha512-iRyUFh6m5OSbhekHxlZb5SkdfUVpaXhk+e8fgQviZL6iZ1chV3Q030/+M6jO2A6VM235A1hYuUmBMVsrLAvKEA==",
"requires": {
"@types/geojson": "*"
}
},
"angular": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.7.7.tgz",
"integrity": "sha512-MH3JEGd8y/EkNCKJ8EV6Ch0j9X0rZTta/QVIDpBWaIdfh85/e5KO8+ZKgvWIb02MQuiS20pDFmMFlv4ZaLcLWg=="
},
"leaflet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.4.0.tgz",
"integrity": "sha512-x9j9tGY1+PDLN9pcWTx9/y6C5nezoTMB8BLK5jTakx+H7bPlnbCHfi9Hjg+Qt36sgDz/cb9lrSpNQXmk45Tvhw=="
},
"utm-latlng": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/utm-latlng/-/utm-latlng-1.0.5.tgz",
"integrity": "sha1-rmBbTCLSapYVmoH2vLOSkyEj18I="
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"dependencies": {
"@types/angular": "^1.6.54",
"@types/leaflet": "^1.4.2",
"angular": "^1.7.7",
"leaflet": "^1.4.0",
"utm-latlng": "^1.0.5"
}
}
+624
View File
@@ -0,0 +1,624 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer {
max-width: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-control-zoom-out {
font-size: 20px;
}
.leaflet-touch .leaflet-control-zoom-in {
font-size: 22px;
}
.leaflet-touch .leaflet-control-zoom-out {
font-size: 24px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
+105
View File
@@ -0,0 +1,105 @@
table {
background-color: darkgray;
}
th,
td {
text-align: left;
padding: 5px;
background-color: white;
}
.bigBr {
line-height: 200%;
}
#map {
height: 100vh;
width: 100%;
}
#parentContainer {
width: 100%;
height: 100vh;
overflow: hidden;
background-color: white;
}
#connectionConatiner {
border: 1px solid black;
padding: 10px;
margin: auto;
width: 300px;
text-align: center;
}
#mapContainer {
height: 100%;
width: 66%;
float: left;
}
#guiContainer {
height: 100%;
width: 34%;
float: right;
box-sizing: border-box;
padding: 20px;
}
#arCtrl {
position: absolute;
background-color: white;
top: 90vh;
height: 10vh;
width: 98%;
text-align: center;
align-content: center;
}
#mp {
overflow: auto;
height: 350px;
}
@media only screen and (max-width: 500px) {
#map {
height: 50%;
width: 99%;
position: fixed;
}
#parentContainer {
width: 100%;
height: 100vh;
overflow: hidden;
background-color: white;
}
#mapContainer {
height: 50%;
width: 100%;
float: unset;
}
#guiContainer {
position: fixed;
overflow: auto;
top: 55%;
height: 45%;
box-sizing: border-box;
padding: 20px;
width: 100%;
float: unset;
}
h1 {
font-size: 20px;
}
body {
font-size: 15px;
}
}
+65
View File
@@ -0,0 +1,65 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "*": ["types/*"] },
/* Basic Options */
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
"types": [
"angular",
"leaflet"
] /* Type declaration files to be included in compilation. */,
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}