From 2e39b7811a641c8584085bb2aae4951e1f440dff Mon Sep 17 00:00:00 2001 From: Ross Hill Date: Thu, 8 Feb 2024 19:35:17 -0500 Subject: [PATCH] Add coordinate/zoom inputs and share link --- README.md | 2 +- client/app/MandelbrotLayer.ts | 131 ++++++++++++ client/app/MandelbrotMap.ts | 160 ++++++++++++++ client/app/main.ts | 381 +++++++++++++--------------------- client/app/worker.js | 30 +++ client/app/worker.ts | 42 ---- client/css/styles.scss | 59 +++++- client/index.html | 77 +++++-- client/package-lock.json | 280 ++++++++++++++----------- client/package.json | 2 +- client/webpack.config.js | 2 +- mandelbrot/src/lib.rs | 58 +++--- 12 files changed, 776 insertions(+), 448 deletions(-) create mode 100644 client/app/MandelbrotLayer.ts create mode 100644 client/app/MandelbrotMap.ts create mode 100644 client/app/worker.js delete mode 100644 client/app/worker.ts diff --git a/README.md b/README.md index 61f690bd..eadc95eb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ - [Mandelbrot set code - mandelbrot/src/lib.rs](mandelbrot/src/lib.rs) - [Rust tests - mandelbrot/src/lib_test.rs](mandelbrot/src/lib_test.rs) -- [Web Worker - client/app/worker.ts](client/app/worker.ts) +- [Web Worker - client/app/worker.js](client/app/worker.js) - [Leaflet tile generation - client/app/main.ts](client/app/main.ts) ## Features diff --git a/client/app/MandelbrotLayer.ts b/client/app/MandelbrotLayer.ts new file mode 100644 index 00000000..406f49d4 --- /dev/null +++ b/client/app/MandelbrotLayer.ts @@ -0,0 +1,131 @@ +import debounce from "lodash/debounce"; +import * as L from "leaflet"; +import MandelbrotMap from "./MandelbrotMap"; +import { config } from "./main"; + +type Done = (error: null, tile: HTMLCanvasElement) => void; + +class MandelbrotLayer extends L.GridLayer { + tileSize: number; + _map: MandelbrotMap; + tilesToGenerate: Array<{ + position: L.Coords; + canvas: HTMLCanvasElement; + done: Done; + }> = []; + + constructor() { + super({ + noWrap: true, + tileSize: 200, + }); + } + + private getComplexBoundsOfTile(tilePosition: L.Coords) { + const { re: re_min, im: im_min } = this._map.tilePositionToComplexParts( + tilePosition.x, + tilePosition.y, + tilePosition.z + ); + + const { re: re_max, im: im_max } = this._map.tilePositionToComplexParts( + tilePosition.x + 1, + tilePosition.y + 1, + tilePosition.z + ); + + const bounds = { + re_min, + re_max, + im_min, + im_max, + }; + + return bounds; + } + + private generateTile( + canvas: HTMLCanvasElement, + tilePosition: L.Coords, + done: Done + ) { + const context = canvas.getContext("2d"); + + const scaledTileSize = config.highDpiTiles + ? this.getTileSize().x * Math.max(window.devicePixelRatio || 2, 2) + : this.getTileSize().x; + + canvas.width = scaledTileSize; + canvas.height = scaledTileSize; + + const bounds = this.getComplexBoundsOfTile(tilePosition); + + this._map.pool.queue(async ({ getTile }) => { + getTile({ + bounds, + maxIterations: config.iterations, + exponent: config.exponent, + tileSize: scaledTileSize, + colorScheme: config.colorScheme, + reverseColors: config.reverseColors, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }).then((data: any) => { + const imageData = new ImageData( + Uint8ClampedArray.from(data), + scaledTileSize, + scaledTileSize + ); + context.putImageData(imageData, 0, 0); + done(null, canvas); + }); + }); + + return canvas; + } + + private generateTiles() { + const tilesToGenerate = this.tilesToGenerate.splice( + 0, + this.tilesToGenerate.length + ); + + const mapZoom = this._map.getZoom(); + + const seenTasks = new Set(); + const relevantTasks = tilesToGenerate.filter((task) => { + const positionString = JSON.stringify(task.position); + if (task.position.z === mapZoom && !seenTasks.has(positionString)) { + seenTasks.add(positionString); + return true; + } + return false; + }); + + relevantTasks.forEach((task) => { + this.generateTile(task.canvas, task.position, task.done); + }); + } + + debounceTileGeneration = debounce(this.generateTiles, 350); + + createTile(tilePosition: L.Coords, done: Done) { + const canvas = L.DomUtil.create( + "canvas", + "leaflet-tile" + ) as HTMLCanvasElement; + this.tilesToGenerate.push({ position: tilePosition, canvas, done }); + this.debounceTileGeneration(); + return canvas; + } + + refresh() { + let currentMap: MandelbrotMap | null = null; + if (this._map) { + currentMap = this._map as MandelbrotMap; + this.removeFrom(this._map); + } + this.addTo(currentMap); + } +} + +export { MandelbrotLayer }; diff --git a/client/app/MandelbrotMap.ts b/client/app/MandelbrotMap.ts new file mode 100644 index 00000000..83e321ae --- /dev/null +++ b/client/app/MandelbrotMap.ts @@ -0,0 +1,160 @@ +import throttle from "lodash/throttle"; +import * as L from "leaflet"; +import { saveAs } from "file-saver"; +import domToImage from "dom-to-image"; +import { Pool, Worker, spawn } from "threads"; +import { MandelbrotLayer } from "./MandelbrotLayer"; +import { config } from "./main"; + +class MandelbrotMap extends L.Map { + mandelbrotLayer: MandelbrotLayer; + mapId: string; + defaultPosition: [number, number]; + defaultZoom: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pool: Pool; + + constructor({ htmlId: mapId }: { htmlId: string }) { + super(mapId, { + attributionControl: false, + maxZoom: 32, + zoomAnimationThreshold: 32, + }); + + this.createPool(); + this.mapId = mapId; + this.mandelbrotLayer = new MandelbrotLayer().addTo(this); + this.defaultPosition = [0, 0]; + this.defaultZoom = 3; + this.setView(this.defaultPosition, this.defaultZoom); + + this.on("drag", function () { + this.mandelbrotLayer.debounceTileGeneration.flush(); + }); + this.on("click", this.handleMapClick); + + this.on("load", this.throttleSetDomElementValues); + this.on("move", this.throttleSetDomElementValues); + this.on("moveend", this.throttleSetDomElementValues); + this.on("zoomend", this.throttleSetDomElementValues); + this.on("viewreset", this.throttleSetDomElementValues); + this.on("resize", this.throttleSetDomElementValues); + } + + handleMapClick = (e: L.LeafletMouseEvent) => { + if (e.originalEvent.altKey) { + this.setView(e.latlng, this.getZoom()); + } + }; + + tilePositionToComplexParts( + x: number, + y: number, + z: number + ): { re: number; im: number } { + const scaleFactor = this.mandelbrotLayer.getTileSize().x / 128; + const d = 2 ** (z - 2); + const re = (x / d) * scaleFactor - 4; + const im = (y / d) * scaleFactor - 4; + return { re, im }; + } + + complexPartsToTilePosition(re: number, im: number, z: number) { + const scaleFactor = this.mandelbrotLayer.getTileSize().x / 128; + const d = 2 ** (z - 2); + const x = ((re + 4) * d) / scaleFactor; + const y = ((im + 4) * d) / scaleFactor; + return { x, y }; + } + + private setDomElementValues = () => { + const tileSize = [ + this.mandelbrotLayer.getTileSize().x, + this.mandelbrotLayer.getTileSize().y, + ]; + const point = this.project(this.getCenter(), this.getZoom()).unscaleBy( + new L.Point(tileSize[0], tileSize[1]) + ); + + const position = { ...point, z: this.getZoom() }; + + const { re, im } = this.tilePositionToComplexParts( + position.x, + position.y, + position.z + ); + + config.re = re; + (document.querySelector("#re")).value = String(re); + + config.im = im; + (document.querySelector("#im")).value = String(im); + + config.zoom = position.z; + (document.querySelector("#zoom")).value = String( + position.z + ); + + (( + document.querySelector("#shareLink") + )).href = `?re=${re}&im=${im}&z=${position.z}&i=${config.iterations}&e=${config.exponent}&c=${config.colorScheme}&r=${config.reverseColors}&sharing=true`; + }; + + private complexPartsToLatLng(re: number, im: number, z: number) { + const tileSize = [ + this.mandelbrotLayer.getTileSize().x, + this.mandelbrotLayer.getTileSize().y, + ]; + + const { x, y } = this.complexPartsToTilePosition(re, im, z); + + const latLng = this.unproject( + L.point(x, y).scaleBy(new L.Point(tileSize[0], tileSize[1])), + z + ); + + return latLng; + } + + throttleSetDomElementValues = throttle(this.setDomElementValues, 200); + + async createPool() { + if (this.pool) { + this.pool.terminate(true); + } + + this.pool = Pool(() => spawn(new Worker("./worker.js"))); + } + + async refresh(resetView = false) { + await this.createPool(); + if (resetView) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any)._resetView(this.defaultPosition, this.defaultZoom); + } else { + const pointToCenter = this.complexPartsToLatLng( + config.re, + config.im, + config.zoom + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any)._resetView(pointToCenter, config.zoom); + } + } + + async saveImage() { + const zoomControl = this.zoomControl; + const mapElement = document.getElementById(this.mapId); + const width = mapElement.offsetWidth; + const height = mapElement.offsetHeight; + this.removeControl(zoomControl); + const blob = await domToImage.toBlob(mapElement, { width, height }); + this.addControl(zoomControl); + saveAs( + blob, + `mandelbrot${Date.now()}r${config.re}im${config.im}z${config.zoom}.png` + ); + } +} + +export default MandelbrotMap; diff --git a/client/app/main.ts b/client/app/main.ts index 7072b638..d1055e03 100644 --- a/client/app/main.ts +++ b/client/app/main.ts @@ -1,260 +1,52 @@ import "./static"; import debounce from "lodash/debounce"; import * as L from "leaflet"; -import { saveAs } from "file-saver"; -import domToImage from "dom-to-image"; -import { EsThreadPool, EsThread } from "threads-es/controller"; +import MandelbrotMap from "./MandelbrotMap"; -interface MandelbrotConfig { +type MandelbrotConfig = { iterations: number; exponent: number; colorScheme: string; reverseColors: boolean; highDpiTiles: boolean; -} -interface NumberInput { - id: "iterations" | "exponent"; + re: number; + im: number; + zoom: number; +}; + +type NumberInput = { + id: "iterations" | "exponent" | "re" | "im" | "zoom"; map: MandelbrotMap; minValue: number; defaultValue: number; maxValue: number; + allowFraction?: boolean; resetView?: boolean; -} +}; -interface SelectInput { +type SelectInput = { id: "colorScheme"; map: MandelbrotMap; -} +}; -interface CheckboxInput { +type CheckboxInput = { id: "reverseColors" | "highDpiTiles"; map: MandelbrotMap; hidden?: boolean; -} - -interface Done { - (error: null, tile: HTMLCanvasElement): void; -} +}; -const config: MandelbrotConfig = { +export const config: MandelbrotConfig = { iterations: 200, exponent: 2, colorScheme: "turbo", reverseColors: false, highDpiTiles: false, -}; -class MandelbrotLayer extends L.GridLayer { - tileSize: number; - _map: MandelbrotMap; - taskQueue: Array<{ - coords: L.Coords; - canvas: HTMLCanvasElement; - done: Done; - }> = []; - - constructor() { - super({ - noWrap: true, - tileSize: 200, - }); - } - - private mapCoordsToComplex( - x: number, - y: number, - z: number - ): { re: number; im: number } { - const scaleFactor = this.getTileSize().x / 128.5; - const d = 2 ** (z - 2); - const re = (x / d) * scaleFactor - 4; - const im = (y / d) * scaleFactor - 4; - return { re, im }; - } - - private getMappedCoords(coords: L.Coords) { - const { re: re_min, im: im_min } = this.mapCoordsToComplex( - coords.x, - coords.y, - coords.z - ); - - const { re: re_max, im: im_max } = this.mapCoordsToComplex( - coords.x + 1, - coords.y + 1, - coords.z - ); - - const mappedCoords = { - re_min, - re_max, - im_min, - im_max, - }; - - return mappedCoords; - } - - private actualCreateTile( - canvas: HTMLCanvasElement, - coords: L.Coords, - done: Done - ) { - const context = canvas.getContext("2d"); - const scaledTileSize = config.highDpiTiles - ? this.getTileSize().x * Math.max(window.devicePixelRatio || 2, 2) - : this.getTileSize().x; - - canvas.width = scaledTileSize; - canvas.height = scaledTileSize; - - const mappedCoords = this.getMappedCoords(coords); - - this._map.pool - ?.queue((thread) => - thread.methods.getTile({ - coords: mappedCoords, - maxIterations: config.iterations, - exponent: config.exponent, - tileSize: scaledTileSize, - colorScheme: config.colorScheme, - reverseColors: config.reverseColors, - }) - ) - .then((result) => { - const imageData = new ImageData( - Uint8ClampedArray.from(result), - scaledTileSize, - scaledTileSize - ); - if (canvas.dataset.z !== String(coords.z)) { - canvas.dataset.z = JSON.stringify(coords.z); - context.putImageData(imageData, 0, 0); - done(null, canvas); - } - }); - - return canvas; - } - - private processTileGenerationQueue() { - const taskQueue = this.taskQueue.splice(0, this.taskQueue.length); - - const mapZoom = this._map.getZoom(); - - const relevantTasks = taskQueue.filter((task) => { - return task.coords.z === mapZoom; - }); - - relevantTasks.forEach((task) => { - this.actualCreateTile(task.canvas, task.coords, task.done); - }); - } - - debounceTileGeneration = debounce(() => { - this.processTileGenerationQueue(); - }, 300); - - createTile(coords: L.Coords, done: Done) { - const canvas = L.DomUtil.create( - "canvas", - "leaflet-tile" - ) as HTMLCanvasElement; - this.taskQueue.push({ coords, canvas, done }); - this.debounceTileGeneration(); - return canvas; - } - - refresh() { - let currentMap: MandelbrotMap | null = null; - if (this._map) { - currentMap = this._map as MandelbrotMap; - this.removeFrom(this._map); - } - this.addTo(currentMap); - } -} - -class MandelbrotMap extends L.Map { - mandelbrotLayer: MandelbrotLayer; - mapId: string; - defaultPosition: [number, number]; - defaultZoom: number; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pool: EsThreadPool; - - constructor({ htmlId: mapId }: { htmlId: string }) { - super(mapId, { - attributionControl: false, - maxZoom: 32, - zoomAnimationThreshold: 32, - }); - - this.createPool().then(() => { - this.refresh(false); - this.mandelbrotLayer.refresh(); - }); - this.mapId = mapId; - this.mandelbrotLayer = new MandelbrotLayer().addTo(this); - this.defaultPosition = [0, 0]; - this.defaultZoom = 3; - this.setView(this.defaultPosition, this.defaultZoom); - this.mandelbrotLayer.refresh(); - - this.on("drag", function () { - this.mandelbrotLayer.debounceTileGeneration.flush(); - }); - this.on("click", this.handleMapClick); - } - - handleMapClick = (e: L.LeafletMouseEvent) => { - if (e.originalEvent.altKey) { - this.setView(e.latlng, this.getZoom()); - } - }; - - async createPool() { - if (this.pool) { - this.pool.terminate(); - } - - this.pool = await EsThreadPool.Spawn( - () => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - EsThread.Spawn( - new Worker(new URL("./worker.ts", import.meta.url), { - type: "module", - }) - ), - { - size: navigator.hardwareConcurrency || 4, - } - ); - } - - async refresh(resetView = false) { - await this.createPool(); - if (resetView) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any)._resetView(this.defaultPosition, this.defaultZoom); - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this as any)._resetView(this.getCenter(), this.getZoom()); - } - } - - async saveImage() { - const zoomControl = this.zoomControl; - const mapElement = document.getElementById(this.mapId); - const width = mapElement.offsetWidth; - const height = mapElement.offsetHeight; - this.removeControl(zoomControl); - const blob = await domToImage.toBlob(mapElement, { width, height }); - this.addControl(zoomControl); - saveAs(blob, `mandelbrot-${Date.now()}.png`); - } -} + re: 0, + im: 0, + zoom: 3, +}; function handleNumberInput({ id, @@ -263,11 +55,14 @@ function handleNumberInput({ minValue, maxValue, resetView, + allowFraction, }: NumberInput) { const input = document.getElementById(id); input.value = String(config[id]); input.oninput = debounce(({ target }) => { - let parsedValue = Number.parseInt((target).value, 10); + let parsedValue = allowFraction + ? Number.parseFloat((target).value) + : Number.parseInt((target).value, 10); if ( isNaN(parsedValue) || parsedValue < minValue || @@ -315,6 +110,29 @@ function handleDom(map: MandelbrotMap) { maxValue: 10 ** 9, resetView: true, }); + handleNumberInput({ + id: "re", + map, + minValue: -2, + defaultValue: 0, + maxValue: 2, + allowFraction: true, + }); + handleNumberInput({ + id: "im", + map, + minValue: -2, + defaultValue: 0, + maxValue: 2, + allowFraction: true, + }); + handleNumberInput({ + id: "zoom", + map, + minValue: 0, + defaultValue: 3, + maxValue: 32, + }); handleSelectInput({ id: "colorScheme", map }); handleCheckboxInput({ id: "reverseColors", map }); handleCheckboxInput({ @@ -327,11 +145,20 @@ function handleDom(map: MandelbrotMap) { const fullScreenButton: HTMLButtonElement = document.querySelector("#full-screen"); - if (document.fullscreenEnabled) { - fullScreenButton.onclick = toggleFullScreen; - } else { - fullScreenButton.style.display = "none"; - } + const exitFullScreenButton: HTMLButtonElement = + document.querySelector("#exit-full-screen"); + fullScreenButton.onclick = toggleFullScreen; + exitFullScreenButton.onclick = toggleFullScreen; + + const resetButton: HTMLButtonElement = document.querySelector("#reset"); + resetButton.onclick = () => map.refresh(true); + + const hideShowControlsButton: HTMLButtonElement = document.querySelector( + "#hide-show-controls" + ); + hideShowControlsButton.onclick = () => { + document.body.classList.toggle("hideOverlays"); + }; const saveButton: HTMLButtonElement = document.querySelector("#save-image"); try { @@ -354,10 +181,17 @@ function handleDom(map: MandelbrotMap) { } document.addEventListener("fullscreenchange", () => { - const button: HTMLButtonElement = document.querySelector("#full-screen"); - button.innerText = document.fullscreenElement - ? "Exit Full Screen" - : "Full Screen"; + const fullScreenButton: HTMLButtonElement = + document.querySelector("#full-screen"); + const exitFullScreenButton: HTMLButtonElement = + document.querySelector("#exit-full-screen"); + if (document.fullscreenElement) { + fullScreenButton.style.display = "none"; + exitFullScreenButton.style.display = "inline-block"; + } else { + fullScreenButton.style.display = "inline-block"; + exitFullScreenButton.style.display = "none"; + } }); const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; @@ -374,10 +208,81 @@ function handleDom(map: MandelbrotMap) { if (event.key === "h") { document.body.classList.toggle("hideOverlays"); } + if (event.key === "s") { + map.saveImage(); + } + if (event.key === "r") { + map.refresh(); + } + if (event.key === "f") { + toggleFullScreen(); + } + if (event.key === "b") { + map.refresh(true); + } }); + + if (L.Browser.mobile || L.Browser.android || L.Browser.ie) { + const banner = ( + document.querySelector("#mobile-not-supported") + ); + banner.style.display = "flex"; + } } const map = new MandelbrotMap({ htmlId: "leaflet-map", }); handleDom(map); + +window.addEventListener("load", function () { + const queryParams = new URLSearchParams(window.location.search); + const re = queryParams.get("re"); + const im = queryParams.get("im"); + const zoom = queryParams.get("z"); + const iterations = queryParams.get("i"); + const exponent = queryParams.get("e"); + const colorScheme = queryParams.get("c"); + const reverseColors = queryParams.get("r"); + const sharing = queryParams.get("sharing"); + + if (re && im && zoom) { + config.re = Number(re); + config.im = Number(im); + config.zoom = Number(zoom); + + if (iterations) { + config.iterations = Number(iterations); + (document.querySelector("#iterations")).value = + iterations; + } + if (exponent) { + config.exponent = Number(exponent); + (document.querySelector("#exponent")).value = exponent; + } + if (colorScheme) { + config.colorScheme = colorScheme; + (document.querySelector("#colorScheme")).value = + colorScheme; + } + if (reverseColors) { + config.reverseColors = reverseColors === "true"; + (document.querySelector("#reverseColors")).checked = + config.reverseColors; + } + + if (sharing) { + window.history.replaceState( + {}, + document.title, + window.location.href.replace("&sharing=true", "") + ); + } else { + window.history.replaceState({}, document.title, window.location.pathname); + } + + if (config.re !== 0 && config.im !== 0 && config.zoom !== 3) { + map.refresh(); + } + } +}); diff --git a/client/app/worker.js b/client/app/worker.js new file mode 100644 index 00000000..e4778b62 --- /dev/null +++ b/client/app/worker.js @@ -0,0 +1,30 @@ +import { expose } from "threads/worker"; + +import("../../mandelbrot/pkg").then((wasm) => { + wasm.init(); + + const workerApi = { + getTile: ({ + bounds, + maxIterations, + exponent, + tileSize, + colorScheme, + reverseColors, + }) => { + return wasm.get_tile( + bounds.re_min, + bounds.re_max, + bounds.im_min, + bounds.im_max, + maxIterations, + exponent, + tileSize, + colorScheme, + reverseColors + ); + }, + }; + + expose(workerApi); +}); diff --git a/client/app/worker.ts b/client/app/worker.ts deleted file mode 100644 index 9dd2f17d..00000000 --- a/client/app/worker.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { exposeApi } from "threads-es/worker"; -import("../../mandelbrot/pkg").then((wasm) => { - wasm.init(); - - const workerApi = { - getTile: ({ - coords, - maxIterations, - exponent, - tileSize, - colorScheme, - reverseColors, - }: { - coords: { - re_min: number; - re_max: number; - im_min: number; - im_max: number; - }; - maxIterations: number; - exponent: number; - tileSize: number; - colorScheme: string; - reverseColors: boolean; - }) => { - const data = wasm.get_tile( - coords.re_min, - coords.re_max, - coords.im_min, - coords.im_max, - maxIterations, - exponent, - tileSize, - colorScheme, - reverseColors - ); - return data; - }, - }; - - exposeApi(workerApi); -}); diff --git a/client/css/styles.scss b/client/css/styles.scss index 32f0a987..610b0384 100644 --- a/client/css/styles.scss +++ b/client/css/styles.scss @@ -54,14 +54,21 @@ body.hideOverlays { #inputsWrapper { @extend .overlay; + max-height: calc(100% - 40px); + overflow-y: auto; + border-bottom-left-radius: 8px; top: 0; right: 0; - width: 164px; + width: 170px; display: flex; flex-direction: column; gap: 12px; + .shortcutHint { + text-decoration: underline; + } + .inputSeparator { height: 1px; border-bottom: 1px solid white; @@ -108,6 +115,12 @@ body.hideOverlays { display: none; } } + + #shareLink { + margin: 0 auto; + margin-top: -4px; + margin-bottom: 4px; + } } #attribution { @@ -138,7 +151,49 @@ select { color: black; &:hover { - cursor: pointer; opacity: 0.9; } } + +button, +select, +input[type="checkbox"] { + cursor: pointer; +} + +input[type="text"] { + cursor: text; +} + +button#exit-full-screen { + display: none; +} + +#mobile-not-supported { + display: none; + background: black; + color: white; + z-index: 3000; + position: absolute; + inset: 0; + align-items: center; + justify-content: center; + + p { + max-width: 300px; + text-align: center; + line-height: 1.5; + } +} + +@media (hover: none) { + #mobile-not-supported { + display: flex; + } +} + +@media (max-width: 500px) { + #mobile-not-supported { + display: flex; + } +} diff --git a/client/index.html b/client/index.html index aceb17f4..f0d07476 100644 --- a/client/index.html +++ b/client/index.html @@ -72,11 +72,12 @@ +
+

