diff --git a/frontend/server/app.js b/frontend/server/app.js
index 1f43110f..be9c829c 100644
--- a/frontend/server/app.js
+++ b/frontend/server/app.js
@@ -15,7 +15,7 @@ module.exports = function (configLocation) {
var indexRouter = require('./routes/index');
var uikitRouter = require('./routes/uikit');
var usersRouter = require('./routes/users');
- var resourcesRouter = require('./routes/resources');
+ var resourcesRouter = require('./routes/resources')(configLocation);
var pluginsRouter = require('./routes/plugins');
/* Load the config and create the express app */
diff --git a/frontend/server/routes/resources.js b/frontend/server/routes/resources.js
index ebb34ad7..e2460f42 100644
--- a/frontend/server/routes/resources.js
+++ b/frontend/server/routes/resources.js
@@ -4,140 +4,152 @@ const fs = require('fs');
const pfs = require('fs/promises')
const router = express.Router();
-router.get('/theme/*', function (req, res, next) {
- var reqTheme = "olympus";
-
- /* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */
- if (reqTheme === "parrot" && !req.url.includes(".css"))
- res.redirect('/themes/parrot/images/parrot.svg');
- else
- res.redirect(req.url.replace("theme", "themes/" + reqTheme));
-});
+module.exports = function (configLocation) {
+ router.get('/theme/*', function (req, res, next) {
+ var reqTheme = "olympus";
+
+ /* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */
+ if (reqTheme === "parrot" && !req.url.includes(".css"))
+ res.redirect('/themes/parrot/images/parrot.svg');
+ else
+ res.redirect(req.url.replace("theme", "themes/" + reqTheme));
+ });
-router.put('/theme/:newTheme', function (req, res, next) {
- res.end("Ok");
-});
+ router.put('/theme/:newTheme', function (req, res, next) {
+ res.end("Ok");
+ });
-router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) {
- let map = req.params.map;
- let x = req.params.x;
- let y = req.params.y;
- let z = req.params.z;
-
- if (fs.existsSync(`.\\public\\maps\\${map}`)) {
- if (!await renderImage(map, z, x, y, res)) {
- /* No image was found */
+ router.get('/config', function (req, res, next) {
+ if (fs.existsSync(configLocation)) {
+ let rawdata = fs.readFileSync(configLocation);
+ config = JSON.parse(rawdata);
+ res.send(JSON.stringify(config.frontend));
+ res.end()
+ } else {
res.sendStatus(404);
}
- } else {
- /* The requested map does not exist */
- res.sendStatus(404);
- }
-});
+ });
-async function renderImage(map, z, x, y, res, recursionDepth = 0) {
- if (recursionDepth == 20) {
- console.log("Render image, maximum recursion depth reached")
- /* We have reached limit recusion depth, something went wrong */
- return false;
- }
- /* If the requested image exists, send it straight away */
- if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
- res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
- return true;
- } else {
- /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */
- let sourceZoom = z - 1;
- let sourceX = Math.floor(x / 2);
- let sourceY = Math.floor(y / 2);
+ router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) {
+ let map = req.params.map;
+ let x = req.params.x;
+ let y = req.params.y;
+ let z = req.params.z;
- /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */
- while (sourceZoom >= 0) {
- /* We have found a possible source image */
- if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) {
- /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */
- let retries = 10;
- while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) {
- await new Promise(r => setTimeout(r, 1000));
- retries--;
- }
- /* Check if we exited because we reached the maximum retry value */
- if (retries != 0) {
- /* Recursively recall the function now that we have a new "source" image */
- return await renderImage(map, z, x, y, res, recursionDepth + 1);
- } else {
- return false;
- }
- } else {
- /* Keep searching at a higher level */
- sourceZoom = sourceZoom - 1;
- sourceX = Math.floor(sourceX / 2);
- sourceY = Math.floor(sourceY / 2);
+ if (fs.existsSync(`.\\public\\maps\\${map}`)) {
+ if (!await renderImage(map, z, x, y, res)) {
+ /* No image was found */
+ res.sendStatus(404);
}
+ } else {
+ /* The requested map does not exist */
+ res.sendStatus(404);
+ }
+ });
+
+ async function renderImage(map, z, x, y, res, recursionDepth = 0) {
+ if (recursionDepth == 20) {
+ console.log("Render image, maximum recursion depth reached")
+ /* We have reached limit recusion depth, something went wrong */
+ return false;
}
- return false;
- }
-}
+ /* If the requested image exists, send it straight away */
+ if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
+ res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+ return true;
+ } else {
+ /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */
+ let sourceZoom = z - 1;
+ let sourceX = Math.floor(x / 2);
+ let sourceY = Math.floor(y / 2);
-async function splitTile(map, z, x, y) {
- try {
- /* Load the source image */
- let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
-
- /* Create the necessary folders */
- await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true })
- await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true })
-
- /* Split the image into four parts */
- await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y);
- img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
- await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y);
- img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
- await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1);
- img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
- await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1);
- return true;
- } catch (err) {
- if (err.code !== 'EBUSY') {
- console.error(err);
+ /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */
+ while (sourceZoom >= 0) {
+ /* We have found a possible source image */
+ if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) {
+ /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */
+ let retries = 10;
+ while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) {
+ await new Promise(r => setTimeout(r, 1000));
+ retries--;
+ }
+ /* Check if we exited because we reached the maximum retry value */
+ if (retries != 0) {
+ /* Recursively recall the function now that we have a new "source" image */
+ return await renderImage(map, z, x, y, res, recursionDepth + 1);
+ } else {
+ return false;
+ }
+ } else {
+ /* Keep searching at a higher level */
+ sourceZoom = sourceZoom - 1;
+ sourceX = Math.floor(sourceX / 2);
+ sourceY = Math.floor(sourceY / 2);
+ }
+ }
+ return false;
}
- return false;
}
-}
-/* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */
-function resizePromise(img, left, top, map, z, x, y) {
- return new Promise((res, rej) => {
- if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
- res(true);
- }
- img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) {
- if (err) {
- rej(err);
- } else {
- try {
- fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
- } catch (err) {
- if (err.code === 'EBUSY') {
- /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */
- rej(err);
- } else if (err.code === 'ENOENT') {
- /* Someone else renamed the file already so this is not a real error */
- if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
- res(true);
- }
- /* Something odd happened, reject and try again */
- else {
+ async function splitTile(map, z, x, y) {
+ try {
+ /* Load the source image */
+ let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+
+ /* Create the necessary folders */
+ await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true })
+ await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true })
+
+ /* Split the image into four parts */
+ await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y);
+ img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+ await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y);
+ img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+ await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1);
+ img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+ await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1);
+ return true;
+ } catch (err) {
+ if (err.code !== 'EBUSY') {
+ console.error(err);
+ }
+ return false;
+ }
+ }
+
+ /* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */
+ function resizePromise(img, left, top, map, z, x, y) {
+ return new Promise((res, rej) => {
+ if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
+ res(true);
+ }
+ img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) {
+ if (err) {
+ rej(err);
+ } else {
+ try {
+ fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
+ } catch (err) {
+ if (err.code === 'EBUSY') {
+ /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */
+ rej(err);
+ } else if (err.code === 'ENOENT') {
+ /* Someone else renamed the file already so this is not a real error */
+ if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
+ res(true);
+ }
+ /* Something odd happened, reject and try again */
+ else {
+ rej(err);
+ }
+ } else {
rej(err);
}
- } else {
- rej(err);
}
+ res(true)
}
- res(true)
- }
- });
- })
+ });
+ })
+ }
+ return router;
}
-
-module.exports = router;
diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts
index 52e185cd..fc96f47a 100644
--- a/frontend/website/src/constants/constants.ts
+++ b/frontend/website/src/constants/constants.ts
@@ -154,7 +154,7 @@ export const mapBounds = {
"SinaiMap": { bounds: new LatLngBounds([34.312222, 28.523333], [25.946944, 36.897778]), zoom: 4 },
}
-export const mapLayers = {
+export const defaultMapLayers = {
"ArcGIS Satellite": {
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
minZoom: 1,
@@ -190,12 +190,6 @@ export const mapLayers = {
minZoom: 1,
maxZoom: 20,
attribution: 'CyclOSM | Map data: © OpenStreetMap contributors'
- },
- "DCS": {
- urlTemplate: 'http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png',
- minZoom: 16,
- maxZoom: 20,
- attribution: 'Eagle Dynamics'
}
}
diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts
index a10afd7d..32eccb44 100644
--- a/frontend/website/src/map/map.ts
+++ b/frontend/website/src/map/map.ts
@@ -12,7 +12,7 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import { SVGInjector } from '@tanem/svg-injector'
-import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
+import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
import { CoalitionArea } from "./coalitionarea/coalitionarea";
import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu";
import { DrawingCursor } from "./coalitionarea/drawingcursor";
@@ -90,6 +90,7 @@ export class Map extends L.Map {
#coalitionAreaContextMenu: CoalitionAreaContextMenu = new CoalitionAreaContextMenu("coalition-area-contextmenu");
#mapSourceDropdown: Dropdown;
+ #mapLayers: any = defaultMapLayers;
#mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS;
#mapVisibilityOptionsDropdown: Dropdown;
#optionButtons: { [key: string]: HTMLButtonElement[] } = {}
@@ -120,10 +121,10 @@ export class Map extends L.Map {
this.#ID = ID;
- this.setLayer(Object.keys(mapLayers)[0]);
+ this.setLayer(Object.keys(this.#mapLayers)[0]);
/* Minimap */
- var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 });
+ var minimapLayer = new L.TileLayer(this.#mapLayers[Object.keys(this.#mapLayers)[0]].urlTemplate, { minZoom: 0, maxZoom: 13 });
this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]);
this.#miniMapPolyline = new L.Polyline([], { color: '#202831' });
this.#miniMapPolyline.addTo(this.#miniMapLayerGroup);
@@ -203,6 +204,18 @@ export class Map extends L.Map {
this.getContainer().toggleAttribute("data-hide-labels", !this.getVisibilityOptions()[SHOW_UNIT_LABELS]);
});
+ document.addEventListener("configLoaded", () => {
+ let config = getApp().getConfig();
+ if (config.additionalMaps) {
+ let additionalMaps = config.additionalMaps;
+ this.#mapLayers = {
+ ...this.#mapLayers,
+ ...additionalMaps
+ }
+ this.#mapSourceDropdown.setOptions(this.getLayers());
+ }
+ })
+
/* Pan interval */
this.#panInterval = window.setInterval(() => {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
@@ -234,8 +247,8 @@ export class Map extends L.Map {
if (this.#layer != null)
this.removeLayer(this.#layer)
- if (layerName in mapLayers) {
- const layerData = mapLayers[layerName as keyof typeof mapLayers];
+ if (layerName in this.#mapLayers) {
+ const layerData = this.#mapLayers[layerName];
var options: L.TileLayerOptions = {
attribution: layerData.attribution,
minZoom: layerData.minZoom,
@@ -248,7 +261,7 @@ export class Map extends L.Map {
}
getLayers() {
- return Object.keys(mapLayers);
+ return Object.keys(this.#mapLayers);
}
/* State machine */
diff --git a/frontend/website/src/olympusapp.ts b/frontend/website/src/olympusapp.ts
index 4d916d11..e2c5dc09 100644
--- a/frontend/website/src/olympusapp.ts
+++ b/frontend/website/src/olympusapp.ts
@@ -33,6 +33,7 @@ export class OlympusApp {
/* Global data */
#activeCoalition: string = "blue";
#latestVersion: string|undefined = undefined;
+ #config: any = {};
/* Main leaflet map, extended by custom methods */
#map: Map | null = null;
@@ -251,6 +252,19 @@ export class OlympusApp {
latestVersionSpan.classList.toggle("new-version", this.#latestVersion !== VERSION);
}
})
+
+ /* Load the config file from the server */
+ const configRequest = new Request(location.href + "resources/config");
+ fetch(configRequest).then((response) => {
+ if (response.status === 200) {
+ return response.json();
+ } else {
+ throw new Error("Error retrieving config file");
+ }
+ }).then((res) => {
+ this.#config = res;
+ document.dispatchEvent(new CustomEvent("configLoaded"));
+ })
}
#setupEvents() {
@@ -446,4 +460,8 @@ export class OlympusApp {
img.addEventListener("load", () => { SVGInjector(img); });
})
}
+
+ getConfig() {
+ return this.#config;
+ }
}
\ No newline at end of file
diff --git a/olympus.json b/olympus.json
index e40a4843..54c8916f 100644
--- a/olympus.json
+++ b/olympus.json
@@ -14,6 +14,14 @@
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
"username": null,
"password": null
- }
+ },
+ "additionalMaps": {
+ "DCS": {
+ "urlTemplate": "http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png",
+ "minZoom": 8,
+ "maxZoom": 20,
+ "attribution": "Eagle Dynamics"
+ }
+ }
}
}
\ No newline at end of file