import { Circle, LatLng, Polygon } from "leaflet"; import * as turf from "@turf/turf"; import { UnitDatabase } from "../unit/databases/unitdatabase"; import { aircraftDatabase } from "../unit/databases/aircraftdatabase"; import { helicopterDatabase } from "../unit/databases/helicopterdatabase"; import { groundUnitDatabase } from "../unit/databases/groundunitdatabase"; import { ROEs, emissionsCountermeasures, reactionsToThreat, states, } from "../constants/constants"; import { navyUnitDatabase } from "../unit/databases/navyunitdatabase"; import { DateAndTime, UnitBlueprint } from "../interfaces"; import { Converter } from "usng"; import { MGRS } from "../types/types"; import { getApp } from "../olympusapp"; import { featureCollection } from "turf"; export function bearing( lat1: number, lon1: number, lat2: number, lon2: number ) { const φ1 = deg2rad(lat1); // φ, λ in radians const φ2 = deg2rad(lat2); const λ1 = deg2rad(lon1); // φ, λ in radians const λ2 = deg2rad(lon2); const y = Math.sin(λ2 - λ1) * Math.cos(φ2); const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); const θ = Math.atan2(y, x); const brng = (rad2deg(θ) + 360) % 360; // in degrees return brng; } export function distance( lat1: number, lon1: number, lat2: number, lon2: number ) { const R = 6371e3; // metres const φ1 = deg2rad(lat1); // φ, λ in radians const φ2 = deg2rad(lat2); const Δφ = deg2rad(lat2 - lat1); const Δλ = deg2rad(lon2 - lon1); const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const d = R * c; // in metres return d; } export function bearingAndDistanceToLatLng( lat: number, lon: number, brng: number, dist: number ) { const R = 6371e3; // metres const φ1 = deg2rad(lat); // φ, λ in radians const λ1 = deg2rad(lon); const φ2 = Math.asin( Math.sin(φ1) * Math.cos(dist / R) + Math.cos(φ1) * Math.sin(dist / R) * Math.cos(brng) ); const λ2 = λ1 + Math.atan2( Math.sin(brng) * Math.sin(dist / R) * Math.cos(φ1), Math.cos(dist / R) - Math.sin(φ1) * Math.sin(φ2) ); return new LatLng(rad2deg(φ2), rad2deg(λ2)); } export function ConvertDDToDMS(D: number, lng: boolean) { var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; var deg = 0 | (D < 0 ? (D = -D) : D); var min = 0 | (((D += 1e-9) % 1) * 60); var sec = (0 | (((D * 60) % 1) * 6000)) / 100; var dec = Math.round((sec - Math.floor(sec)) * 100); var sec = Math.floor(sec); if (lng) return ( dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"' ); else return ( dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + '"' ); } export function dataPointMap(container: HTMLElement, data: any) { Object.keys(data).forEach((key) => { const val = "" + data[key]; // Ensure a string container.querySelectorAll(`[data-point="${key}"]`).forEach((el) => { // We could probably have options here if (el instanceof HTMLInputElement) { el.value = val; } else if (el instanceof HTMLElement) { el.innerText = val; } }); }); } export function deg2rad(deg: number) { var pi = Math.PI; return deg * (pi / 180); } export function rad2deg(rad: number) { var pi = Math.PI; return rad / (pi / 180); } export function generateUUIDv4() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } export function keyEventWasInInput(event: KeyboardEvent) { const target = event.target; return ( target instanceof HTMLElement && ["INPUT", "TEXTAREA"].includes(target.nodeName) ); } export function reciprocalHeading(heading: number): number { return heading > 180 ? heading - 180 : heading + 180; } /** * Prepend numbers to the start of a string * * @param num subject number * @param places places to pad * @param decimal whether this is a decimal number or not * * */ export const zeroAppend = function ( num: number, places: number, decimal: boolean = false ) { var string = decimal ? num.toFixed(2) : String(num); while (string.length < places) { string = "0" + string; } return string; }; export const zeroPad = function (num: number, places: number) { var string = String(num); while (string.length < places) { string += "0"; } return string; }; export function similarity(s1: string, s2: string) { var longer = s1; var shorter = s2; if (s1.length < s2.length) { longer = s2; shorter = s1; } var longerLength = longer.length; if (longerLength == 0) { return 1.0; } return (longerLength - editDistance(longer, shorter)) / longerLength; } export function editDistance(s1: string, s2: string) { s1 = s1.toLowerCase(); s2 = s2.toLowerCase(); var costs = new Array(); for (var i = 0; i <= s1.length; i++) { var lastValue = i; for (var j = 0; j <= s2.length; j++) { if (i == 0) costs[j] = j; else { if (j > 0) { var newValue = costs[j - 1]; if (s1.charAt(i - 1) != s2.charAt(j - 1)) newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; costs[j - 1] = lastValue; lastValue = newValue; } } } if (i > 0) costs[s2.length] = lastValue; } return costs[s2.length]; } export function latLngToMGRS( lat: number, lng: number, precision: number = 4 ): MGRS | false { if (precision < 0 || precision > 6) { console.error( "latLngToMGRS: precision must be a number >= 0 and <= 6. Given precision: " + precision ); return false; } const mgrs = new Converter({}).LLtoMGRS(lat, lng, precision); const match = mgrs.match(new RegExp(`^(\\d{2})([A-Z])([A-Z])([A-Z])(\\d+)$`)); const easting = match[5].substr(0, match[5].length / 2); const northing = match[5].substr(match[5].length / 2); let output: MGRS = { bandLetter: match[2], columnLetter: match[3], groups: [match[1] + match[2], match[3] + match[4], easting, northing], easting: easting, northing: northing, precision: precision, rowLetter: match[4], string: match[0], zoneNumber: match[1], }; return output; } export function latLngToUTM(lat: number, lng: number) { return new Converter({}).LLtoUTM(lat, lng); } export function latLngToMercator( lat: number, lng: number ): { x: number; y: number } { var rMajor = 6378137; //Equatorial Radius, WGS84 var shift = Math.PI * rMajor; var x = (lng * shift) / 180; var y = Math.log(Math.tan(((90 + lat) * Math.PI) / 360)) / (Math.PI / 180); y = (y * shift) / 180; return { x: x, y: y }; } export function mercatorToLatLng(x: number, y: number) { var rMajor = 6378137; //Equatorial Radius, WGS84 var shift = Math.PI * rMajor; var lng = (x / shift) * 180.0; var lat = (y / shift) * 180.0; lat = (180 / Math.PI) * (2 * Math.atan(Math.exp((lat * Math.PI) / 180.0)) - Math.PI / 2.0); return { lng: lng, lat: lat }; } export function knotsToMs(knots: number) { return knots / 1.94384; } export function msToKnots(ms: number) { return ms * 1.94384; } export function ftToM(ft: number) { return ft * 0.3048; } export function mToFt(m: number) { return m / 0.3048; } export function mToNm(m: number) { return m * 0.000539957; } export function nmToM(nm: number) { return nm / 0.000539957; } export function nmToFt(nm: number) { return nm * 6076.12; } export function areaContains(latlng: LatLng, area: Polygon | Circle) { if (area instanceof Polygon) return polyContains(latlng, area); else return circleContains(latlng, area); } export function polyContains(latlng: LatLng, polygon: Polygon) { let coordinates = [ (polygon.getLatLngs()[0] as LatLng[]).map((latlng) => { return [latlng.lng, latlng.lat]; }), ]; coordinates[0].push([ polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat, ]); const poly = turf.polygon(coordinates); return turf.inside(turf.point([latlng.lng, latlng.lat]), poly); } export function circleContains(latlng: LatLng, circle: Circle) { const poly = turf.circle( turf.point([circle.getLatLng().lng, circle.getLatLng().lat]), circle.getRadius() / 1000, 100, "kilometers" ); return turf.inside(turf.point([latlng.lng, latlng.lat]), poly); } export function polyCenter(polygon: Polygon) { let coordinates = [ (polygon.getLatLngs()[0] as LatLng[]).map((latlng) => { return [latlng.lng, latlng.lat]; }), ]; coordinates[0].push([ polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat, ]); const poly = turf.polygon(coordinates); const center = turf.center(featureCollection([poly])); return new LatLng( center.geometry.coordinates[1], center.geometry.coordinates[0] ); } export function randomPointInPoly(polygon: Polygon): LatLng { var bounds = polygon.getBounds(); var x_min = bounds.getEast(); var x_max = bounds.getWest(); var y_min = bounds.getSouth(); var y_max = bounds.getNorth(); var lat = y_min + Math.random() * (y_max - y_min); var lng = x_min + Math.random() * (x_max - x_min); let coordinates = [ (polygon.getLatLngs()[0] as LatLng[]).map((latlng) => { return [latlng.lng, latlng.lat]; }), ]; coordinates[0].push([ polygon.getLatLngs()[0][0].lng, polygon.getLatLngs()[0][0].lat, ]); const poly = turf.polygon(coordinates); var inside = turf.inside(turf.point([lng, lat]), poly); if (inside) { return new LatLng(lat, lng); } else { return randomPointInPoly(polygon); } } export function polygonArea(polygon: Polygon) { var poly = polygon.toGeoJSON(); return turf.area(poly); } export function randomUnitBlueprint( unitDatabase: UnitDatabase, options: { type?: string; role?: string; ranges?: string[]; eras?: string[]; coalition?: string; } ) { /* Start from all the unit blueprints in the database */ var unitBlueprints = Object.values(unitDatabase.getBlueprints()); /* If a specific type or role is provided, use only the blueprints of that type or role */ if (options.type && options.role) { console.error( "Can't create random unit if both type and role are provided. Either create by type or by role." ); return null; } if (options.type) { unitBlueprints = unitDatabase.getByType(options.type); } else if (options.role) { unitBlueprints = unitDatabase.getByType(options.role); } /* Keep only the units that have a range included in the requested values */ if (options.ranges) { unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { var rangeType = ""; var range = unitBlueprint.acquisitionRange; if (range !== undefined) { if (range >= 0 && range < 10000) rangeType = "Short range"; else if (range >= 10000 && range < 100000) rangeType = "Medium range"; else if (range >= 100000 && range < 999999) rangeType = "Long range"; } return options.ranges?.includes(rangeType); }); } /* Keep only the units that have an era included in the requested values */ if (options.eras) { unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { return unitBlueprint.era ? options.eras?.includes(unitBlueprint.era) : true; }); } /* Keep only the units that have the correct coalition, if selected */ if (options.coalition) { unitBlueprints = unitBlueprints.filter((unitBlueprint: UnitBlueprint) => { return unitBlueprint.coalition && unitBlueprint.coalition !== "" ? options.coalition === unitBlueprint.coalition : true; }); } var index = Math.floor(Math.random() * unitBlueprints.length); return unitBlueprints[index]; } export function getMarkerCategoryByName(name: string) { if (aircraftDatabase.getByName(name) != null) return "aircraft"; else if (helicopterDatabase.getByName(name) != null) return "helicopter"; else if (groundUnitDatabase.getByName(name) != null) { var type = groundUnitDatabase.getByName(name)?.type ?? ""; if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) return "groundunit-sam"; else return "groundunit-other"; } else if (navyUnitDatabase.getByName(name) != null) return "navyunit"; else return "aircraft"; // TODO add other unit types } export function getUnitDatabaseByCategory(category: string) { if (category.toLowerCase() == "aircraft") return aircraftDatabase; else if (category.toLowerCase() == "helicopter") return helicopterDatabase; else if (category.toLowerCase().includes("groundunit")) return groundUnitDatabase; else if (category.toLowerCase().includes("navyunit")) return navyUnitDatabase; else return null; } export function getUnitCategoryByBlueprint(blueprint: UnitBlueprint) { for (let database of [ getApp()?.getAircraftDatabase(), getApp()?.getHelicopterDatabase(), getApp()?.getGroundUnitDatabase(), getApp()?.getNavyUnitDatabase(), ]) { if (blueprint.name in database.blueprints) return database.getCategory(); } return "unknown"; } export function enumToState(state: number) { if (state < states.length) return states[state]; else return states[0]; } export function enumToROE(ROE: number) { if (ROE < ROEs.length) return ROEs[ROE]; else return ROEs[0]; } export function enumToReactionToThreat(reactionToThreat: number) { if (reactionToThreat < reactionsToThreat.length) return reactionsToThreat[reactionToThreat]; else return reactionsToThreat[0]; } export function enumToEmissioNCountermeasure(emissionCountermeasure: number) { if (emissionCountermeasure < emissionsCountermeasures.length) return emissionsCountermeasures[emissionCountermeasure]; else return emissionsCountermeasures[0]; } export function enumToCoalition(coalitionID: number) { switch (coalitionID) { case 0: return "neutral"; case 1: return "red"; case 2: return "blue"; } return ""; } export function coalitionToEnum(coalition: string) { switch (coalition) { case "neutral": return 0; case "red": return 1; case "blue": return 2; } return 0; } export function convertDateAndTimeToDate(dateAndTime: DateAndTime) { const date = dateAndTime.date; const time = dateAndTime.time; if (!date) { return new Date(); } let year = date.Year; let month = date.Month - 1; if (month < 0) { month = 11; year--; } return new Date(year, month, date.Day, time.h, time.m, time.s); } export function getGroundElevation(latlng: LatLng, callback: CallableFunction) { /* Get the ground elevation from the server endpoint */ const xhr = new XMLHttpRequest(); xhr.open("GET", `api/elevation/${latlng.lat}/${latlng.lng}`, true); xhr.timeout = 500; // ms xhr.responseType = "json"; xhr.onload = () => { var status = xhr?.status; if (status === 200) { callback(xhr.response); } }; xhr.send(); } export function getWikipediaEntry(search: string, callback: CallableFunction) { /* Get the ground elevation from the server endpoint */ const xhr = new XMLHttpRequest(); xhr.open( "GET", `https://en.wikipedia.org/w/api.php?format=json&action=query&prop=extracts|pageimages&exintro&explaintext&generator=search&gsrsearch=intitle:${search}&gsrlimit=1&redirects=1&origin=*`, true ); xhr.timeout = 500; // ms xhr.responseType = "json"; xhr.onload = () => { var status = xhr?.status; if (status === 200) { callback(xhr.response); } }; xhr.send(); } export function getFunctionArguments(func) { var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm; var ARGUMENT_NAMES = /([^\s,]+)/g; var fnStr = func.toString().replace(STRIP_COMMENTS, ""); var result = fnStr .slice(fnStr.indexOf("(") + 1, fnStr.indexOf(")")) .match(ARGUMENT_NAMES); if (result === null) result = []; return result; } export function filterBlueprintsByLabel( blueprints: { [key: string]: UnitBlueprint }, filterString: string ) { var filteredBlueprints: { [key: string]: UnitBlueprint } = {}; if (blueprints) { Object.entries(blueprints).forEach(([key, value]) => { if ( value.enabled && (filterString === "" || value.label.includes(filterString)) ) filteredBlueprints[key] = value; }); } return filteredBlueprints; } export function getUnitsByLabel(filterString: string) { /* Filter aircrafts, helicopters, and navyunits */ const filteredAircraft = filterBlueprintsByLabel( getApp()?.getAircraftDatabase()?.blueprints, filterString ); const filteredHelicopters = filterBlueprintsByLabel( getApp()?.getHelicopterDatabase()?.blueprints, filterString ); const filteredNavyUnits = filterBlueprintsByLabel( getApp()?.getNavyUnitDatabase()?.blueprints, filterString ); /* Split ground units between air defence and all others */ var filteredAirDefense: { [key: string]: UnitBlueprint } = {}; var filteredGroundUnits: { [key: string]: UnitBlueprint } = {}; Object.keys(getApp()?.getGroundUnitDatabase()?.blueprints ?? {}).forEach( (key) => { var blueprint = getApp()?.getGroundUnitDatabase()?.blueprints[key]; var type = blueprint.label; if (/\bAAA|SAM\b/.test(type) || /\bmanpad|stinger\b/i.test(type)) { filteredAirDefense[key] = blueprint; } else { filteredGroundUnits[key] = blueprint; } } ); filteredAirDefense = filterBlueprintsByLabel(filteredAirDefense, filterString); filteredGroundUnits = filterBlueprintsByLabel(filteredGroundUnits, filterString); return [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] }