// #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(`

${feature.properties.name}

`); } }).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 ? `` : "AR not supported on desktop."; 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 `

${IsStringEmpty(m.tag) ? m.designation : m.tag}

ID ${m.id}
Object ID ${m.objectId}
Supported Asset Type IDs ${m.supportedAssetTypeIds}

${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