feat: completed formation menu

This commit is contained in:
Davide Passoni 2025-01-20 15:07:58 +01:00
parent 9bd206a750
commit 5b6f58a38e
7 changed files with 278 additions and 199 deletions

View File

@ -53,25 +53,46 @@ export const IRST = 8;
export const RWR = 16;
export const DLINK = 32;
export enum UnitState {
NONE = "none",
IDLE = "idle",
REACH_DESTINATION = "reach-destination",
ATTACK = "attack",
FOLLOW = "follow",
LAND = "land",
REFUEL = "refuel",
AWACS = "AWACS",
TANKER = "tanker",
BOMB_POINT = "bomb-point",
CARPET_BOMB = "carpet-bomb",
BOMB_BUILDING = "bomb-building",
FIRE_AT_AREA = "fire-at-area",
SIMULATE_FIRE_FIGHT = "simulate-fire-fight",
SCENIC_AAA = "scenic-aaa",
MISS_ON_PURPOSE = "miss-on-purpose",
LAND_AT_POINT = "land-at-point",
}
export const states: string[] = [
"none",
"idle",
"reach-destination",
"attack",
"follow",
"land",
"refuel",
"AWACS",
"tanker",
"bomb-point",
"carpet-bomb",
"bomb-building",
"fire-at-area",
"simulate-fire-fight",
"scenic-aaa",
"miss-on-purpose",
"land-at-point",
UnitState.NONE,
UnitState.IDLE,
UnitState.REACH_DESTINATION,
UnitState.ATTACK,
UnitState.FOLLOW,
UnitState.LAND,
UnitState.REFUEL,
UnitState.AWACS,
UnitState.TANKER,
UnitState.BOMB_POINT,
UnitState.CARPET_BOMB,
UnitState.BOMB_BUILDING,
UnitState.FIRE_AT_AREA,
UnitState.SIMULATE_FIRE_FIGHT,
UnitState.SCENIC_AAA,
UnitState.MISS_ON_PURPOSE,
UnitState.LAND_AT_POINT,
];
export const ROEs: string[] = ["free", "designated", "", "return", "hold"];
export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"];
export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"];
@ -284,7 +305,6 @@ export const formationTypes = {
custom: "Custom",
};
export enum OlympusState {
NOT_INITIALIZED = "Not initialized",
SERVER = "Server",
@ -302,7 +322,7 @@ export enum OlympusState {
AIRBASE = "Airbase",
GAME_MASTER = "Game master",
IMPORT_EXPORT = "Import/export",
WARNING = "Warning modal"
WARNING = "Warning modal",
}
export const NO_SUBSTATE = "No substate";
@ -351,16 +371,15 @@ export enum OptionsSubstate {
export enum ImportExportSubstate {
NO_SUBSTATE = "No substate",
IMPORT = "IMPORT",
EXPORT = "EXPORT"
EXPORT = "EXPORT",
}
export enum WarningSubstate {
NO_SUBSTATE = "No substate",
NOT_CHROME = "Not chrome",
NOT_SECURE = "Not secure"
NOT_SECURE = "Not secure",
}
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
@ -389,7 +408,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
AWACSMode: false,
AWACSCoalition: "blue",
hideChromeWarning: false,
hideSecureWarning: false
hideSecureWarning: false,
};
export const MAP_HIDDEN_TYPES_DEFAULTS = {
@ -542,7 +561,7 @@ export namespace ContextActions {
.getUnitsManager()
.addDestination(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
},
{ type: ContextActionType.MOVE, code: null}
{ type: ContextActionType.MOVE, code: null }
);
export const DELETE = new ContextAction(

View File

@ -447,22 +447,25 @@ export function deepCopyTable(table) {
}
export function computeStandardFormationOffset(formation, idx) {
let offset = { x: 0, y: 0 };
let offset = { x: 0, y: 0, z: 0 };
if (formation === "trail") {
offset.y = 50 * idx;
offset.y = 75 * idx;
offset.x = 0;
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "echelon-lh" || formation == "custom" /* default fallback if needed */) {
offset.y = 50 * idx;
offset.x = -50 * idx;
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "echelon-rh") {
offset.y = 50 * idx;
offset.x = 50 * idx;
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1;
} else if (formation === "line-abreast-lh") {
offset.y = 0;
offset.x = -50 * idx;
offset.x = -75 * idx;
} else if (formation === "line-abreast-rh") {
offset.y = 0;
offset.x = 50 * idx;
offset.x = 75 * idx;
} else if (formation === "front") {
offset.y = -100 * idx;
offset.x = 0;
@ -475,7 +478,8 @@ export function computeStandardFormationOffset(formation, idx) {
for (let i = 0; i < idx; i++) {
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
offset = { x: xl * 50, y: yl * 50 };
offset = { x: xl * 50, y: yl * 50, z: 0 };
offset.z = -Math.sqrt(offset.x * offset.x + offset.y * offset.y) * 0.1
if (yr == 0) {
layer++;
xr = 0;
@ -494,3 +498,31 @@ export function computeStandardFormationOffset(formation, idx) {
}
return offset;
}
export function nearestNiceNumber(number) {
let niceNumbers = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000];
let niceNumber = niceNumbers[0];
for (let i = 0; i < niceNumbers.length; i++) {
if (niceNumbers[i] >= number) {
niceNumber = niceNumbers[i];
break;
}
}
return niceNumber;
}
export function roundToNearestFive(number) {
return Math.round(number / 5) * 5;
}
export function toDCSFormationOffset(offset: {x: number, y: number, z: number}) {
// X: front-rear, positive front
// Y: top-bottom, positive top
// Z: left-right, positive right
return { x: -offset.y, y: offset.z, z: offset.x };
}
export function fromDCSFormationOffset(offset: {x: number, y: number, z: number}) {
return { x: offset.z, y: -offset.x, z: offset.y };
}

View File

@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
export function Draggable(props: {
position: { x: number; y: number };
position: { x: number; y: number, z: number };
children: JSX.Element | JSX.Element[];
disabled: boolean;
onPositionChange: (position: { x: number; y: number }) => void;
onPositionChange: (position: { x: number; y: number, z: number }) => void;
}) {
const [dragging, setDragging] = useState(false);
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
@ -15,7 +15,7 @@ export function Draggable(props: {
e.stopPropagation();
e.preventDefault();
setRefPosition({ x: e.clientX, y: e.clientY });
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y });
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y, z: props.position.z });
}
},
[dragging, refPosition]

View File

@ -1,32 +1,60 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Draggable } from "./draggable";
import { Unit } from "../../../unit/unit";
import { FaArrowDown, FaArrowUp } from "react-icons/fa";
import { nearestNiceNumber } from "../../../other/utils";
export function DraggableSilhouette(props: {
position: { x: number; y: number };
position: { x: number; y: number; z: number };
unit: Unit;
zoom: number;
scale: number;
disabled: boolean;
angle: number;
onPositionChange: (position: { x: number; y: number }) => void;
showVerticalOffset: boolean;
onPositionChange: (position: { x: number; y: number; z: number }) => void;
src?: string;
}) {
const imgHeight = Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled ? 1 : props.zoom));
return (
<Draggable position={props.position} onPositionChange={props.onPositionChange} disabled={props.disabled}>
<img
data-disabled = {props.disabled}
data-disabled={props.disabled}
className={`
align-center opacity-80 invert
data-[disabled=false]:cursor-move
`}
src={props.src ?? `./images/units/${props.unit?.getBlueprint()?.filename}`}
style={{
maxWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
minWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
rotate: `${props.disabled? props.angle: 90}deg`,
maxWidth: `${imgHeight}px`,
minWidth: `${imgHeight}px`,
rotate: `${props.disabled ? props.angle : 90}deg`,
}}
></img>
onWheel={(e) => {
e.stopPropagation();
let delta = nearestNiceNumber(Math.max(1, 0.1 * Math.abs(props.position.z)));
let newZ = props.position.z + (e.deltaY > 0 ? -delta : delta);
newZ = Math.round(newZ / delta) * delta;
props.onPositionChange({ x: props.position.x, y: props.position.y, z: newZ });
}}
/>
{props.showVerticalOffset ? (
<div className={`absolute flex w-full justify-center text-gray-400`}
style={{top: `calc(50% + ${imgHeight / 2}px)`}}>
<div className="flex gap-1">
{Math.round(props.position.z) > 0 ? (
<FaArrowUp className={`my-auto text-xs`} />
) : Math.round(props.position.z) < 0 ? (
<FaArrowDown className={`my-auto text-xs`} />
) : (
<></>
)}
{Math.round(Math.abs(props.position.z))} <div>ft</div>
</div>
</div>
) : (
<></>
)}
</Draggable>
);
}

