diff --git a/client/demo.js b/client/demo.js
index b26fe651..1a4bf887 100644
--- a/client/demo.js
+++ b/client/demo.js
@@ -634,6 +634,9 @@ class DemoDataGenerator {
units(req, res){
var ret = this.demoUnits;
+ for (let ID in this.demoUnits["units"]){
+ this.demoUnits["units"][ID].flightData.latitude += 0.00;
+ }
ret.time = Date.now();
res.send(JSON.stringify(ret));
};
diff --git a/client/public/images/icons/arrows-to-eye-solid.svg b/client/public/images/icons/arrows-to-eye-solid.svg
new file mode 100644
index 00000000..11815283
--- /dev/null
+++ b/client/public/images/icons/arrows-to-eye-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/images/icons/echelon-lh.svg b/client/public/images/icons/echelon-lh.svg
new file mode 100644
index 00000000..9359bfba
--- /dev/null
+++ b/client/public/images/icons/echelon-lh.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/client/public/images/icons/echelon-rh.svg b/client/public/images/icons/echelon-rh.svg
new file mode 100644
index 00000000..2da057de
--- /dev/null
+++ b/client/public/images/icons/echelon-rh.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/client/public/images/icons/echelon.svg b/client/public/images/icons/echelon.svg
new file mode 100644
index 00000000..21bb81bb
--- /dev/null
+++ b/client/public/images/icons/echelon.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/client/public/images/icons/follow.svg b/client/public/images/icons/follow.svg
index 2ec555ac..b3296b24 100644
--- a/client/public/images/icons/follow.svg
+++ b/client/public/images/icons/follow.svg
@@ -7,14 +7,14 @@
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
- sodipodi:docname="follow.svg"
- id="svg14"
- version="1.1"
- fill="none"
- viewBox="0 0 16 16"
+ width="16"
height="16"
- width="16">
+ viewBox="0 0 16 16"
+ fill="none"
+ version="1.1"
+ id="svg14"
+ sodipodi:docname="follow.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
@@ -30,28 +30,29 @@
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1017"
+ id="namedview16"
+ showgrid="false"
+ inkscape:zoom="12.142857"
+ inkscape:cx="22.433158"
+ inkscape:cy="14.663012"
+ inkscape:window-x="1912"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg14" />
+ id="path2"
+ style="fill:#000000;fill-opacity:1;stroke-width:1.38669" />
diff --git a/client/public/images/icons/front.svg b/client/public/images/icons/front.svg
new file mode 100644
index 00000000..cfb0f29a
--- /dev/null
+++ b/client/public/images/icons/front.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/client/public/images/icons/line-abreast.svg b/client/public/images/icons/line-abreast.svg
new file mode 100644
index 00000000..c146a848
--- /dev/null
+++ b/client/public/images/icons/line-abreast.svg
@@ -0,0 +1,65 @@
+
+
diff --git a/client/public/images/icons/trail.svg b/client/public/images/icons/trail.svg
new file mode 100644
index 00000000..fb23fd08
--- /dev/null
+++ b/client/public/images/icons/trail.svg
@@ -0,0 +1,63 @@
+
+
diff --git a/client/public/images/reference-system-test.svg b/client/public/images/reference-system-test.svg
new file mode 100644
index 00000000..ff6f152a
--- /dev/null
+++ b/client/public/images/reference-system-test.svg
@@ -0,0 +1,370 @@
+
+
diff --git a/client/public/images/reference-system.svg b/client/public/images/reference-system.svg
new file mode 100644
index 00000000..cd71d782
--- /dev/null
+++ b/client/public/images/reference-system.svg
@@ -0,0 +1,218 @@
+
+
diff --git a/client/public/stylesheets/contextmenus.css b/client/public/stylesheets/contextmenus.css
index 78b9d3e7..7443e972 100644
--- a/client/public/stylesheets/contextmenus.css
+++ b/client/public/stylesheets/contextmenus.css
@@ -235,6 +235,10 @@
width: 16px;
}
+#center-map::before {
+ content: url( /images/icons/arrows-to-eye-solid.svg );
+}
+
#refuel::before {
content: url( /images/icons/fuel.svg );
}
@@ -247,6 +251,68 @@
content: url( /images/icons/follow.svg );
}
+#trail::before {
+ content: url( /images/icons/trail.svg );
+}
+
+#echelon-lh::before {
+ content: url( /images/icons/echelon-lh.svg );
+}
+
+#echelon-rh::before {
+ content: url( /images/icons/echelon-rh.svg );
+}
+
+#line-abreast::before {
+ content: url( /images/icons/line-abreast.svg );
+}
+
+#front::before {
+ content: url( /images/icons/front.svg );
+}
+
+#custom::before {
+ content: url( /images/icons/custom.svg );
+}
+
+#custom-formation-dialog {
+ width: 250px;
+}
+
+#custom-formation-dialog > .ol-dialog-content {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ row-gap: 10px;
+ align-items: center;
+}
+
+#custom-formation-dialog > .ol-dialog-content > .ol-group {
+ width: 100%;
+ justify-content: space-between;
+}
+
+#reference-system {
+ content: url( /images/reference-system.svg );
+ display: inline-block;
+ filter: invert(100%);
+ width: 50px;
+ transform: translate(-50%, -50%);
+ position: absolute;
+}
+
+.formation-position-clock {
+ transform: translate(-50%, -50%);
+ display: flex;
+ position: absolute;
+ align-items: center;
+ justify-content: center;
+ height: 20px;
+ width: 20px;
+}
+
/* Airbase context menu */
#airbase-contextmenu {
display: flex;
diff --git a/client/src/controls/unitcontextmenu.ts b/client/src/controls/unitcontextmenu.ts
index 51bde81b..ea6a82cf 100644
--- a/client/src/controls/unitcontextmenu.ts
+++ b/client/src/controls/unitcontextmenu.ts
@@ -1,8 +1,21 @@
import { ContextMenu } from "./contextmenu";
export class UnitContextMenu extends ContextMenu {
+ #callback: CallableFunction | null = null;
+
constructor(id: string) {
super(id);
+
+ document.addEventListener("applyCustomFormation", () => {
+ var dialog = document.getElementById("custom-formation-dialog");
+ if (dialog)
+ {
+ dialog.classList.add("hide");
+ }
+
+ if (this.#callback)
+ this.#callback()
+ })
}
setOptions(options: {[key: string]: string}, callback: CallableFunction)
diff --git a/client/src/map/map.ts b/client/src/map/map.ts
index e76021ee..3509221b 100644
--- a/client/src/map/map.ts
+++ b/client/src/map/map.ts
@@ -19,6 +19,7 @@ export class Map extends L.Map {
#preventLeftClick: boolean = false;
#leftClickTimer: number = 0;
#lastMousePosition: L.Point = new L.Point(0, 0);
+ #centerUnit: Unit | null = null;
#mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu");
#unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu");
@@ -29,7 +30,7 @@ export class Map extends L.Map {
constructor(ID: string) {
/* Init the leaflet map */
//@ts-ignore
- super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true });
+ super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: false });
this.setView([37.23, -115.8], 12);
this.setLayer("ArcGIS Satellite");
@@ -39,7 +40,9 @@ export class Map extends L.Map {
/* Register event handles */
this.on("click", (e: any) => this.#onClick(e));
- this.on("dblclick", (e: any) => this.#onDoubleClick(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));
@@ -56,6 +59,11 @@ export class Map extends L.Map {
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())
}
@@ -195,6 +203,18 @@ export class Map extends L.Map {
//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;
+ }
+ }
+
/* Event handlers */
#onClick(e: any) {
if (!this.#preventLeftClick) {
@@ -256,4 +276,16 @@ export class Map extends L.Map {
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});
+ }
}
diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts
index ba30d161..b282b4a9 100644
--- a/client/src/units/unit.ts
+++ b/client/src/units/unit.ts
@@ -142,7 +142,6 @@ export class Unit extends Marker {
}
setData(data: UpdateData) {
- document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this }));
var updateMarker = false;
if ((data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude))
@@ -212,6 +211,8 @@ export class Unit extends Marker {
}
else
this.#clearPath();
+
+ document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this }));
}
getData() {
@@ -413,11 +414,14 @@ export class Unit extends Marker {
#onContextMenu(e: any) {
var options: {[key: string]: string} = {};
+
+ options["Center"] = `Center map
`;
+
if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().includes(this)))
{
options = {
'Attack': `Attack
`,
- 'Follow': `Follow
`
+ 'Follow': `Follow
`,
}
}
else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0)
@@ -433,18 +437,50 @@ export class Unit extends Marker {
getMap().showUnitContextMenu(e);
getMap().getUnitContextMenu().setOptions(options, (option: string) => {
getMap().hideUnitContextMenu();
- this.#executeAction(option);
+ this.#executeAction(e, option);
});
}
}
- #executeAction(action: string) {
+ #executeAction(e: any, action: string) {
+ if (action === "Center")
+ getMap().centerOnUnit(this.ID);
if (action === "Attack")
getUnitsManager().selectedUnitsAttackUnit(this.ID);
- if (action === "Refuel")
+ else if (action === "Refuel")
getUnitsManager().selectedUnitsRefuel();
- if (action === "Follow")
+ else if (action === "Follow")
+ this.#showFollowOptions(e);
+ }
+
+ #showFollowOptions(e: any) {
+ var options: {[key: string]: string} = {};
+
+ options = {
+ 'Trail': `Trail
`,
+ 'Echelon (LH)': `Echelon (LH)
`,
+ 'Echelon (RH)': `Echelon (RH)
`,
+ 'Line abreast': `Line abreast
`,
+ 'Front': `In front
`,
+ 'Custom': `Custom
`
+ }
+
+ getMap().getUnitContextMenu().setOptions(options, (option: string) => {
+ getMap().hideUnitContextMenu();
+ this.#applyFollowOptions(option);
+ });
+ getMap().showUnitContextMenu(e);
+ }
+
+ #applyFollowOptions(action: string)
+ {
+ if (action === "Custom")
+ {
+ document.getElementById("custom-formation-dialog")?.classList.remove("hide");
+ }
+ else {
getUnitsManager().selectedUnitsFollowUnit(this.ID);
+ }
}
#updateMarker() {
diff --git a/client/views/contextmenus.ejs b/client/views/contextmenus.ejs
index b386ec9a..6c3e0335 100644
--- a/client/views/contextmenus.ejs
+++ b/client/views/contextmenus.ejs
@@ -79,6 +79,44 @@
+
+