mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2026-03-22 15:17:23 +01:00
Adding zone export to GeoJSON, added versioning and hash updates (#1312)
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Code quality / quality (push) Has been cancelled
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
Code quality / quality (push) Has been cancelled
* Adding zone export to GeoJSON, added versioning and hash updates * Fixing copilot findings and test not using production code call * Correcting collection of disconnected features --------- Co-authored-by: Joe McMahon <joe@mcmahongroup.org> Co-authored-by: Azgaar <maxganiev@yandex.com>
This commit is contained in:
parent
b87225665e
commit
0ff0311a98
5 changed files with 471 additions and 8 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -1353,7 +1353,6 @@
|
||||||
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1394,7 +1393,6 @@
|
||||||
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
|
"integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/browser": "4.0.18",
|
"@vitest/browser": "4.0.18",
|
||||||
"@vitest/mocker": "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",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -2163,7 +2160,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2475,7 +2471,6 @@
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -2551,7 +2546,6 @@
|
||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.0.18",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.0.18",
|
||||||
|
|
|
||||||
|
|
@ -574,3 +574,121 @@ function saveGeoJsonMarkers() {
|
||||||
const fileName = getFileName("Markers") + ".geojson";
|
const fileName = getFileName("Markers") + ".geojson";
|
||||||
downloadFile(JSON.stringify(json), fileName, "application/json");
|
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");
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = "1.112.3";
|
const VERSION = "1.112.4";
|
||||||
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
|
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>New routes generation algorithm</li>
|
||||||
<li>Routes overview tool</li>
|
<li>Routes overview tool</li>
|
||||||
<li>Configurable longitude</li>
|
<li>Configurable longitude</li>
|
||||||
|
<li>Export zones to GeoJSON</li>
|
||||||
</ul>
|
</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>
|
<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="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="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="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>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
GeoJSON format is used in GIS tools such as QGIS. Check out
|
GeoJSON format is used in GIS tools such as QGIS. Check out
|
||||||
|
|
@ -8553,6 +8554,6 @@
|
||||||
<script defer src="modules/io/save.js?v=1.111.0"></script>
|
<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/load.js?v=1.111.0"></script>
|
||||||
<script defer src="modules/io/cloud.js?v=1.106.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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue