mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-25 00:27:24 +01:00
Merge branch 'master' into refactor/migrate-military-generator
This commit is contained in:
commit
3f282c74d0
15 changed files with 2885 additions and 1470 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -1353,7 +1353,6 @@
|
|||
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
|
|
@ -1394,7 +1393,6 @@
|
|||
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
|
|
@ -1876,7 +1874,6 @@
|
|||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -2163,7 +2160,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2475,7 +2471,6 @@
|
|||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -2551,7 +2546,6 @@
|
|||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
|
|
|
|||
|
|
@ -574,3 +574,121 @@ function saveGeoJsonMarkers() {
|
|||
const fileName = getFileName("Markers") + ".geojson";
|
||||
downloadFile(JSON.stringify(json), fileName, "application/json");
|
||||
}
|
||||
|
||||
function saveGeoJsonZones() {
|
||||
const {zones, cells, vertices} = pack;
|
||||
const json = {type: "FeatureCollection", features: []};
|
||||
|
||||
// Helper function to convert zone cells to polygon coordinates
|
||||
// Handles multiple disconnected components and holes properly
|
||||
function getZonePolygonCoordinates(zoneCells) {
|
||||
const cellsInZone = new Set(zoneCells);
|
||||
const ofSameType = (cellId) => cellsInZone.has(cellId);
|
||||
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
|
||||
|
||||
const checkedCells = new Set();
|
||||
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
|
||||
|
||||
// Find all boundary components by tracing each connected region
|
||||
for (const cellId of zoneCells) {
|
||||
if (checkedCells.has(cellId)) continue;
|
||||
|
||||
// Check if this cell is on the boundary (has a neighbor outside the zone)
|
||||
const neighbors = cells.c[cellId];
|
||||
const onBorder = neighbors.some(ofDifferentType);
|
||||
if (!onBorder) continue;
|
||||
|
||||
// Check if this is an inner lake (hole) - skip if so
|
||||
const feature = pack.features[cells.f[cellId]];
|
||||
if (feature.type === "lake" && feature.shoreline) {
|
||||
if (feature.shoreline.every(ofSameType)) continue;
|
||||
}
|
||||
|
||||
// Find a starting vertex that's on the boundary
|
||||
const cellVertices = cells.v[cellId];
|
||||
let startingVertex = null;
|
||||
|
||||
for (const vertexId of cellVertices) {
|
||||
const vertexCells = vertices.c[vertexId];
|
||||
if (vertexCells.some(ofDifferentType)) {
|
||||
startingVertex = vertexId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startingVertex === null) continue;
|
||||
|
||||
// Use connectVertices to trace the boundary (reusing existing logic)
|
||||
const vertexChain = connectVertices({
|
||||
vertices,
|
||||
startingVertex,
|
||||
ofSameType,
|
||||
addToChecked: (cellId) => checkedCells.add(cellId),
|
||||
closeRing: false, // We'll close it manually after converting to coordinates
|
||||
});
|
||||
|
||||
if (vertexChain.length < 3) continue;
|
||||
|
||||
// Convert vertex chain to coordinates
|
||||
const coordinates = [];
|
||||
for (const vertexId of vertexChain) {
|
||||
const [x, y] = vertices.p[vertexId];
|
||||
coordinates.push(getCoordinates(x, y, 4));
|
||||
}
|
||||
|
||||
// Close the ring (first coordinate = last coordinate)
|
||||
if (coordinates.length > 0) {
|
||||
coordinates.push(coordinates[0]);
|
||||
}
|
||||
|
||||
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
|
||||
if (coordinates.length >= 4) {
|
||||
rings.push(coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
return rings;
|
||||
}
|
||||
|
||||
// Filter and process zones
|
||||
zones.forEach(zone => {
|
||||
// Exclude hidden zones and zones with no cells
|
||||
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;
|
||||
|
||||
const rings = getZonePolygonCoordinates(zone.cells);
|
||||
|
||||
// Skip if no valid rings were generated
|
||||
if (rings.length === 0) return;
|
||||
|
||||
const properties = {
|
||||
id: zone.i,
|
||||
name: zone.name,
|
||||
type: zone.type,
|
||||
color: zone.color,
|
||||
cells: zone.cells
|
||||
};
|
||||
|
||||
// If there's only one ring, use Polygon geometry
|
||||
if (rings.length === 1) {
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
geometry: {type: "Polygon", coordinates: rings},
|
||||
properties
|
||||
};
|
||||
json.features.push(feature);
|
||||
} else {
|
||||
// Multiple disconnected components: use MultiPolygon
|
||||
// Each component is wrapped in its own array
|
||||
const multiPolygonCoordinates = rings.map(ring => [ring]);
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
geometry: {type: "MultiPolygon", coordinates: multiPolygonCoordinates},
|
||||
properties
|
||||
};
|
||||
json.features.push(feature);
|
||||
}
|
||||
});
|
||||
|
||||
const fileName = getFileName("Zones") + ".geojson";
|
||||
downloadFile(JSON.stringify(json), fileName, "application/json");
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -330,15 +330,11 @@ function editHeightmap(options) {
|
|||
c.y = p[1];
|
||||
}
|
||||
|
||||
// recalculate zones to grid
|
||||
zones.selectAll("g").each(function () {
|
||||
const zone = d3.select(this);
|
||||
const dataCells = zone.attr("data-cells");
|
||||
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
|
||||
const g = cells.map(i => pack.cells.g[i]);
|
||||
zone.attr("data-cells", g);
|
||||
zone.selectAll("*").remove();
|
||||
});
|
||||
const zoneGridCellsMap = new Map();
|
||||
for (const zone of pack.zones) {
|
||||
if (!zone.cells || !zone.cells.length) continue;
|
||||
zoneGridCellsMap.set(zone.i, zone.cells.map(i => pack.cells.g[i]));
|
||||
}
|
||||
|
||||
Features.markupGrid();
|
||||
if (erosionAllowed) addLakesInDeepDepressions();
|
||||
|
|
@ -448,24 +444,18 @@ function editHeightmap(options) {
|
|||
Lakes.defineNames();
|
||||
}
|
||||
|
||||
// restore zones from grid
|
||||
zones.selectAll("g").each(function () {
|
||||
const zone = d3.select(this);
|
||||
const g = zone.attr("data-cells");
|
||||
const gCells = g ? g.split(",").map(i => +i) : [];
|
||||
const cells = pack.cells.i.filter(i => gCells.includes(pack.cells.g[i]));
|
||||
const gridToPackMap = new Map();
|
||||
for (const i of pack.cells.i) {
|
||||
const g = pack.cells.g[i];
|
||||
if (!gridToPackMap.has(g)) gridToPackMap.set(g, []);
|
||||
gridToPackMap.get(g).push(i);
|
||||
}
|
||||
|
||||
zone.attr("data-cells", cells);
|
||||
zone.selectAll("*").remove();
|
||||
const base = zone.attr("id") + "_"; // id generic part
|
||||
zone
|
||||
.selectAll("*")
|
||||
.data(cells)
|
||||
.enter()
|
||||
.append("polygon")
|
||||
.attr("points", d => getPackPolygon(d))
|
||||
.attr("id", d => base + d);
|
||||
});
|
||||
for (const zone of pack.zones) {
|
||||
const gridCells = zoneGridCellsMap.get(zone.i);
|
||||
if (!gridCells || !gridCells.length) continue;
|
||||
zone.cells = gridCells.flatMap(g => gridToPackMap.get(g) || []);
|
||||
}
|
||||
|
||||
// recalculate ice
|
||||
Ice.generate();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||
*/
|
||||
|
||||
const VERSION = "1.112.1";
|
||||
const VERSION = "1.112.4";
|
||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
||||
|
||||
{
|
||||
|
|
@ -49,6 +49,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
|
|||
<li>New routes generation algorithm</li>
|
||||
<li>Routes overview tool</li>
|
||||
<li>Configurable longitude</li>
|
||||
<li>Export zones to GeoJSON</li>
|
||||
</ul>
|
||||
|
||||
<p>Join our <a href="${discord}" target="_blank">Discord server</a> and <a href="${reddit}" target="_blank">Reddit community</a> to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.</p>
|
||||
|
|
|
|||
|
|
@ -6152,6 +6152,7 @@
|
|||
<button onclick="saveGeoJsonRoutes()" data-tip="Download routes data in GeoJSON format">routes</button>
|
||||
<button onclick="saveGeoJsonRivers()" data-tip="Download rivers data in GeoJSON format">rivers</button>
|
||||
<button onclick="saveGeoJsonMarkers()" data-tip="Download markers data in GeoJSON format">markers</button>
|
||||
<button onclick="saveGeoJsonZones()" data-tip="Download zones data in GeoJSON format">zones</button>
|
||||
</div>
|
||||
<p>
|
||||
GeoJSON format is used in GIS tools such as QGIS. Check out
|
||||
|
|
@ -8494,13 +8495,11 @@
|
|||
|
||||
<script defer src="config/heightmap-templates.js"></script>
|
||||
<script defer src="config/precreated-heightmaps.js"></script>
|
||||
<script defer src="modules/markers-generator.js?v=1.107.0"></script>
|
||||
<script defer src="modules/resample.js?v=1.112.1"></script>
|
||||
<script defer src="libs/alea.min.js?v1.105.0"></script>
|
||||
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
|
||||
<script defer src="libs/lineclip.min.js?v1.105.0"></script>
|
||||
<script defer src="libs/simplify.js?v1.105.6"></script>
|
||||
<script defer src="modules/fonts.js?v=1.99.03"></script>
|
||||
<script defer src="modules/ui/layers.js?v=1.111.0"></script>
|
||||
<script defer src="modules/ui/measurers.js?v=1.99.00"></script>
|
||||
<script defer src="modules/ui/style-presets.js?v=1.100.00"></script>
|
||||
|
|
@ -8512,7 +8511,7 @@
|
|||
<script defer src="modules/ui/editors.js?v=1.112.1"></script>
|
||||
<script defer src="modules/ui/tools.js?v=1.111.0"></script>
|
||||
<script defer src="modules/ui/world-configurator.js?v=1.105.4"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.105.2"></script>
|
||||
<script defer src="modules/ui/heightmap-editor.js?v=1.112.2"></script>
|
||||
<script defer src="modules/ui/provinces-editor.js?v=1.108.1"></script>
|
||||
<script defer src="modules/ui/biomes-editor.js?v=1.112.0"></script>
|
||||
<script defer src="modules/ui/namesbase-editor.js?v=1.105.11"></script>
|
||||
|
|
@ -8554,6 +8553,6 @@
|
|||
<script defer src="modules/io/save.js?v=1.111.0"></script>
|
||||
<script defer src="modules/io/load.js?v=1.111.0"></script>
|
||||
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
|
||||
<script defer src="modules/io/export.js?v=1.108.13"></script>
|
||||
<script defer src="modules/io/export.js?v=1.112.2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,272 +1,351 @@
|
|||
"use strict";
|
||||
import { byId } from "../utils";
|
||||
|
||||
const fonts = [
|
||||
{family: "Arial"},
|
||||
{family: "Brush Script MT"},
|
||||
{family: "Century Gothic"},
|
||||
{family: "Comic Sans MS"},
|
||||
{family: "Copperplate"},
|
||||
{family: "Courier New"},
|
||||
{family: "Garamond"},
|
||||
{family: "Georgia"},
|
||||
{family: "Herculanum"},
|
||||
{family: "Impact"},
|
||||
{family: "Papyrus"},
|
||||
{family: "Party LET"},
|
||||
{family: "Times New Roman"},
|
||||
{family: "Verdana"},
|
||||
declare global {
|
||||
var declareFont: (font: FontDefinition) => void;
|
||||
var getUsedFonts: (svg: SVGSVGElement) => FontDefinition[];
|
||||
var loadFontsAsDataURI: (
|
||||
fonts: FontDefinition[],
|
||||
) => Promise<FontDefinition[]>;
|
||||
var addGoogleFont: (family: string) => Promise<void>;
|
||||
var addLocalFont: (family: string) => void;
|
||||
var addWebFont: (family: string, src: string) => void;
|
||||
var fonts: FontDefinition[];
|
||||
}
|
||||
|
||||
type FontDefinition = {
|
||||
family: string;
|
||||
src?: string;
|
||||
unicodeRange?: string;
|
||||
variant?: string;
|
||||
};
|
||||
|
||||
window.fonts = [
|
||||
{ family: "Arial" },
|
||||
{ family: "Brush Script MT" },
|
||||
{ family: "Century Gothic" },
|
||||
{ family: "Comic Sans MS" },
|
||||
{ family: "Copperplate" },
|
||||
{ family: "Courier New" },
|
||||
{ family: "Garamond" },
|
||||
{ family: "Georgia" },
|
||||
{ family: "Herculanum" },
|
||||
{ family: "Impact" },
|
||||
{ family: "Papyrus" },
|
||||
{ family: "Party LET" },
|
||||
{ family: "Times New Roman" },
|
||||
{ family: "Verdana" },
|
||||
{
|
||||
family: "Almendra SC",
|
||||
src: "url(https://fonts.gstatic.com/s/almendrasc/v13/Iure6Yx284eebowr7hbyTaZOrLQ.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Amarante",
|
||||
src: "url(https://fonts.gstatic.com/s/amarante/v22/xMQXuF1KTa6EvGx9bp-wAXs.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Amatic SC",
|
||||
src: "url(https://fonts.gstatic.com/s/amaticsc/v11/TUZ3zwprpvBS1izr_vOMscGKfrUC.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Arima Madurai",
|
||||
src: "url(https://fonts.gstatic.com/s/arimamadurai/v14/t5tmIRoeKYORG0WNMgnC3seB3T7Prw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Architects Daughter",
|
||||
src: "url(https://fonts.gstatic.com/s/architectsdaughter/v8/RXTgOOQ9AAtaVOHxx0IUBM3t7GjCYufj5TXV5VnA2p8.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Bitter",
|
||||
src: "url(https://fonts.gstatic.com/s/bitter/v12/zfs6I-5mjWQ3nxqccMoL2A.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Caesar Dressing",
|
||||
src: "url(https://fonts.gstatic.com/s/caesardressing/v6/yYLx0hLa3vawqtwdswbotmK4vrRHdrz7.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Cinzel",
|
||||
src: "url(https://fonts.gstatic.com/s/cinzel/v7/zOdksD_UUTk1LJF9z4tURA.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Dancing Script",
|
||||
src: "url(https://fonts.gstatic.com/s/dancingscript/v9/KGBfwabt0ZRLA5W1ywjowUHdOuSHeh0r6jGTOGdAKHA.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Eagle Lake",
|
||||
src: "url(https://fonts.gstatic.com/s/eaglelake/v24/ptRMTiqbbuNJDOiKj9wG1On4KCFtpe4.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Faster One",
|
||||
src: "url(https://fonts.gstatic.com/s/fasterone/v17/H4ciBXCHmdfClFb-vWhf-LyYhw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Forum",
|
||||
src: "url(https://fonts.gstatic.com/s/forum/v16/6aey4Ky-Vb8Ew8IROpI.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Fredericka the Great",
|
||||
src: "url(https://fonts.gstatic.com/s/frederickathegreat/v6/9Bt33CxNwt7aOctW2xjbCstzwVKsIBVV--Sjxbc.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Gloria Hallelujah",
|
||||
src: "url(https://fonts.gstatic.com/s/gloriahallelujah/v9/CA1k7SlXcY5kvI81M_R28cNDay8z-hHR7F16xrcXsJw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Great Vibes",
|
||||
src: "url(https://fonts.gstatic.com/s/greatvibes/v5/6q1c0ofG6NKsEhAc2eh-3Y4P5ICox8Kq3LLUNMylGO4.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Henny Penny",
|
||||
src: "url(https://fonts.gstatic.com/s/hennypenny/v17/wXKvE3UZookzsxz_kjGSfPQtvXI.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "IM Fell English",
|
||||
src: "url(https://fonts.gstatic.com/s/imfellenglish/v7/xwIisCqGFi8pff-oa9uSVAkYLEKE0CJQa8tfZYc_plY.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Kelly Slab",
|
||||
src: "url(https://fonts.gstatic.com/s/kellyslab/v15/-W_7XJX0Rz3cxUnJC5t6fkQLfg.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Kranky",
|
||||
src: "url(https://fonts.gstatic.com/s/kranky/v24/hESw6XVgJzlPsFn8oR2F.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Lobster Two",
|
||||
src: "url(https://fonts.gstatic.com/s/lobstertwo/v18/BngMUXZGTXPUvIoyV6yN5-fN5qU.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Lugrasimo",
|
||||
src: "url(https://fonts.gstatic.com/s/lugrasimo/v4/qkBXXvoF_s_eT9c7Y7au455KsgbLMA.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Kaushan Script",
|
||||
src: "url(https://fonts.gstatic.com/s/kaushanscript/v6/qx1LSqts-NtiKcLw4N03IEd0sm1ffa_JvZxsF_BEwQk.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Macondo",
|
||||
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "MedievalSharp",
|
||||
src: "url(https://fonts.gstatic.com/s/medievalsharp/v9/EvOJzAlL3oU5AQl2mP5KdgptMqhwMg.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Metal Mania",
|
||||
src: "url(https://fonts.gstatic.com/s/metalmania/v22/RWmMoKWb4e8kqMfBUdPFJdXFiaQ.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Metamorphous",
|
||||
src: "url(https://fonts.gstatic.com/s/metamorphous/v7/Wnz8HA03aAXcC39ZEX5y133EOyqs.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Montez",
|
||||
src: "url(https://fonts.gstatic.com/s/montez/v8/aq8el3-0osHIcFK6bXAPkw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Nova Script",
|
||||
src: "url(https://fonts.gstatic.com/s/novascript/v10/7Au7p_IpkSWSTWaFWkumvlQKGFw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Orbitron",
|
||||
src: "url(https://fonts.gstatic.com/s/orbitron/v9/HmnHiRzvcnQr8CjBje6GQvesZW2xOQ-xsNqO47m55DA.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Oregano",
|
||||
src: "url(https://fonts.gstatic.com/s/oregano/v13/If2IXTPxciS3H4S2oZDVPg.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Pirata One",
|
||||
src: "url(https://fonts.gstatic.com/s/pirataone/v22/I_urMpiDvgLdLh0fAtofhi-Org.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Sail",
|
||||
src: "url(https://fonts.gstatic.com/s/sail/v16/DPEjYwiBxwYJJBPJAQ.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Satisfy",
|
||||
src: "url(https://fonts.gstatic.com/s/satisfy/v8/2OzALGYfHwQjkPYWELy-cw.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Shadows Into Light",
|
||||
src: "url(https://fonts.gstatic.com/s/shadowsintolight/v7/clhLqOv7MXn459PTh0gXYFK2TSYBz0eNcHnp4YqE4Ts.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
{
|
||||
family: "Tapestry",
|
||||
src: "url(https://fonts.gstatic.com/s/macondo/v21/RrQQboN9-iB1IXmOe2LE0Q.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Uncial Antiqua",
|
||||
src: "url(https://fonts.gstatic.com/s/uncialantiqua/v5/N0bM2S5WOex4OUbESzoESK-i-MfWQZQ.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Underdog",
|
||||
src: "url(https://fonts.gstatic.com/s/underdog/v6/CHygV-jCElj7diMroWSlWV8.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "UnifrakturMaguntia",
|
||||
src: "url(https://fonts.gstatic.com/s/unifrakturmaguntia/v16/WWXPlieVYwiGNomYU-ciRLRvEmK7oaVemGZM.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD",
|
||||
},
|
||||
{
|
||||
family: "Yellowtail",
|
||||
src: "url(https://fonts.gstatic.com/s/yellowtail/v8/GcIHC9QEwVkrA19LJU1qlPk_vArhqVIZ0nv9q090hN8.woff2)",
|
||||
unicodeRange:
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215"
|
||||
}
|
||||
"U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215",
|
||||
},
|
||||
];
|
||||
|
||||
declareDefaultFonts(); // execute once on load
|
||||
|
||||
function declareFont(font) {
|
||||
const {family, src, ...rest} = font;
|
||||
window.declareFont = (font: FontDefinition) => {
|
||||
const { family, src, ...rest } = font;
|
||||
addFontOption(family);
|
||||
|
||||
if (!src) return;
|
||||
const fontFace = new FontFace(family, src, {...rest, display: "block"});
|
||||
const fontFace = new FontFace(family, src, { ...rest, display: "block" });
|
||||
document.fonts.add(fontFace);
|
||||
}
|
||||
};
|
||||
|
||||
declareDefaultFonts(); // execute once on load
|
||||
|
||||
function declareDefaultFonts() {
|
||||
fonts.forEach(font => declareFont(font));
|
||||
fonts.forEach((font) => {
|
||||
declareFont(font);
|
||||
});
|
||||
}
|
||||
|
||||
function getUsedFonts(svg) {
|
||||
function addFontOption(family: string) {
|
||||
const options = document.getElementById("styleSelectFont")!;
|
||||
const option = document.createElement("option");
|
||||
option.value = family;
|
||||
option.innerText = family;
|
||||
option.style.fontFamily = family;
|
||||
options.append(option);
|
||||
}
|
||||
|
||||
async function fetchGoogleFont(family: string) {
|
||||
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
const text = await resp.text();
|
||||
|
||||
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
|
||||
const fonts = fontFaceRules!.map((fontFace) => {
|
||||
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)?.[1];
|
||||
const src = `url(${srcURL})`;
|
||||
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
|
||||
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
|
||||
|
||||
const font: FontDefinition = { family, src };
|
||||
if (unicodeRange) font.unicodeRange = unicodeRange;
|
||||
if (variant && variant !== "normal") font.variant = variant;
|
||||
return font;
|
||||
});
|
||||
|
||||
return fonts;
|
||||
} catch (err) {
|
||||
ERROR && console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readBlobAsDataURL(blob: Blob) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
window.loadFontsAsDataURI = async (fonts: FontDefinition[]) => {
|
||||
const promises = fonts.map(async (font) => {
|
||||
const url = font.src?.match(/url\(['"]?(.+?)['"]?\)/)?.[1];
|
||||
if (!url) return font;
|
||||
const resp = await fetch(url);
|
||||
const blob = await resp.blob();
|
||||
const dataURL = await readBlobAsDataURL(blob);
|
||||
|
||||
return { ...font, src: `url('${dataURL}')` };
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
};
|
||||
|
||||
window.getUsedFonts = (svg: SVGSVGElement) => {
|
||||
const usedFontFamilies = new Set();
|
||||
|
||||
const labelGroups = svg.querySelectorAll("#labels g");
|
||||
|
|
@ -282,112 +361,66 @@ function getUsedFonts(svg) {
|
|||
const legendFont = legend?.getAttribute("font-family");
|
||||
if (legendFont) usedFontFamilies.add(legendFont);
|
||||
|
||||
const usedFonts = fonts.filter(font => usedFontFamilies.has(font.family));
|
||||
const usedFonts = fonts.filter((font) => usedFontFamilies.has(font.family));
|
||||
return usedFonts;
|
||||
}
|
||||
};
|
||||
|
||||
function addFontOption(family) {
|
||||
const options = document.getElementById("styleSelectFont");
|
||||
const option = document.createElement("option");
|
||||
option.value = family;
|
||||
option.innerText = family;
|
||||
option.style.fontFamily = family;
|
||||
options.add(option);
|
||||
}
|
||||
|
||||
async function fetchGoogleFont(family) {
|
||||
const url = `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}`;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
const text = await resp.text();
|
||||
|
||||
const fontFaceRules = text.match(/font-face\s*{[^}]+}/g);
|
||||
const fonts = fontFaceRules.map(fontFace => {
|
||||
const srcURL = fontFace.match(/url\(['"]?(.+?)['"]?\)/)[1];
|
||||
const src = `url(${srcURL})`;
|
||||
const unicodeRange = fontFace.match(/unicode-range: (.*?);/)?.[1];
|
||||
const variant = fontFace.match(/font-style: (.*?);/)?.[1];
|
||||
|
||||
const font = {family, src};
|
||||
if (unicodeRange) font.unicodeRange = unicodeRange;
|
||||
if (variant && variant !== "normal") font.variant = variant;
|
||||
return font;
|
||||
});
|
||||
|
||||
return fonts;
|
||||
} catch (err) {
|
||||
ERROR && console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readBlobAsDataURL(blob) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFontsAsDataURI(fonts) {
|
||||
const promises = fonts.map(async font => {
|
||||
const url = font.src.match(/url\(['"]?(.+?)['"]?\)/)[1];
|
||||
const resp = await fetch(url);
|
||||
const blob = await resp.blob();
|
||||
const dataURL = await readBlobAsDataURL(blob);
|
||||
|
||||
return {...font, src: `url('${dataURL}')`};
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
async function addGoogleFont(family) {
|
||||
window.addGoogleFont = async (family: string) => {
|
||||
const fontRanges = await fetchGoogleFont(family);
|
||||
if (!fontRanges) return tip("Cannot fetch Google font for this value", true, "error", 4000);
|
||||
if (!fontRanges)
|
||||
return tip("Cannot fetch Google font for this value", true, "error", 4000);
|
||||
tip(`Google font ${family} is loading...`, true, "warn", 4000);
|
||||
|
||||
const promises = fontRanges.map(range => {
|
||||
const {src, unicodeRange, variant} = range;
|
||||
const fontFace = new FontFace(family, src, {unicodeRange, variant, display: "block"});
|
||||
const promises = fontRanges.map((range) => {
|
||||
const { src, unicodeRange } = range;
|
||||
const fontFace = new FontFace(family, src!, {
|
||||
unicodeRange,
|
||||
display: "block",
|
||||
});
|
||||
return fontFace.load();
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.then(fontFaces => {
|
||||
fontFaces.forEach(fontFace => document.fonts.add(fontFace));
|
||||
.then((fontFaces) => {
|
||||
fontFaces.forEach((fontFace) => {
|
||||
document.fonts.add(fontFace);
|
||||
});
|
||||
fonts.push(...fontRanges);
|
||||
tip(`Google font ${family} is added to the list`, true, "success", 4000);
|
||||
addFontOption(family);
|
||||
document.getElementById("styleSelectFont").value = family;
|
||||
const select = byId<HTMLSelectElement>("styleSelectFont");
|
||||
if (select) select.value = family;
|
||||
changeFont();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
tip(`Failed to load Google font ${family}`, true, "error", 4000);
|
||||
ERROR && console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function addLocalFont(family) {
|
||||
fonts.push({family});
|
||||
window.addLocalFont = (family: string) => {
|
||||
fonts.push({ family });
|
||||
|
||||
const fontFace = new FontFace(family, `local(${family})`, {display: "block"});
|
||||
const fontFace = new FontFace(family, `local(${family})`, {
|
||||
display: "block",
|
||||
});
|
||||
document.fonts.add(fontFace);
|
||||
tip(`Local font ${family} is added to the fonts list`, true, "success", 4000);
|
||||
addFontOption(family);
|
||||
document.getElementById("styleSelectFont").value = family;
|
||||
const select = byId<HTMLSelectElement>("styleSelectFont");
|
||||
if (select) select.value = family;
|
||||
changeFont();
|
||||
}
|
||||
};
|
||||
|
||||
function addWebFont(family, url) {
|
||||
window.addWebFont = (family: string, url: string) => {
|
||||
const src = `url('${url}')`;
|
||||
fonts.push({family, src});
|
||||
fonts.push({ family, src });
|
||||
|
||||
const fontFace = new FontFace(family, src, {display: "block"});
|
||||
const fontFace = new FontFace(family, src, { display: "block" });
|
||||
document.fonts.add(fontFace);
|
||||
tip(`Font ${family} is added to the list`, true, "success", 4000);
|
||||
addFontOption(family);
|
||||
document.getElementById("styleSelectFont").value = family;
|
||||
const select = byId<HTMLSelectElement>("styleSelectFont");
|
||||
if (select) select.value = family;
|
||||
changeFont();
|
||||
}
|
||||
};
|
||||
|
|
@ -16,3 +16,5 @@ import "./provinces-generator";
|
|||
import "./emblem";
|
||||
import "./ice";
|
||||
import "./military-generator";
|
||||
import "./markers-generator";
|
||||
import "./fonts";
|
||||
|
|
|
|||
1802
src/modules/markers-generator.ts
Normal file
1802
src/modules/markers-generator.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -482,6 +482,12 @@ class MilitaryModule {
|
|||
|
||||
return regiments as MilitaryRegiment[];
|
||||
};
|
||||
|
||||
// remove all existing regiment notes before regenerating
|
||||
for (let i = notes.length - 1; i >= 0; i--) {
|
||||
if (notes[i].id.startsWith("regiment")) notes.splice(i, 1);
|
||||
}
|
||||
|
||||
// get regiments for each state
|
||||
valid.forEach((s) => {
|
||||
s.military = createRegiments(s.temp.platoons, s);
|
||||
|
|
@ -594,7 +600,14 @@ class MilitaryModule {
|
|||
: gauss(options.year - 100, 150, 1, options.year - 6);
|
||||
const conflict = campaign ? ` during the ${campaign.name}` : "";
|
||||
const legend = `Regiment was formed in ${year} ${options.era}${conflict}. ${station}${troops}`;
|
||||
notes.push({ id: `regiment${s.i}-${r.i}`, name: r.name, legend });
|
||||
const id = `regiment${s.i}-${r.i}`;
|
||||
const existing = notes.find(n => n.id === id);
|
||||
if (existing) {
|
||||
existing.name = r.name;
|
||||
existing.legend = legend;
|
||||
} else {
|
||||
notes.push({id, name: r.name, legend});
|
||||
}
|
||||
}
|
||||
|
||||
// get default regiment emblem
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ declare global {
|
|||
var mapName: HTMLInputElement;
|
||||
var religionsNumber: HTMLInputElement;
|
||||
var distanceUnitInput: HTMLInputElement;
|
||||
var heightUnit: HTMLSelectElement;
|
||||
|
||||
var rivers: Selection<SVGElement, unknown, null, undefined>;
|
||||
var oceanLayers: Selection<SVGGElement, unknown, null, undefined>;
|
||||
|
|
@ -46,6 +47,7 @@ declare global {
|
|||
var defs: Selection<SVGDefsElement, unknown, null, undefined>;
|
||||
var coastline: Selection<SVGGElement, unknown, null, undefined>;
|
||||
var lakes: Selection<SVGGElement, unknown, null, undefined>;
|
||||
var provs: Selection<SVGGElement, unknown, null, undefined>;
|
||||
var getColorScheme: (scheme: string | null) => (t: number) => string;
|
||||
var getColor: (height: number, scheme: (t: number) => string) => string;
|
||||
var svgWidth: number;
|
||||
|
|
@ -78,10 +80,13 @@ declare global {
|
|||
var tip: (
|
||||
message: string,
|
||||
autoHide?: boolean,
|
||||
type?: "info" | "warning" | "error",
|
||||
type?: "info" | "warn" | "error" | "success",
|
||||
timeout?: number,
|
||||
) => void;
|
||||
var locked: (settingId: string) => boolean;
|
||||
var unlock: (settingId: string) => void;
|
||||
var $: (selector: any) => any;
|
||||
var scale: number;
|
||||
var changeFont: () => void;
|
||||
var getFriendlyHeight: (coords: [number, number]) => string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const byId = document.getElementById.bind(document);
|
||||
export const byId = <T extends HTMLElement>(id: string): T | undefined =>
|
||||
document.getElementById(id) as T;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
|
|||
221
tests/e2e/load-map.spec.ts
Normal file
221
tests/e2e/load-map.spec.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import path from "path";
|
||||
|
||||
test.describe("Map loading", () => {
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await context.clearCookies();
|
||||
|
||||
await page.goto("/");
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
// Wait for the hidden file input to be available
|
||||
await page.waitForSelector("#mapToLoad", { state: "attached" });
|
||||
});
|
||||
|
||||
test("should load a saved map file", async ({ page }) => {
|
||||
// Track errors during map loading
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
errors.push(`console.error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the file input element and upload the map file
|
||||
const fileInput = page.locator("#mapToLoad");
|
||||
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
|
||||
await fileInput.setInputFiles(mapFilePath);
|
||||
|
||||
// Wait for map to be fully loaded
|
||||
// mapId is set at the very end of map loading in showStatistics()
|
||||
await page.waitForFunction(() => (window as any).mapId !== undefined, {
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
// Additional wait for rendering to settle
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify map data is loaded
|
||||
const mapData = await page.evaluate(() => {
|
||||
const pack = (window as any).pack;
|
||||
return {
|
||||
hasStates: pack.states && pack.states.length > 1,
|
||||
hasBurgs: pack.burgs && pack.burgs.length > 1,
|
||||
hasCells: pack.cells && pack.cells.i && pack.cells.i.length > 0,
|
||||
hasRivers: pack.rivers && pack.rivers.length > 0,
|
||||
mapId: (window as any).mapId,
|
||||
};
|
||||
});
|
||||
|
||||
expect(mapData.hasStates).toBe(true);
|
||||
expect(mapData.hasBurgs).toBe(true);
|
||||
expect(mapData.hasCells).toBe(true);
|
||||
expect(mapData.hasRivers).toBe(true);
|
||||
expect(mapData.mapId).toBeDefined();
|
||||
|
||||
// Ensure no JavaScript errors occurred during loading
|
||||
// Filter out expected errors (external resources like Google Analytics, fonts)
|
||||
const criticalErrors = errors.filter(
|
||||
(e) =>
|
||||
!e.includes("fonts.googleapis.com") &&
|
||||
!e.includes("google-analytics") &&
|
||||
!e.includes("googletagmanager") &&
|
||||
!e.includes("Failed to load resource")
|
||||
);
|
||||
expect(criticalErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("loaded map should have correct SVG structure", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
errors.push(`console.error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = page.locator("#mapToLoad");
|
||||
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
|
||||
await fileInput.setInputFiles(mapFilePath);
|
||||
|
||||
await page.waitForFunction(() => (window as any).mapId !== undefined, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check essential SVG layers exist
|
||||
const layers = await page.evaluate(() => {
|
||||
return {
|
||||
ocean: !!document.getElementById("ocean"),
|
||||
lakes: !!document.getElementById("lakes"),
|
||||
coastline: !!document.getElementById("coastline"),
|
||||
rivers: !!document.getElementById("rivers"),
|
||||
borders: !!document.getElementById("borders"),
|
||||
burgs: !!document.getElementById("burgIcons"),
|
||||
labels: !!document.getElementById("labels"),
|
||||
};
|
||||
});
|
||||
|
||||
expect(layers.ocean).toBe(true);
|
||||
expect(layers.lakes).toBe(true);
|
||||
expect(layers.coastline).toBe(true);
|
||||
expect(layers.rivers).toBe(true);
|
||||
expect(layers.borders).toBe(true);
|
||||
expect(layers.burgs).toBe(true);
|
||||
expect(layers.labels).toBe(true);
|
||||
|
||||
const criticalErrors = errors.filter(
|
||||
(e) =>
|
||||
!e.includes("fonts.googleapis.com") &&
|
||||
!e.includes("google-analytics") &&
|
||||
!e.includes("googletagmanager") &&
|
||||
!e.includes("Failed to load resource")
|
||||
);
|
||||
expect(criticalErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("loaded map should preserve state data", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
errors.push(`console.error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = page.locator("#mapToLoad");
|
||||
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
|
||||
await fileInput.setInputFiles(mapFilePath);
|
||||
|
||||
await page.waitForFunction(() => (window as any).mapId !== undefined, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify states have proper structure
|
||||
const statesData = await page.evaluate(() => {
|
||||
const pack = (window as any).pack;
|
||||
const states = pack.states.filter((s: any) => s.i !== 0); // exclude neutral
|
||||
|
||||
return {
|
||||
count: states.length,
|
||||
allHaveNames: states.every((s: any) => s.name && s.name.length > 0),
|
||||
allHaveCells: states.every((s: any) => s.cells > 0),
|
||||
allHaveArea: states.every((s: any) => s.area > 0),
|
||||
};
|
||||
});
|
||||
|
||||
expect(statesData.count).toBeGreaterThan(0);
|
||||
expect(statesData.allHaveNames).toBe(true);
|
||||
expect(statesData.allHaveCells).toBe(true);
|
||||
expect(statesData.allHaveArea).toBe(true);
|
||||
|
||||
const criticalErrors = errors.filter(
|
||||
(e) =>
|
||||
!e.includes("fonts.googleapis.com") &&
|
||||
!e.includes("google-analytics") &&
|
||||
!e.includes("googletagmanager") &&
|
||||
!e.includes("Failed to load resource")
|
||||
);
|
||||
expect(criticalErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("loaded map should preserve burg data", async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
errors.push(`console.error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = page.locator("#mapToLoad");
|
||||
const mapFilePath = path.join(__dirname, "../fixtures/demo.map");
|
||||
await fileInput.setInputFiles(mapFilePath);
|
||||
|
||||
await page.waitForFunction(() => (window as any).mapId !== undefined, {
|
||||
timeout: 120000,
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Verify burgs have proper structure
|
||||
const burgsData = await page.evaluate(() => {
|
||||
const pack = (window as any).pack;
|
||||
// Filter out placeholder (i=0) and removed burgs (removed=true or no name)
|
||||
const activeBurgs = pack.burgs.filter(
|
||||
(b: any) => b.i !== 0 && !b.removed && b.name
|
||||
);
|
||||
|
||||
return {
|
||||
count: activeBurgs.length,
|
||||
allHaveNames: activeBurgs.every(
|
||||
(b: any) => b.name && b.name.length > 0
|
||||
),
|
||||
allHaveCoords: activeBurgs.every(
|
||||
(b: any) => typeof b.x === "number" && typeof b.y === "number"
|
||||
),
|
||||
allHaveCells: activeBurgs.every(
|
||||
(b: any) => typeof b.cell === "number"
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
expect(burgsData.count).toBeGreaterThan(0);
|
||||
expect(burgsData.allHaveNames).toBe(true);
|
||||
expect(burgsData.allHaveCoords).toBe(true);
|
||||
expect(burgsData.allHaveCells).toBe(true);
|
||||
|
||||
const criticalErrors = errors.filter(
|
||||
(e) =>
|
||||
!e.includes("fonts.googleapis.com") &&
|
||||
!e.includes("google-analytics") &&
|
||||
!e.includes("googletagmanager") &&
|
||||
!e.includes("Failed to load resource")
|
||||
);
|
||||
expect(criticalErrors).toEqual([]);
|
||||
});
|
||||
});
|
||||
349
tests/e2e/zones-export.spec.ts
Normal file
349
tests/e2e/zones-export.spec.ts
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Zone Export", () => {
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await context.clearCookies();
|
||||
|
||||
await page.goto("/");
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
// Navigate with seed parameter and wait for full load
|
||||
await page.goto("/?seed=test-zones-export&width=1280&height=720");
|
||||
|
||||
// Wait for map generation to complete
|
||||
await page.waitForFunction(
|
||||
() => (window as any).mapId !== undefined,
|
||||
{ timeout: 60000 }
|
||||
);
|
||||
|
||||
// Additional wait for any rendering/animations to settle
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
|
||||
// Helper function to create a test zone programmatically
|
||||
// Uses BFS to select a contiguous set of land cells for stable, representative testing
|
||||
async function createTestZone(page: any): Promise<number> {
|
||||
return await page.evaluate(() => {
|
||||
const { cells, zones } = (window as any).pack;
|
||||
|
||||
// Find a starting land cell (height >= 20)
|
||||
const totalCells = cells.i.length;
|
||||
let startCell = -1;
|
||||
for (let i = 1; i < totalCells; i++) {
|
||||
if (cells.h[i] >= 20) {
|
||||
startCell = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startCell === -1) {
|
||||
throw new Error("No land cells found to create a test zone");
|
||||
}
|
||||
|
||||
// Use BFS to select a contiguous set of 10-20 land cells
|
||||
const zoneCells: number[] = [];
|
||||
const visited = new Set<number>();
|
||||
const queue: number[] = [];
|
||||
|
||||
visited.add(startCell);
|
||||
queue.push(startCell);
|
||||
|
||||
while (queue.length > 0 && zoneCells.length < 20) {
|
||||
const current = queue.shift() as number;
|
||||
|
||||
// Only include land cells in the zone
|
||||
if (cells.h[current] >= 20) {
|
||||
zoneCells.push(current);
|
||||
}
|
||||
|
||||
// Explore neighbors
|
||||
const neighbors: number[] = cells.c[current] || [];
|
||||
for (const neighbor of neighbors) {
|
||||
if (neighbor && !visited.has(neighbor)) {
|
||||
visited.add(neighbor);
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneCells.length < 10) {
|
||||
throw new Error(`Not enough contiguous land cells found: ${zoneCells.length}`);
|
||||
}
|
||||
|
||||
// Generate unique zone ID
|
||||
const zoneId = zones.length;
|
||||
|
||||
// Create zone object
|
||||
const zone = {
|
||||
i: zoneId,
|
||||
name: "Test Export Zone",
|
||||
type: "Test",
|
||||
color: "#FF0000",
|
||||
cells: zoneCells,
|
||||
};
|
||||
|
||||
// Add zone to pack.zones array
|
||||
zones.push(zone);
|
||||
|
||||
return zoneId;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to export zones to GeoJSON without file download
|
||||
// This calls the production code from public/modules/io/export.js
|
||||
async function exportZonesToGeoJson(page: any): Promise<any> {
|
||||
return await page.evaluate(() => {
|
||||
// Mock downloadFile to capture the JSON instead of downloading
|
||||
const originalDownloadFile = (window as any).downloadFile;
|
||||
let capturedJson: any = null;
|
||||
|
||||
(window as any).downloadFile = (data: string) => {
|
||||
capturedJson = JSON.parse(data);
|
||||
};
|
||||
|
||||
// Call the production code
|
||||
(window as any).saveGeoJsonZones();
|
||||
|
||||
// Restore original downloadFile
|
||||
(window as any).downloadFile = originalDownloadFile;
|
||||
|
||||
return capturedJson;
|
||||
});
|
||||
}
|
||||
|
||||
test("should export zone with valid GeoJSON root structure", async ({ page }) => {
|
||||
// Create a test zone
|
||||
const zoneId = await createTestZone(page);
|
||||
expect(zoneId).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Export zones to GeoJSON
|
||||
const geoJson = await exportZonesToGeoJson(page);
|
||||
|
||||
// Validate root GeoJSON structure (Task 5.1)
|
||||
expect(geoJson).toBeDefined();
|
||||
expect(geoJson).toHaveProperty("type");
|
||||
expect(geoJson.type).toBe("FeatureCollection");
|
||||
|
||||
expect(geoJson).toHaveProperty("features");
|
||||
expect(Array.isArray(geoJson.features)).toBe(true);
|
||||
expect(geoJson.features.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify the test zone is in the export
|
||||
const testZoneFeature = geoJson.features.find((f: any) => f.properties.id === zoneId);
|
||||
expect(testZoneFeature).toBeDefined();
|
||||
expect(testZoneFeature.properties.name).toBe("Test Export Zone");
|
||||
|
||||
// Validate Feature structure (Task 5.2)
|
||||
expect(testZoneFeature).toHaveProperty("type");
|
||||
expect(testZoneFeature.type).toBe("Feature");
|
||||
|
||||
expect(testZoneFeature).toHaveProperty("geometry");
|
||||
expect(testZoneFeature.geometry).toBeDefined();
|
||||
expect(typeof testZoneFeature.geometry).toBe("object");
|
||||
|
||||
expect(testZoneFeature.geometry).toHaveProperty("type");
|
||||
// Note: Geometry type can be "Polygon" (single component) or "MultiPolygon" (multiple disconnected components)
|
||||
// For this test with contiguous BFS-selected cells, we expect "Polygon"
|
||||
expect(testZoneFeature.geometry.type).toBe("Polygon");
|
||||
|
||||
expect(testZoneFeature.geometry).toHaveProperty("coordinates");
|
||||
expect(Array.isArray(testZoneFeature.geometry.coordinates)).toBe(true);
|
||||
|
||||
expect(testZoneFeature).toHaveProperty("properties");
|
||||
expect(testZoneFeature.properties).toBeDefined();
|
||||
expect(typeof testZoneFeature.properties).toBe("object");
|
||||
|
||||
// Task 6.1: Validate zone property mapping
|
||||
// Get the test zone from pack.zones in browser context
|
||||
const testZone = await page.evaluate((id: number) => {
|
||||
const { zones } = (window as any).pack;
|
||||
return zones.find((z: any) => z.i === id);
|
||||
}, zoneId);
|
||||
|
||||
expect(testZone).toBeDefined();
|
||||
|
||||
// Assert feature.properties match zone properties
|
||||
expect(testZoneFeature.properties.id).toBe(testZone.i);
|
||||
expect(testZoneFeature.properties.name).toBe(testZone.name);
|
||||
expect(testZoneFeature.properties.type).toBe(testZone.type);
|
||||
expect(testZoneFeature.properties.color).toBe(testZone.color);
|
||||
expect(testZoneFeature.properties.cells).toEqual(testZone.cells);
|
||||
|
||||
// Task 7.1: Validate coordinate array structure
|
||||
const { coordinates } = testZoneFeature.geometry;
|
||||
|
||||
// Assert geometry.coordinates is an array
|
||||
expect(Array.isArray(coordinates)).toBe(true);
|
||||
|
||||
// Assert coordinates array is not empty
|
||||
expect(coordinates.length).toBeGreaterThan(0);
|
||||
|
||||
// Validate each LinearRing in the coordinates array
|
||||
// Note: Zones can have multiple rings (holes) or be MultiPolygon (disconnected components)
|
||||
for (const linearRing of coordinates) {
|
||||
// Assert LinearRing is an array
|
||||
expect(Array.isArray(linearRing)).toBe(true);
|
||||
|
||||
// Task 7.2: Validate LinearRing validity
|
||||
// Assert LinearRing has at least 4 positions
|
||||
expect(linearRing.length).toBeGreaterThanOrEqual(4);
|
||||
|
||||
// Assert first position equals last position (closed ring)
|
||||
const firstPosition = linearRing[0];
|
||||
const lastPosition = linearRing[linearRing.length - 1];
|
||||
expect(firstPosition[0]).toBe(lastPosition[0]);
|
||||
expect(firstPosition[1]).toBe(lastPosition[1]);
|
||||
|
||||
// Assert each position in LinearRing is an array of 2 numbers
|
||||
for (const position of linearRing) {
|
||||
expect(Array.isArray(position)).toBe(true);
|
||||
expect(position.length).toBe(2);
|
||||
expect(typeof position[0]).toBe("number");
|
||||
expect(typeof position[1]).toBe("number");
|
||||
|
||||
// Assert all positions are valid [longitude, latitude] pairs
|
||||
// Longitude should be between -180 and 180
|
||||
expect(position[0]).toBeGreaterThanOrEqual(-180);
|
||||
expect(position[0]).toBeLessThanOrEqual(180);
|
||||
|
||||
// Latitude should be between -90 and 90
|
||||
expect(position[1]).toBeGreaterThanOrEqual(-90);
|
||||
expect(position[1]).toBeLessThanOrEqual(90);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("should exclude hidden zones from GeoJSON export", async ({ page }) => {
|
||||
// Create a regular test zone
|
||||
const regularZoneId = await createTestZone(page);
|
||||
expect(regularZoneId).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Create a hidden zone
|
||||
const hiddenZoneId = await page.evaluate(() => {
|
||||
const { cells, zones } = (window as any).pack;
|
||||
|
||||
// Find a starting land cell that's not already in a zone
|
||||
const totalCells = cells.i.length;
|
||||
let startCell = -1;
|
||||
for (let i = 1; i < totalCells; i++) {
|
||||
const isLand = cells.h[i] >= 20;
|
||||
const notInZone = !zones.some((z: any) => z.cells && z.cells.includes(i));
|
||||
if (isLand && notInZone) {
|
||||
startCell = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startCell === -1) {
|
||||
throw new Error("No available land cells found for hidden zone");
|
||||
}
|
||||
|
||||
// Use BFS to select a contiguous set of 10-20 land cells
|
||||
const zoneCells: number[] = [];
|
||||
const visited = new Set<number>();
|
||||
const queue: number[] = [];
|
||||
|
||||
visited.add(startCell);
|
||||
queue.push(startCell);
|
||||
|
||||
while (queue.length > 0 && zoneCells.length < 20) {
|
||||
const current = queue.shift() as number;
|
||||
|
||||
// Only include land cells not already in a zone
|
||||
const isLand = cells.h[current] >= 20;
|
||||
const notInZone = !zones.some((z: any) => z.cells && z.cells.includes(current));
|
||||
if (isLand && notInZone) {
|
||||
zoneCells.push(current);
|
||||
}
|
||||
|
||||
// Explore neighbors
|
||||
const neighbors: number[] = cells.c[current] || [];
|
||||
for (const neighbor of neighbors) {
|
||||
if (neighbor && !visited.has(neighbor)) {
|
||||
visited.add(neighbor);
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneCells.length < 10) {
|
||||
throw new Error(`Not enough contiguous land cells found: ${zoneCells.length}`);
|
||||
}
|
||||
|
||||
// Generate unique zone ID
|
||||
const zoneId = zones.length;
|
||||
|
||||
// Create hidden zone object
|
||||
const zone = {
|
||||
i: zoneId,
|
||||
name: "Hidden Test Zone",
|
||||
type: "Test",
|
||||
color: "#00FF00",
|
||||
cells: zoneCells,
|
||||
hidden: true, // Mark as hidden
|
||||
};
|
||||
|
||||
// Add zone to pack.zones array
|
||||
zones.push(zone);
|
||||
|
||||
return zoneId;
|
||||
});
|
||||
expect(hiddenZoneId).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Export zones to GeoJSON
|
||||
const geoJson = await exportZonesToGeoJson(page);
|
||||
|
||||
// Validate that the regular zone is in the export
|
||||
const regularZoneFeature = geoJson.features.find((f: any) => f.properties.id === regularZoneId);
|
||||
expect(regularZoneFeature).toBeDefined();
|
||||
expect(regularZoneFeature.properties.name).toBe("Test Export Zone");
|
||||
|
||||
// Validate that the hidden zone is NOT in the export
|
||||
const hiddenZoneFeature = geoJson.features.find((f: any) => f.properties.id === hiddenZoneId);
|
||||
expect(hiddenZoneFeature).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should exclude zones with empty cells array from GeoJSON export", async ({ page }) => {
|
||||
// Create a regular test zone
|
||||
const regularZoneId = await createTestZone(page);
|
||||
expect(regularZoneId).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Create a zone with empty cells array
|
||||
const emptyZoneId = await page.evaluate(() => {
|
||||
const { zones } = (window as any).pack;
|
||||
|
||||
// Generate unique zone ID
|
||||
const zoneId = zones.length;
|
||||
|
||||
// Create zone object with empty cells array
|
||||
const zone = {
|
||||
i: zoneId,
|
||||
name: "Empty Test Zone",
|
||||
type: "Test",
|
||||
color: "#0000FF",
|
||||
cells: [], // Empty cells array
|
||||
};
|
||||
|
||||
// Add zone to pack.zones array
|
||||
zones.push(zone);
|
||||
|
||||
return zoneId;
|
||||
});
|
||||
expect(emptyZoneId).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Export zones to GeoJSON
|
||||
const geoJson = await exportZonesToGeoJson(page);
|
||||
|
||||
// Validate that the regular zone is in the export
|
||||
const regularZoneFeature = geoJson.features.find((f: any) => f.properties.id === regularZoneId);
|
||||
expect(regularZoneFeature).toBeDefined();
|
||||
expect(regularZoneFeature.properties.name).toBe("Test Export Zone");
|
||||
|
||||
// Validate that the empty zone is NOT in the export
|
||||
const emptyZoneFeature = geoJson.features.find((f: any) => f.properties.id === emptyZoneId);
|
||||
expect(emptyZoneFeature).toBeUndefined();
|
||||
});
|
||||
});
|
||||
174
tests/fixtures/demo.map
vendored
Normal file
174
tests/fixtures/demo.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue