mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added ability to add more map sources
This commit is contained in:
parent
9a571132c8
commit
2e1c3ec4b9
@ -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 */
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
},
|
||||
"DCS": {
|
||||
urlTemplate: 'http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png',
|
||||
minZoom: 16,
|
||||
maxZoom: 20,
|
||||
attribution: 'Eagle Dynamics'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
10
olympus.json
10
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user