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

+13
View File
@@ -0,0 +1,13 @@
import { DEBUG } from "./main";
export default class Asset {
public Name: string;
public Id: number;
constructor(Raw: { id: number; designation: string; deviceClass: number }) {
this.Name = Raw.designation;
this.Id = Raw.id;
if (DEBUG) console.log(`new Asset() with Id = ${this.Id}`);
}
}
+69
View File
@@ -0,0 +1,69 @@
import { Helpers } from "./Helpers";
import { DEBUG } from "./main";
export default class Building {
public Footprint: GeoJSON.Feature | null;
public Name: string;
public Id: number;
public RefPoint: Helpers.Coordinates.Utm;
public RotationDegree: number;
constructor(Raw: {
id: number;
designation: string;
areaId: number;
refPoint: {
lat: number;
lon: number;
};
street: string;
number: string;
path: {
lat: number;
lon: number;
}[];
pathRelative: {
lat: number;
lon: number;
}[];
rotationDegree: number;
}) {
try {
let footprintCoords = [
Raw.path.map(p => [
Helpers.Calculations.RadToDeg(p.lon),
Helpers.Calculations.RadToDeg(p.lat)
])
];
footprintCoords[0].push([
Helpers.Calculations.RadToDeg(Raw.path[0].lon),
Helpers.Calculations.RadToDeg(Raw.path[0].lat)
]); // Last point == first point
this.Footprint = {
type: "Feature",
properties: {
name: Raw.designation
},
geometry: {
type: "Polygon",
coordinates: footprintCoords
}
};
} catch {
this.Footprint = null;
}
this.RefPoint = new Helpers.Coordinates.LatLon(
Helpers.Calculations.RadToDeg(Raw.refPoint.lat),
Helpers.Calculations.RadToDeg(Raw.refPoint.lon)
).ToUtm();
this.Name = Raw.designation;
this.Id = Raw.id;
this.RotationDegree = Raw.rotationDegree;
if (DEBUG) console.log(`new Building() with Id = ${this.Id}`);
}
}
+163
View File
@@ -0,0 +1,163 @@
import { Helpers, _, MOBILE } from "./Helpers";
export module Functions {
//#region private
async function checkForPort(ip: string): Promise<number> {
return new Promise<number>(async (resolve, reject) => {
new Helpers.WebSocketRequest(
ip,
JSON.stringify({
type: "servicePortsInformationRequest",
uuid: Helpers.GenUuid()
})
)
.GetResponseAsync()
.then(response => {
for (let p of JSON.parse(response).data[0].services) {
if (p.nameAbbrevation === "ism") {
return resolve(p.ports[0].number);
}
}
return reject();
})
.catch(() => {
return reject();
});
});
}
async function retrieveDataIp(tryLocalhostFirst: boolean): Promise<string> {
return new Promise<string>((resolve, reject) => {
const localhost: string = "ws://localhost";
const defaultIp: string = "ws://192.168.133.140";
const portToCheck: number = 7300;
if (tryLocalhostFirst) {
checkForPort(`${localhost}:${portToCheck}`)
.then(port => {
return resolve(`${localhost}:${port}`);
})
.catch(() =>
checkForPort(`${defaultIp}:${portToCheck}`)
.then(port => {
return resolve(`${defaultIp}:${port}`);
})
.catch(() => {
return reject();
})
);
} else {
checkForPort(`${defaultIp}:${portToCheck}`)
.then(port => resolve(`${defaultIp}:${port}`))
.catch(() =>
checkForPort(`${localhost}:${portToCheck}`)
.then(port => {
return resolve(`${localhost}:${port}`);
})
.catch(() => {
return reject();
})
);
}
});
}
async function loadData(ip: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
new Helpers.WebSocketRequest(
ip,
JSON.stringify({
type: "getEssentials",
uuid: Helpers.GenUuid()
})
)
.GetResponseAsync()
.then(data => resolve(JSON.parse(data).data))
.catch(() => {
return reject();
});
});
}
async function manualLoadData(): Promise<any> {
return new Promise<any>(async (resolve, reject) => {
let data: any;
let serverIp: string = "ws://192.168.133.140:2080";
do {
serverIp = prompt(
"Unable to load data automatically. Please enter ip:",
serverIp
) as string;
try {
data = await loadData(serverIp);
} catch {
continue;
}
} while (_(data));
webSocketIp = serverIp;
return resolve(data);
});
}
function storeInCache(key: string, value: any): void {
window.localStorage.setItem(key, escape(JSON.stringify(value)));
}
function retrieveFromCache(key: string): any | never {
let value = window.localStorage.getItem(key);
if (!_(value)) return JSON.parse(unescape(value as string));
else throw new Error();
}
//#endregion
/**
* Tries to load the data automatically from the websocket,
* then from cache, then from the user. Always caches the
* response.
*/
export async function GetData(): Promise<any> {
return new Promise<any>(async (resolve, reject) => {
var data: any;
try {
let serverIp = await retrieveDataIp(!MOBILE);
data = await loadData(serverIp);
webSocketIp = serverIp;
} catch {
try {
data = retrieveFromCache("data");
} catch {
data = await manualLoadData();
}
} finally {
storeInCache("data", data);
return resolve(data);
}
});
}
var webSocketIp: string | null = null;
export async function GetMountedAssetIp(Id: number): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
if (_(webSocketIp)) return reject();
try {
let ip: string = JSON.parse(
await new Helpers.WebSocketRequest(
webSocketIp as string,
JSON.stringify({
type: "getAssetState",
uuid: Helpers.GenUuid(),
data: {
assetId: Id
}
})
).GetResponseAsync()
).data.ipAddr;
if (!_(ip)) return resolve(`http://${ip}/`);
else return reject();
} catch {
return reject();
}
});
}
}
+158
View File
@@ -0,0 +1,158 @@
import * as UTM from "utm";
import angular from "angular";
/**
* Checks for emptyness
* @returns obj == undefined || obj == null || obj == ""
*/
export function _(obj: any): boolean {
return obj == undefined || obj == null || obj == "" || obj == [];
}
/**
* Retrieves a DOM element
* @returns document.querySelector(query)
*/
export function $(query: string): HTMLElement | null {
return document.querySelector(query);
}
export const MOBILE: boolean = /Android/i.test(navigator.userAgent);
declare global {
interface Array<T> {
includes(obj: any): boolean;
}
}
/**
* Checks if the array includes given element
*/
Array.prototype.includes = function(obj: any) {
return this.find(element => element == obj) != undefined;
};
export module Helpers {
export namespace Calculations {
export function RadToDeg(rad: number): number {
return rad * (180 / Math.PI);
}
export function DegToRad(deg: number): number {
return (deg * Math.PI) / 180;
}
export function RotatePoint(
point: { x: number; y: number },
angleRad: number
): { x: number; y: number } {
return {
x: point.x * Math.cos(angleRad) - point.y * Math.sin(angleRad),
y: point.x * Math.sin(angleRad) + point.y * Math.cos(angleRad)
};
}
}
/**
* Retrieves angular scope
*/
export function GetScope(): any {
return angular.element($("body") as Element).scope() as any;
}
/**
* ArAssets-YYYY-MM-DDTHH:mm:ss.sssZ
*/
export function GenUuid(): string {
return `ArAssets-${new Date().toISOString()}`;
}
export class WebSocketRequest {
/**
* Execute with .GetResponseAsync()
*/
constructor(private Ip: string, private Request: string) {}
/**
* Resolves into the response of the websocket
*/
public async GetResponseAsync(): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
let ws = new WebSocket(this.Ip);
ws.onopen = () => {
ws.send(this.Request);
};
ws.onmessage = response => {
ws.close();
return resolve(response.data);
};
ws.onclose = event => {
if (event.code > 1000) return reject(event.reason);
};
ws.onerror = error => {
return reject(error);
};
} catch {
return reject(); // Just to be sure
}
});
}
}
/** @deprecated */
export async function GetPositionAsync(): Promise<Coordinates.LatLon> {
return new Promise<Coordinates.LatLon>((resolve, reject) => {
navigator.geolocation.getCurrentPosition(pos => {
resolve(
new Coordinates.LatLon(
pos.coords.latitude,
pos.coords.longitude
)
);
});
reject();
});
}
export namespace Coordinates {
// const utm: UTMLatLng = new UTMLatLng("WGS 84");
// const PRECISION: number = 7;
export class LatLon {
constructor(public Lat: number, public Lon: number) {}
public ToUtm(): Utm {
let temp = UTM.fromLatLon(
this.Lat,
this.Lon
);
return new Utm(
temp.easting,
temp.northing,
temp.zoneNum,
temp.zoneLetter
);
}
}
export class Utm {
constructor(
public Easting: number,
public Northing: number,
public ZoneNumber: number,
public ZoneLetter: string
) {}
public ToLatLon(): LatLon {
let temp = UTM.toLatLon(
this.Easting,
this.Northing,
this.ZoneNumber,
this.ZoneLetter
);
return new LatLon(temp.latitude, temp.longitude);
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
import L from "leaflet";
import { DEBUG } from "./main";
export default class Layer {
public LeafletLayer: L.LayerGroup = new L.LayerGroup();
public Visible: boolean = true;
public LayerIds: number[] = [];
constructor(public Name: string) {
if (DEBUG) console.log(`new Layer() with Name = ${this.Name}`);
}
}
+33
View File
@@ -0,0 +1,33 @@
import L from "leaflet";
import { DEBUG } from "./main";
export default class Location extends L.ImageOverlay {
public Name: string;
constructor(Raw: {
img: string;
name: string;
bounds: {
min: {
lat: number;
lon: number;
};
max: {
lat: number;
lon: number;
};
};
}) {
super(
`map/${Raw.img}`,
new L.LatLngBounds([
[Raw.bounds.min.lat, Raw.bounds.min.lon],
[Raw.bounds.max.lat, Raw.bounds.max.lon]
])
);
this.Name = Raw.name;
if (DEBUG) console.log(`new Location() with Name = ${this.Name}`);
}
}
+174
View File
@@ -0,0 +1,174 @@
import L from "leaflet";
import Building from "./Building";
import { Helpers, _, MOBILE } from "./Helpers";
import { Functions } from "./Functions";
import { DEBUG } from "./main";
export default class MountingPoint extends L.Marker {
public Name: string = "";
public Id: number = -1;
public SupportedAssets: number[] = [];
public Level: number = -1;
public MountedAsset: number = -1;
private constructor() {
super(new L.LatLng(0, 0));
throw new Error("Use static method MountingPoint.Create() instead.");
// Don't use this.
// This is just here so you can't use the constructor from
// outside the class
}
/**
* MountingPoint factory method
*/
public static async Create(
Raw: {
id: number;
designation: string;
tag: string;
objectId: number;
levelId: number;
roomId: number;
assetId: number;
position3d: {
x: number;
y: number;
z: number;
};
supportedAssetTypeIds: number[];
},
Building: Building
): Promise<MountingPoint> {
return new Promise<MountingPoint>(async resolve => {
// Wrong building?
if (Building.Id != Raw.objectId) throw new Error("Wrong building");
let rotatedRelativePosition = Helpers.Calculations.RotatePoint(
{
x: Raw.position3d.x,
y: Raw.position3d.y
},
Helpers.Calculations.DegToRad(Building.RotationDegree)
); // Rotate mp position around building rotation degree
let temp = Object.create(Building.RefPoint);
temp.Easting += rotatedRelativePosition.x;
temp.Northing += rotatedRelativePosition.y;
let position = temp.ToLatLon();
// Add building ref point to mp relative position
let mounted = Raw.assetId != -1;
let mp = new L.Marker(new L.LatLng(position.Lat, position.Lon), {
icon: new L.Icon({
iconUrl: mounted
? "img/marker_mounted.png"
: "img/marker_default.png",
iconSize: [30, 30]
})
}) as MountingPoint; // Setup Leaflet marker
mp.Name = _(Raw.tag) ? Raw.designation : Raw.tag;
mp.SupportedAssets = Raw.supportedAssetTypeIds || [];
mp.Id = Raw.id;
mp.Level = Raw.levelId;
mp.MountedAsset = Raw.assetId;
let popup = document.createElement("div") as HTMLDivElement;
// <h1>
let header = document.createElement("h2") as HTMLHeadingElement;
header.innerText = mp.Name;
// <table>
let table = document.createElement("table") as HTMLTableElement;
table.insertRow().insertCell().innerText = "ID";
table.rows[0].insertCell().innerText = mp.Id.toString();
table.insertRow().insertCell().innerText = "Object ID";
table.rows[1].insertCell().innerText = Raw.objectId.toString();
table.insertRow().insertCell().innerText = "Supported asset IDs";
table.rows[2].insertCell().innerText = mp.SupportedAssets.toString();
// <tr>
let asset = document.createElement("tr") as HTMLTableRowElement;
try {
let link = document.createElement("a") as HTMLAnchorElement;
link.innerText = "Mounted asset ID";
link.setAttribute(
"href",
await Functions.GetMountedAssetIp(mp.MountedAsset)
);
link.setAttribute("target", MOBILE ? "_parent" : "_blank");
asset.insertCell().appendChild(link);
} catch {
asset.insertCell().innerText = "Mounted asset ID";
}
asset.insertCell().innerText = mp.MountedAsset.toString();
if (mounted) table.appendChild(asset);
// <button>
let button = document.createElement("button") as HTMLButtonElement;
button.innerText = MOBILE ? "Show in AR" : "Raw info";
button.onclick = MOBILE
? () => window.AR.StartAr(mp.Id)
: () =>
window.MP.MoreInfo(
escape(JSON.stringify(Raw, null, "\t"))
);
// Build popup
popup.appendChild(header);
popup.appendChild(table);
popup.appendChild(document.createElement("hr") as HTMLHRElement);
popup.appendChild(button);
mp.bindPopup(popup);
if (DEBUG) console.log(`new MountingPoint() with ID = ${mp.Id}`);
return resolve(mp);
});
}
}
function _moreInfo(info: string) {
let popup = open("", Helpers.GenUuid()) as Window;
// <pre>
let display = popup.document.createElement("pre") as HTMLPreElement;
display.innerHTML = unescape(info);
display.style.fontSize = "15";
// <button>
let finish = popup.document.createElement("button") as HTMLButtonElement;
finish.innerHTML = "Done";
finish.setAttribute("onclick", "window.close()");
// build popup
popup.document.body.appendChild(popup.document.createElement(
"hr"
) as HTMLHRElement);
popup.document.body.appendChild(display);
popup.document.body.appendChild(popup.document.createElement(
"hr"
) as HTMLHRElement);
popup.document.body.appendChild(finish);
}
//#region
declare global {
interface Window {
MP: {
MoreInfo: (info: string) => void;
};
}
}
window.MP = window.MP || {
MoreInfo: _moreInfo
};
//#endregion
+92
View File
@@ -0,0 +1,92 @@
/**
* This is only used in the mobile app
*/
import { Helpers, $ } from "./Helpers";
import MountingPoint from "./MountingPoint";
import { DEBUG } from "./main";
declare var AR: any; // Wikitude is loaded from html
var currentPoi: any = undefined;
function _startAr(mpId: number): void {
let $scope = Helpers.GetScope();
let mountingPoint: MountingPoint | undefined = undefined;
for (let mp of $scope.objects.mountingPoints as MountingPoint[]) {
if (mp.Id == mpId) {
mountingPoint = mp;
break;
}
} // Get correct mounting point
mountingPoint = mountingPoint as MountingPoint; // Bruh.
currentPoi = new AR.GeoObject(
[
new AR.GeoLocation(
mountingPoint.getLatLng().lat,
mountingPoint.getLatLng().lng
)
],
{
drawables: {
cam: [
new AR.ImageDrawable(
new AR.ImageResource("img/armarker.png"),
2.5,
{ zOrder: 0, opacity: 1 }
),
new AR.Label(mountingPoint.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.ar.current = mountingPoint.Name;
$scope.ar.enabled = true;
$scope.$apply();
($("body") as HTMLBodyElement).style.backgroundColor = "transparent";
}
function _endAr(): void {
let $scope = Helpers.GetScope();
$scope.ar.current = "";
$scope.ar.enabled = false;
currentPoi.destroy();
currentPoi = undefined;
$scope.$apply();
($("body") as HTMLBodyElement).style.backgroundColor = "white";
}
//#region
declare global {
interface Window {
AR: {
StartAr: (mpId: number) => void;
EndAr: () => void;
};
}
}
window.AR = window.AR || {
StartAr: _startAr,
EndAr: _endAr
};
//#endregion
if (DEBUG) AR.logger.activateDebugMode();
+240
View File
@@ -0,0 +1,240 @@
import angular from "angular";
import L from "leaflet";
import GeoJSON from "geojson";
import { _ } from "./Helpers";
import { Functions } from "./Functions";
import Building from "./Building";
import Layer from "./Layer";
import Asset from "./Asset";
import MountingPoint from "./MountingPoint";
import Location from "./Location";
export const DEBUG: boolean = false; // Debug mode
const NGAPP: angular.IModule = angular.module("main", []);
NGAPP.controller("ctrl", async ($scope, $http) => {
const dataRequest: any = Functions.GetData(); // Request data from websocket
//#region $scope init
$scope.html = {
disableInput: true, // Hides everything and displays a loading animation
mpQuery: "" // Text input for searching a mounting point
};
$scope.objects = {
layers: null, // Layer[]
assets: {
available: null, // Asset[]
selected: null // Asset
},
mountingPoints: null, // MountingPoint[]
locations: null // Location[]
};
$scope.functions = {
search: null, // function(): (function(mountingPoint: MountingPoint): boolean)
updateView: null, // function(): void
openPopup: null // function(mountingPoint: MountingPoint): void
};
$scope.ar = {
enabled: false, // AR mode enabled when true
current: "" // Name of mounting point currently shown in ar
};
//#endregion
//#region Map
const defaultView: L.LatLngBounds = new L.LatLngBounds([
[53.0528064472226, 8.89412999153137],
[53.056520842139, 8.90030980110168]
]); // Default view centered on Bremen
const map = new L.Map("map", { attributionControl: false }).fitBounds(
defaultView
); // Create the map
try {
var locations: Location[] = (await $http.get(
"map/overlays.json"
)).data.map((raw: any) => new Location(raw)); // Create map overlays
$scope.objects.locations = locations;
for (let location of locations) location.addTo(map); // Add overlays to map
map.fitBounds(locations[0].getBounds()); // Center on the first overlay
} catch (e) {
if (DEBUG)
console.error(`Could not load map overlays:\n${JSON.stringify(e)}`);
}
//#endregion
const data = await dataRequest; // Wait for websocket response
var t0 = performance.now(); // Time measurement
//#region Buildings
let buildings = (data.objects as any[]).map(raw => new Building(raw));
let leafletFootprints = new L.GeoJSON(
{
type: "FeatureCollection",
features: buildings
.map(building => building.Footprint)
.filter(footprint => footprint != null)
} as GeoJSON.FeatureCollection,
{
onEachFeature: (feature, layer) => {
layer.bindPopup(`<h2>${feature.properties.name}</h2>`);
}
}
).addTo(map);
//#endregion
//#region Layers
let layers: Layer[] = [];
for (let level of data.levels) {
let add = true;
for (let layer of layers) {
if (layer.Name === level.designation) {
// Same level name -> same layer
add = false;
layer.LayerIds.push(level.id);
break;
}
}
if (add) {
let temp = new Layer(level.designation);
temp.LayerIds.push(level.id);
layers.push(temp);
}
}
for (let layer of layers) layer.LeafletLayer.addTo(map);
let outdoorLayer = new Layer("outdoor");
outdoorLayer.LayerIds = [-1];
outdoorLayer.LeafletLayer.addTo(map);
let footprintLayer = new Layer("buildings");
footprintLayer.LeafletLayer = leafletFootprints;
footprintLayer.LeafletLayer.addTo(map);
layers.push(outdoorLayer, footprintLayer);
$scope.objects.layers = layers;
//#endregion
//#region Assets
let assets = (data.assetTypes as any[]).map(raw => new Asset(raw));
$scope.objects.assets.available = assets;
$scope.objects.assets.selected = assets[0];
//#endregion
//#region Mounting points
let mountingPoints: MountingPoint[] = [];
for (let mountingPoint of data.mountingPoints) {
for (let building of buildings) {
if (building.Id == mountingPoint.objectId) {
// Find matching building for each mounting point
let temp = await MountingPoint.Create(mountingPoint, building);
mountingPoints.push(temp);
for (let layer of layers) {
if (layer.LayerIds.includes(temp.Level)) {
// Add mounting point to correct layer
layer.LeafletLayer.addLayer(temp);
}
}
}
}
}
for (let layer of layers) {
layer.LeafletLayer.addTo(map);
// Add all layers to the map
}
$scope.objects.mountingPoints = mountingPoints;
//#endregion
//#region Angular functions
/** @function */ $scope.functions.search = () => {
return (mountingPoint: MountingPoint) => {
/* Correct asset type? */
let correctAssetType = false;
for (let id of mountingPoint.SupportedAssets) {
if (id == $scope.objects.assets.selected.Id) {
correctAssetType = true;
break;
}
}
/* Correct layer? */
let correctLayer = false;
for (let layer of layers) {
if (layer.LayerIds.includes(mountingPoint.Level)) {
correctLayer = true;
break;
}
}
/* ID or Name matches query? */
let query = $scope.html.mpQuery;
let foundBySearch = _(query)
? true
: mountingPoint.Name.includes(query) ||
mountingPoint.Id.toString().includes(query);
return correctAssetType && correctLayer && foundBySearch;
};
};
/** @function */ $scope.functions.updateView = () => {
for (let layer of layers) {
if (layer.Name === "buildings") {
if (layer.Visible) leafletFootprints.addTo(map);
else leafletFootprints.removeFrom(map);
continue;
}
for (let mountingPoint of layer.LeafletLayer.getLayers() as MountingPoint[]) {
if (
layer.Visible &&
mountingPoint.SupportedAssets.includes(
$scope.objects.assets.selected.Id
)
)
mountingPoint.addTo(map);
else mountingPoint.removeFrom(map);
}
}
};
/** @function */ $scope.functions.openPopup = (
mountingPoint: MountingPoint
) => {
mountingPoint.openPopup();
map.panTo(mountingPoint.getLatLng());
};
/** @function */ $scope.functions.goToLocation = (
bounds: L.LatLngBounds
) => {
map.fitBounds(bounds);
};
$scope.functions.updateView();
$scope.html.disableInput = false;
$scope.$apply();
console.log(`Finished loading in ${performance.now() - t0}ms.`);
});