+ This application is not designed for mobile devices or small screens. +

+
-
- H -
Hide/show controls
-
shift + drag
Zoom in on a region
@@ -97,7 +98,7 @@
- +
@@ -113,17 +114,14 @@ - - - - - - - - - - - + + + + + + + + @@ -136,7 +134,7 @@ - + @@ -155,9 +153,48 @@
- - - + Coordinates +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + +
+ + Share this view +
By diff --git a/client/package-lock.json b/client/package-lock.json index d9de0a63..4b3c89b1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,7 +14,7 @@ "leaflet": "1.7.1", "lodash": "^4.17.21", "long": "^5.2.3", - "threads-es": "^1.0.0", + "threads": "^1.7.0", "util": "^0.12.5" }, "bin": { @@ -464,9 +464,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "version": "20.11.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", + "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -554,16 +554,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.20.0.tgz", - "integrity": "sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/type-utils": "6.20.0", - "@typescript-eslint/utils": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -589,15 +589,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", - "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { @@ -617,13 +617,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.20.0.tgz", - "integrity": "sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -634,13 +634,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.20.0.tgz", - "integrity": "sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.20.0", - "@typescript-eslint/utils": "6.20.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -661,9 +661,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.20.0.tgz", - "integrity": "sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -674,13 +674,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.20.0.tgz", - "integrity": "sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/visitor-keys": "6.20.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -702,17 +702,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.20.0.tgz", - "integrity": "sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.20.0", - "@typescript-eslint/types": "6.20.0", - "@typescript-eslint/typescript-estree": "6.20.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -727,12 +727,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.20.0.tgz", - "integrity": "sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.20.0", + "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -743,11 +743,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/event-target": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@ungap/event-target/-/event-target-0.2.4.tgz", - "integrity": "sha512-u9Fd3k2qfMtn+0dxbCn/y0pzQ9Ucw6lWR984CrHcbxc+WzcMkJE4VjWHWSb9At40MjwMyHCkJNXroS55Osshhw==" - }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -1316,13 +1311,17 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", + "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.3", + "set-function-length": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1332,7 +1331,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -1348,9 +1346,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001583", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001583.tgz", - "integrity": "sha512-acWTYaha8xfhA/Du/z4sNZjHUWjkiuoAi2LM+T/aL+kemKQgPT1xBb/YKjlQ0Qo8gvbHsGNplrEJ+9G3gL7i4Q==", + "version": "1.0.30001585", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz", + "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==", "dev": true, "funding": [ { @@ -1382,16 +1380,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1404,6 +1396,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -1692,7 +1687,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1724,13 +1718,14 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz", + "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==", "dependencies": { - "get-intrinsic": "^1.2.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.2", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -1925,9 +1920,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.656", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", - "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", + "version": "1.4.662", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.662.tgz", + "integrity": "sha512-gfl1XVWTQmPHhqEG0kN77SpUxaqPpMb9r83PT4gvKhg7P3irSxru3lW85RxvK1uI1j2CAcTWPjG/HbE0IP/Rtg==", "dev": true }, "node_modules/emojis-list": { @@ -1983,9 +1978,9 @@ } }, "node_modules/es-errors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.0.0.tgz", - "integrity": "sha512-yHV74THqMJUyFKkHyN7hyENcEZM3Dj2a2IrdClY+IT4BFQHkIVwlh8s6uZfjsFydMdNHv0F5mWgAA3ajFbsvVQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "engines": { "node": ">= 0.4" } @@ -1997,9 +1992,9 @@ "dev": true }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -2222,6 +2217,15 @@ "node": ">=8" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -2441,9 +2445,9 @@ } }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2669,11 +2673,11 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", - "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { - "es-errors": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -3337,6 +3341,17 @@ "node": ">=0.12.0" } }, + "node_modules/is-observable": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", + "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -3848,8 +3863,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multicast-dns": { "version": "7.2.5", @@ -3970,6 +3984,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/observable-fns": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz", + "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -4279,9 +4298,9 @@ } }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -4817,9 +4836,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -4979,13 +4998,14 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -5042,14 +5062,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5287,12 +5311,21 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/threads-es": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/threads-es/-/threads-es-1.0.0.tgz", - "integrity": "sha512-ytsl7XSKyX0FJR8xwvwnqL+UUVzoArBJYuE2sWNNlC2Lu0hAArTzTpPXBvlaiiw31d+oj/bZblImSaEfIK+aHQ==", + "node_modules/threads": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", + "integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==", "dependencies": { - "@ungap/event-target": "^0.2.4" + "callsites": "^3.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", + "observable-fns": "^0.6.1" + }, + "funding": { + "url": "https://github.com/andywer/threads.js?sponsor=1" + }, + "optionalDependencies": { + "tiny-worker": ">= 2" } }, "node_modules/thunky": { @@ -5301,6 +5334,15 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "optional": true, + "dependencies": { + "esm": "^3.2.25" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5323,12 +5365,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" diff --git a/client/package.json b/client/package.json index 520f43d8..2389ad38 100644 --- a/client/package.json +++ b/client/package.json @@ -44,7 +44,7 @@ "leaflet": "1.7.1", "lodash": "^4.17.21", "long": "^5.2.3", - "threads-es": "^1.0.0", + "threads": "^1.7.0", "util": "^0.12.5" }, "engines": { diff --git a/client/webpack.config.js b/client/webpack.config.js index 93c94198..d7e48c38 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -49,7 +49,7 @@ const appConfig = { }; const workerConfig = { - entry: "./app/worker.ts", + entry: "./app/worker.js", target: "webworker", plugins: [ new WasmPackPlugin({ diff --git a/mandelbrot/src/lib.rs b/mandelbrot/src/lib.rs index 3523ce2f..ebb56e55 100644 --- a/mandelbrot/src/lib.rs +++ b/mandelbrot/src/lib.rs @@ -89,35 +89,20 @@ fn rect_in_set( const NUM_COLOR_CHANNELS: usize = 4; -static COLOROUS_PALETTES: Lazy> = Lazy::new(|| { +static COLOR_PALETTES: Lazy> = Lazy::new(|| { let mut map = HashMap::new(); - map.insert("blueGreen".to_string(), colorous::BLUE_GREEN); - map.insert("bluePurple".to_string(), colorous::BLUE_PURPLE); - map.insert("blues".to_string(), colorous::BLUES); map.insert("brownGreen".to_string(), colorous::BROWN_GREEN); map.insert("cividis".to_string(), colorous::CIVIDIS); map.insert("cool".to_string(), colorous::COOL); map.insert("cubehelix".to_string(), colorous::CUBEHELIX); - map.insert("greenBlue".to_string(), colorous::GREEN_BLUE); - map.insert("greens".to_string(), colorous::GREENS); - map.insert("greys".to_string(), colorous::GREYS); map.insert("inferno".to_string(), colorous::INFERNO); map.insert("magma".to_string(), colorous::MAGMA); - map.insert("orangeRed".to_string(), colorous::ORANGE_RED); - map.insert("oranges".to_string(), colorous::ORANGES); - map.insert("pinkGreen".to_string(), colorous::PINK_GREEN); map.insert("plasma".to_string(), colorous::PLASMA); - map.insert("purpleBlue".to_string(), colorous::PURPLE_BLUE); - map.insert("purpleBlueGreen".to_string(), colorous::PURPLE_BLUE_GREEN); map.insert("purpleGreen".to_string(), colorous::PURPLE_GREEN); map.insert("purpleOrange".to_string(), colorous::PURPLE_ORANGE); - map.insert("purpleRed".to_string(), colorous::PURPLE_RED); - map.insert("purples".to_string(), colorous::PURPLES); map.insert("rainbow".to_string(), colorous::RAINBOW); map.insert("redBlue".to_string(), colorous::RED_BLUE); map.insert("redGrey".to_string(), colorous::RED_GREY); - map.insert("redPurple".to_string(), colorous::RED_PURPLE); - map.insert("reds".to_string(), colorous::REDS); map.insert("redYellowBlue".to_string(), colorous::RED_YELLOW_BLUE); map.insert("redYellowGreen".to_string(), colorous::RED_YELLOW_GREEN); map.insert("sinebow".to_string(), colorous::SINEBOW); @@ -125,13 +110,31 @@ static COLOROUS_PALETTES: Lazy> = Lazy::new( map.insert("turbo".to_string(), colorous::TURBO); map.insert("viridis".to_string(), colorous::VIRIDIS); map.insert("warm".to_string(), colorous::WARM); - map.insert("yellowGreen".to_string(), colorous::YELLOW_GREEN); - map.insert("yellowGreenBlue".to_string(), colorous::YELLOW_GREEN_BLUE); map.insert( "yellowOrangeBrown".to_string(), colorous::YELLOW_ORANGE_BROWN, ); + map +}); + +static REVERSE_COLOR_PALETTES: Lazy> = Lazy::new(|| { + let mut map = HashMap::new(); + map.insert("blues".to_string(), colorous::BLUES); + map.insert("greenBlue".to_string(), colorous::GREEN_BLUE); + map.insert("greens".to_string(), colorous::GREENS); + map.insert("greys".to_string(), colorous::GREYS); + map.insert("orangeRed".to_string(), colorous::ORANGE_RED); + map.insert("oranges".to_string(), colorous::ORANGES); + map.insert("pinkGreen".to_string(), colorous::PINK_GREEN); + map.insert("purpleBlueGreen".to_string(), colorous::PURPLE_BLUE_GREEN); + map.insert("purpleRed".to_string(), colorous::PURPLE_RED); + map.insert("purples".to_string(), colorous::PURPLES); + map.insert("redPurple".to_string(), colorous::RED_PURPLE); + map.insert("reds".to_string(), colorous::REDS); + map.insert("yellowGreen".to_string(), colorous::YELLOW_GREEN); + map.insert("yellowGreenBlue".to_string(), colorous::YELLOW_GREEN_BLUE); map.insert("yellowOrangeRed".to_string(), colorous::YELLOW_ORANGE_RED); + map }); @@ -145,15 +148,22 @@ pub fn get_tile( exponent: u32, image_side_length: usize, color_scheme: String, - reverse_colors: bool, + _reverse_colors: bool, ) -> Vec { + let mut reverse_colors = _reverse_colors; + let min_channel_value = 0; let max_channel_value = 255; - let palette = if color_scheme != "turbo" && COLOROUS_PALETTES.contains_key(&color_scheme) { - COLOROUS_PALETTES.get(&color_scheme).unwrap() - } else { - &colorous::TURBO - }; + let mut palette = &colorous::TURBO; + + if COLOR_PALETTES.contains_key(&color_scheme) { + palette = COLOR_PALETTES.get(&color_scheme).unwrap(); + } + + if REVERSE_COLOR_PALETTES.contains_key(&color_scheme) { + palette = REVERSE_COLOR_PALETTES.get(&color_scheme).unwrap(); + reverse_colors = !reverse_colors; + } let output_size: usize = image_side_length * image_side_length * NUM_COLOR_CHANNELS;