665 lines
17 KiB
TypeScript
Executable File
665 lines
17 KiB
TypeScript
Executable File
// #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
|