import * as L from "leaflet" import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map"; import { getUnitsManager } from ".."; import { BoxSelect } from "./boxselect"; import { MapContextMenu } from "../controls/mapcontextmenu"; import { UnitContextMenu } from "../controls/unitcontextmenu"; import { AirbaseContextMenu } from "../controls/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; import { Airbase } from "../missionhandler/airbase"; import { Unit } from "../units/unit"; // TODO a bit of a hack, this module is provided as pure javascript only require("../../node_modules/leaflet.nauticscale/dist/leaflet.nauticscale.js") export const IDLE = "IDLE"; export const MOVE_UNIT = "MOVE_UNIT"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); export class ClickableMiniMap extends MiniMap { constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) { super(layer, options); } getMap() { //@ts-ignore needed to access not exported member. A bit of a hack, required to access click events return this._miniMap; } } export class Map extends L.Map { #state: string; #layer: L.TileLayer | null = null; #preventLeftClick: boolean = false; #leftClickTimer: number = 0; #deafultPanDelta: number = 100; #panInterval: number | null = null; #panDeltaX: number; #panDeltaY: number; #lastMousePosition: L.Point = new L.Point(0, 0); #centerUnit: Unit | null = null; #miniMap: ClickableMiniMap | null = null; #miniMapLayerGroup: L.LayerGroup; #mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu"); #unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu"); #airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu"); #mapSourceDropdown: Dropdown; constructor(ID: string) { /* Init the leaflet map */ //@ts-ignore super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); this.setLayer("ArcGIS Satellite"); /* Minimap */ /* Draw the limits of the maps in the minimap*/ var latlngs = [[ // NTTR new L.LatLng(39.7982463, -119.985425), new L.LatLng(34.4037128, -119.7806729), new L.LatLng(34.3483316, -112.4529351), new L.LatLng(39.7372411, -112.1130805), new L.LatLng(39.7982463, -119.985425) ], [ // Syria new L.LatLng(37.3630556, 29.2686111), new L.LatLng(31.8472222, 29.8975), new L.LatLng(32.1358333, 42.1502778), new L.LatLng(37.7177778, 42.3716667), new L.LatLng(37.3630556, 29.2686111) ], [ // Caucasus new L.LatLng(39.6170191, 27.634935), new L.LatLng(38.8735863, 47.1423108), new L.LatLng(47.3907982, 49.3101946), new L.LatLng(48.3955879, 26.7753625), new L.LatLng(39.6170191, 27.634935) ], [ // Persian Gulf new L.LatLng(32.9355285, 46.5623682), new L.LatLng(21.729393, 47.572675), new L.LatLng(21.8501348, 63.9734737), new L.LatLng(33.131584, 64.7313594), new L.LatLng(32.9355285, 46.5623682) ], [ // Marianas new L.LatLng(22.09, 135.0572222), new L.LatLng(10.5777778, 135.7477778), new L.LatLng(10.7725, 149.3918333), new L.LatLng(22.5127778, 149.5427778), new L.LatLng(22.09, 135.0572222) ] ]; var minimapLayer = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); var miniMapPolyline = new L.Polyline(latlngs, { color: '#202831' }); miniMapPolyline.addTo(this.#miniMapLayerGroup); /* Scale */ //@ts-ignore TODO more hacking because the module is provided as a pure javascript module only L.control.scalenautic({ position: "topright", maxWidth: 300, nautic: true, metric: true, imperial: false }).addTo(this); /* Init the state machine */ this.#state = IDLE; /* Register event handles */ this.on("click", (e: any) => this.#onClick(e)); this.on("dblclick", (e: any) => this.#onDoubleClick(e)); this.on("zoomstart", (e: any) => this.#onZoom(e)); this.on("drag", (e: any) => this.centerOnUnit(null)); this.on("contextmenu", (e: any) => this.#onContextMenu(e)); this.on('selectionend', (e: any) => this.#onSelectionEnd(e)); this.on('mousedown', (e: any) => this.#onMouseDown(e)); this.on('mouseup', (e: any) => this.#onMouseUp(e)); this.on('mousemove', (e: any) => this.#onMouseMove(e)); document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { ev.detail._element.classList.toggle("off"); document.body.toggleAttribute("data-hide-" + ev.detail.coalition); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { document.body.toggleAttribute("data-hide-" + ev.detail.category); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); document.addEventListener("unitUpdated", (ev: CustomEvent) => { if (this.#centerUnit != null && ev.detail == this.#centerUnit) this.#panToUnit(this.#centerUnit); }); this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName), this.getLayers()) this.#panDeltaX = 0; this.#panDeltaY = 0; this.#panInterval = window.setInterval(() => { this.panBy(new L.Point(this.#panDeltaX, this.#panDeltaY)); }, 20); } setLayer(layerName: string) { if (this.#layer != null) { this.removeLayer(this.#layer) } if (layerName == "ArcGIS Satellite") { this.#layer = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" }); } else if (layerName == "USGS Topo") { this.#layer = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', { maxZoom: 20, attribution: 'Tiles courtesy of the U.S. Geological Survey' }); } else if (layerName == "OpenStreetMap Mapnik") { this.#layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }); } else if (layerName == "OPENVKarte") { this.#layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' }); } else if (layerName == "Esri.DeLorme") { this.#layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', minZoom: 1, maxZoom: 11 }); } else if (layerName == "CyclOSM") { this.#layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { maxZoom: 20, attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' }); } this.#layer?.addTo(this); } getLayers() { return ["ArcGIS Satellite", "USGS Topo", "OpenStreetMap Mapnik", "OPENVKarte", "Esri.DeLorme", "CyclOSM"] } /* State machine */ setState(state: string) { this.#state = state; if (this.#state === IDLE) { L.DomUtil.removeClass(this.getContainer(), 'crosshair-cursor-enabled'); } else if (this.#state === MOVE_UNIT) { L.DomUtil.addClass(this.getContainer(), 'crosshair-cursor-enabled'); } document.dispatchEvent(new CustomEvent("mapStateChanged")); } getState() { return this.#state; } /* Context Menus */ hideAllContextMenus() { this.hideMapContextMenu(); this.hideUnitContextMenu(); this.hideAirbaseContextMenu(); } showMapContextMenu(e: any) { this.hideAllContextMenus(); var x = e.originalEvent.x; var y = e.originalEvent.y; this.#mapContextMenu.show(x, y, e.latlng); document.dispatchEvent(new CustomEvent("mapContextMenu")); } hideMapContextMenu() { this.#mapContextMenu.hide(); document.dispatchEvent(new CustomEvent("mapContextMenu")); } getMapContextMenu() { return this.#mapContextMenu; } showUnitContextMenu(e: any) { this.hideAllContextMenus(); var x = e.originalEvent.x; var y = e.originalEvent.y; this.#unitContextMenu.show(x, y, e.latlng); } getUnitContextMenu() { return this.#unitContextMenu; } hideUnitContextMenu() { this.#unitContextMenu.hide(); } showAirbaseContextMenu(e: any, airbase: Airbase) { this.hideAllContextMenus(); var x = e.originalEvent.x; var y = e.originalEvent.y; this.#airbaseContextMenu.show(x, y, e.latlng); this.#airbaseContextMenu.setAirbase(airbase); } getAirbaseContextMenu() { return this.#airbaseContextMenu; } hideAirbaseContextMenu() { this.#airbaseContextMenu.hide(); } /* Mouse coordinates */ getMousePosition() { return this.#lastMousePosition; } getMouseCoordinates() { return this.containerPointToLatLng(this.#lastMousePosition); } /* Spawn from air base */ spawnFromAirbase(e: any) { //this.#aircraftSpawnMenu(e); } centerOnUnit(ID: number | null) { if (ID != null) { this.options.scrollWheelZoom = 'center'; this.#centerUnit = getUnitsManager().getUnitByID(ID); } else { this.options.scrollWheelZoom = undefined; this.#centerUnit = null; } } setTheatre(theatre: string) { var bounds = new L.LatLngBounds([-90, -180], [90, 180]); var miniMapZoom = 5; if (theatre == "Syria") bounds = new L.LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]); else if (theatre == "MarianaIslands") bounds = new L.LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]); else if (theatre == "Nevada") bounds = new L.LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]) else if (theatre == "PersianGulf") bounds = new L.LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]) else if (theatre == "Falklands") { // TODO } else if (theatre == "Caucasus") { bounds = new L.LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]) miniMapZoom = 4; } this.setView(bounds.getCenter(), 8); this.setMaxBounds(bounds); if (this.#miniMap) this.#miniMap.remove(); //@ts-ignore // Needed because some of the inputs are wrong in the original module interface this.#miniMap = new ClickableMiniMap(this.#miniMapLayerGroup, { position: "topright", width: 192 * 1.5, height: 108 * 1.5, zoomLevelFixed: miniMapZoom, centerFixed: bounds.getCenter() }).addTo(this); this.#miniMap.disableInteractivity(); this.#miniMap.getMap().on("click", (e: any) => { if (this.#miniMap) this.setView(e.latlng); }) } getMiniMapLayerGroup() { return this.#miniMapLayerGroup; } handleMapPanning(e: any) { if (e.type === "keyup"){ switch (e.code) { case "KeyA": case "KeyD": case "ArrowLeft": case "ArrowRight": this.#panDeltaX = 0; break; case "KeyW": case "KeyS": case "ArrowUp": case "ArrowDown": this.#panDeltaY = 0 break; } } else { switch (e.code) { case 'KeyD': case 'ArrowRight': this.#panDeltaX = this.#deafultPanDelta; break; case 'KeyA': case 'ArrowLeft': this.#panDeltaX = -this.#deafultPanDelta; break; case 'KeyW': case 'ArrowUp': this.#panDeltaY = -this.#deafultPanDelta; break; case 'KeyS': case 'ArrowDown': this.#panDeltaY = this.#deafultPanDelta; break; } } } /* Event handlers */ #onClick(e: any) { if (!this.#preventLeftClick) { this.hideAllContextMenus(); if (this.#state === IDLE) { } else if (this.#state === MOVE_UNIT) { this.setState(IDLE); getUnitsManager().deselectAllUnits(); } } } #onDoubleClick(e: any) { } #onContextMenu(e: any) { this.hideMapContextMenu(); if (this.#state === IDLE) { if (this.#state == IDLE) { this.showMapContextMenu(e); } } else if (this.#state === MOVE_UNIT) { if (!e.originalEvent.ctrlKey) { getUnitsManager().selectedUnitsClearDestinations(); } getUnitsManager().selectedUnitsAddDestination(e.latlng) } } #onSelectionEnd(e: any) { clearTimeout(this.#leftClickTimer); this.#preventLeftClick = true; this.#leftClickTimer = window.setTimeout(() => { this.#preventLeftClick = false; }, 200); getUnitsManager().selectFromBounds(e.selectionBounds); } #onMouseDown(e: any) { this.hideAllContextMenus(); } #onMouseUp(e: any) { } #onMouseMove(e: any) { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; } #onZoom(e: any) { if (this.#centerUnit != null) this.#panToUnit(this.#centerUnit); } #panToUnit(unit: Unit) { var unitPosition = new L.LatLng(unit.getFlightData().latitude, unit.getFlightData().longitude); this.setView(unitPosition, this.getZoom(), { animate: false }); } }