View File

@ -1,19 +1,21 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Unit } from "../../../unit/unit";
import { DraggableSilhouette } from "./draggablesilhouette";
import { FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt } from "react-icons/fa";
import { FaArrowDown, FaArrowUp, FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt, FaQuestionCircle } from "react-icons/fa";
import { OlToggle } from "../../components/oltoggle";
const FT_TO_PX = 1;
export function FormationCanvas(props: {
units: Unit[];
unitPositions: { x: number; y: number }[];
setUnitPositions: (positions: { x: number; y: number }[]) => void;
unitPositions: { x: number; y: number; z: number }[];
setUnitPositions: (positions: { x: number; y: number; z: number }[]) => void;
}) {
const [dragging, setDragging] = useState(false);
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
const [dragDelta, setDragDelta] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [showVerticalOffset, setShowVerticalOffset] = useState(true);
/* Init references and hooks */
const containerRef = useRef(null);
@ -79,55 +81,118 @@ export function FormationCanvas(props: {
return (
<>
<div className="flex w-fit gap-1 p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 1.1,
y: position.y * 1.1,
};
})
);
}}
className={`
rounded-lg p-2 text-md flex content-center justify-center gap-2
bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaExpandArrowsAlt className="my-auto" /> <div> Loosen </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 0.9,
y: position.y * 0.9,
};
})
);
}}
className={`
rounded-lg p-2 text-md flex content-center justify-center gap-2
bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaCompressArrowsAlt className="my-auto" /> <div>Tighten</div>
</button>
<div className="flex">
<div className="flex w-fit p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 1.1,
y: position.y * 1.1,
z: position.z,
};
})
);
}}
className={`
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
text-white
hover:bg-gray-700
`}
>
<FaExpandArrowsAlt className="my-auto" /> <div> Loose </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position) => {
return {
x: position.x * 0.9,
y: position.y * 0.9,
z: position.z,
};
})
);
}}
className={`
rounded-r-lg px-3 py-2 text-md flex content-center justify-center
gap-2 bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaCompressArrowsAlt className="my-auto" /> <div className="">Tight</div>
</button>
</div>
<div className="flex w-fit p-1">
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position, idx) => {
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
return {
x: position.x,
y: position.y,
z: position.z + 0.1 * distance,
};
})
);
}}
className={`
rounded-l-lg px-3 py-2 text-md flex content-center justify-center
gap-2 border-r-2 border-gray-400 bg-gray-600 font-medium
text-white
hover:bg-gray-700
`}
>
<FaArrowUp className="my-auto" /> <div> Up </div>
</button>
<button
type="button"
onDoubleClick={(e) => {
e.stopPropagation();
}}
onClick={() => {
props.setUnitPositions(
props.unitPositions.map((position, idx) => {
const [dx, dz] = [-(props.unitPositions[idx].y - props.unitPositions[0].y), props.unitPositions[idx].x - props.unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
return {
x: position.x,
y: position.y,
z: position.z - 0.1 * distance,
};
})
);
}}
className={`
rounded-r-lg px-3 py-2 text-md flex content-center justify-center
gap-2 bg-gray-600 font-medium text-white
hover:bg-gray-700
`}
>
<FaArrowDown className="my-auto" /> <div className="">Down</div>
</button>
</div>
</div>
<div className="flex justify-between"><div className="text-white">Show units vertical offset</div> <OlToggle onClick={() => {setShowVerticalOffset(!showVerticalOffset)}} toggled={showVerticalOffset} /></div>
<div
data-dragging={dragging}
className={`
@ -164,8 +229,9 @@ export function FormationCanvas(props: {
(((props.unitPositions[idx].y - props.unitPositions[0].y) * 1) / zoom) * FT_TO_PX +
dragDelta.y +
containerCenter.y,
z: props.unitPositions[idx].z,
}
: { x: 0, y: 0 };
: { x: 0, y: 0, z: 0 };
let disabled = false;
let overflowX = null as null | string;
@ -213,10 +279,13 @@ export function FormationCanvas(props: {
unit={unit}
scale={FT_TO_PX}
disabled={disabled}
onPositionChange={({ x, y }) => {
showVerticalOffset={showVerticalOffset}
onPositionChange={({ x, y, z }) => {
if (idx === 0) return;
props.unitPositions[idx] = {
x: ((x - props.unitPositions[0].x - dragDelta.x - containerCenter.x) * zoom) / FT_TO_PX - props.unitPositions[0].x,
y: ((y - props.unitPositions[0].y - dragDelta.y - containerCenter.y) * zoom) / FT_TO_PX - props.unitPositions[0].y,
z: z,
};
props.setUnitPositions([...props.unitPositions]);
}}
@ -226,12 +295,12 @@ export function FormationCanvas(props: {
);
})}
</div>
{zoom > 3 && (
<div className="absolute bottom-2 left-2 flex gap-2">
<FaExclamationTriangle className={`text-xl text-yellow-400`} />
<div className="text-white">Silhouettes not to scale!</div>
<FaQuestionCircle className={`text-xl text-gray-400`} />
<div className="text-gray-400">Double click to reset view</div>
</div>
)}
<div className="absolute left-0 top-2 m-[-0.75rem] h-0">
<div
className={`
@ -243,13 +312,9 @@ export function FormationCanvas(props: {
}}
>
{referenceDistance === 5280 && <div className="translate-y-[-8px]">1 NM</div>}
{referenceDistance === 5280 * 2 && (
<div
className={`translate-y-[-8px]`}
>
2 NM
</div>
)}
{referenceDistance === 5280 * 2 && <div className={`
translate-y-[-8px]
`}>2 NM</div>}
{referenceDistance < 5280 && <div className="translate-y-[-8px]">{referenceDistance} ft</div>}
</div>
</div>

View File

@ -1,11 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { Menu } from "./components/menu";
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
import { Unit } from "../../unit/unit";
import { OlRangeSlider } from "../components/olrangeslider";
import { FormationCreationRequestEvent } from "../../events";
import { computeStandardFormationOffset } from "../../other/utils";
import { formationTypes } from "../../constants/constants";
import { computeStandardFormationOffset, fromDCSFormationOffset, toDCSFormationOffset } from "../../other/utils";
import { formationTypes, UnitState } from "../../constants/constants";
import { FormationCanvas } from "./components/formationcanvas";
export function FormationMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
@ -14,10 +13,11 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
/* Init state variables */
const [formationType, setFormationType] = useState("echelon-lh");
const [verticalScale, setVerticalScale] = useState(30);
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number }[]);
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number, z: number }[]);
const verticalRatio = (verticalScale - 50) / 50;
useEffect(() => {
setFormationType("echelon-lh");
}, [props.open])
/* Listen for the setting of a new leader and wingmen and check if the formation is too big */
useEffect(() => {
@ -40,6 +40,23 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
}, [formationType]);
useEffect(setStandardFormation, [formationType]);
const setCurrentFormation = useCallback(() => {
if (
wingmen.every((unit: Unit) => {
return unit.getState() === UnitState.FOLLOW && unit.getLeader() === leader;
})
) {
setUnitPositions([
{ x: 0, y: 0, z: 0 },
...wingmen.map((unit, idx) => {
return fromDCSFormationOffset(unit.getFormationOffset());
}),
]);
setFormationType("custom");
}
}, [leader, wingmen]);
useEffect(setCurrentFormation, [leader, wingmen]);
if (leader && unitPositions.length < [leader, ...wingmen].length) {
/* If more units are added to the group keep the existing positions */
setUnitPositions(
@ -120,18 +137,6 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
Load
</button>
</div>
<span className="text-white">Vertical separation</span>
<div className="flex h-fit content-center gap-4">
<span className="ml-auto min-w-16 text-center text-sm text-white">Down</span>
<OlRangeSlider
className="my-auto"
value={verticalScale}
onChange={(ev) => {
setVerticalScale(Number(ev.target.value));
}}
/>
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
</div>
<FormationCanvas
units={leader ? [leader, ...wingmen] : []}
unitPositions={unitPositions}
@ -148,14 +153,13 @@ export function FormationMenu(props: { open: boolean; onClose: () => void; child
.filter((unit) => unit !== null)
.forEach((unit, idx) => {
if (idx != 0) {
const [dx, dz] = [-(unitPositions[idx].y - unitPositions[0].y), unitPositions[idx].x - unitPositions[0].x];
const distance = Math.sqrt(dx ** 2 + dz ** 2);
const offset = {
const [dx, dy] = [unitPositions[idx].x - unitPositions[0].x, unitPositions[idx].y - unitPositions[0].y];
unit.followUnit(leader.ID, toDCSFormationOffset({
x: dx,
y: distance * verticalRatio,
z: dz,
};
unit.followUnit(leader.ID, offset);
y: dy,
z: unitPositions[idx].z,
}));
}
});
}

View File

@ -807,86 +807,17 @@ export class UnitsManager {
* @param formation Optional parameter, defines a predefined formation type. Values are: "trail", "echelon-lh", "echelon-rh", "line-abreast-lh", "line-abreast-rh", "front", "diamond"
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
*/
followUnit(ID: number, offset?: { x: number; y: number; z: number }, formation?: string, units: Unit[] | null = null) {
followUnit(ID: number, offset?: { x: number; y: number; z: number }, units: Unit[] | null = null) {
if (units === null) units = this.getSelectedUnits();
units = units.filter((unit) => !unit.getHuman());
let callback = (units) => {
if (offset == undefined) {
/* Simple formations with fixed offsets */
offset = { x: 0, y: 0, z: 0 };
if (formation === "trail") {
offset.x = -50;
offset.y = -30;
offset.z = 0;
} else if (formation === "echelon-lh") {
offset.x = -50;
offset.y = -10;
offset.z = -50;
} else if (formation === "echelon-rh") {
offset.x = -50;
offset.y = -10;
offset.z = 50;
} else if (formation === "line-abreast-lh") {
offset.x = 0;
offset.y = 0;
offset.z = -50;
} else if (formation === "line-abreast-rh") {
offset.x = 0;
offset.y = 0;
offset.z = 50;
} else if (formation === "front") {
offset.x = 100;
offset.y = 0;
offset.z = 0;
} else offset = undefined;
}
if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) {
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION);
this.#protectionCallback = callback;
} else callback(units);
};
var count = 1;
var xr = 0;
var yr = 1;
var zr = -1;
var layer = 1;
units.forEach((unit: Unit) => {
if (unit.ID !== ID) {
if (offset != undefined)
/* Offset is set, apply it */
unit.followUnit(ID, {
x: offset.x * count,
y: offset.y * count,
z: offset.z * count,
});
else {
/* More complex formations with variable offsets */
if (formation === "diamond") {
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
unit.followUnit(ID, { x: -yl * 50, y: zr * 10, z: xl * 50 });
if (yr == 0) {
layer++;
xr = 0;
yr = layer;
zr = -layer;
} else {
if (xr < layer) {
xr++;
zr--;
} else {
yr--;
zr++;
}
}
}
}
count++;
}
});
this.#showActionMessage(units, `following unit ${this.getUnitByID(ID)?.getUnitName()}`);
}
@ -1332,7 +1263,7 @@ export class UnitsManager {
getApp().getMap().getMouseCoordinates().lat + unit.position.lat - avgLat,
getApp().getMap().getMouseCoordinates().lng + unit.position.lng - avgLng
);
markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition));
markers.push(getApp().getMap().addTemporaryMarker(position, unit.name, unit.coalition, false));
units.push({ ID: unit.ID, location: position });
});