Skip to content

Commit 0ff0311

Browse files
JoeMcMahon87Joe McMahonAzgaar
authored
Adding zone export to GeoJSON, added versioning and hash updates (#1312)
* 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>
1 parent b872256 commit 0ff0311

5 files changed

Lines changed: 471 additions & 8 deletions

File tree

package-lock.json

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/modules/io/export.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,121 @@ function saveGeoJsonMarkers() {
574574
const fileName = getFileName("Markers") + ".geojson";
575575
downloadFile(JSON.stringify(json), fileName, "application/json");
576576
}
577+
578+
function saveGeoJsonZones() {
579+
const {zones, cells, vertices} = pack;
580+
const json = {type: "FeatureCollection", features: []};
581+
582+
// Helper function to convert zone cells to polygon coordinates
583+
// Handles multiple disconnected components and holes properly
584+
function getZonePolygonCoordinates(zoneCells) {
585+
const cellsInZone = new Set(zoneCells);
586+
const ofSameType = (cellId) => cellsInZone.has(cellId);
587+
const ofDifferentType = (cellId) => !cellsInZone.has(cellId);
588+
589+
const checkedCells = new Set();
590+
const rings = []; // Array of LinearRings (each ring is an array of coordinates)
591+
592+
// Find all boundary components by tracing each connected region
593+
for (const cellId of zoneCells) {
594+
if (checkedCells.has(cellId)) continue;
595+
596+
// Check if this cell is on the boundary (has a neighbor outside the zone)
597+
const neighbors = cells.c[cellId];
598+
const onBorder = neighbors.some(ofDifferentType);
599+
if (!onBorder) continue;
600+
601+
// Check if this is an inner lake (hole) - skip if so
602+
const feature = pack.features[cells.f[cellId]];
603+
if (feature.type === "lake" && feature.shoreline) {
604+
if (feature.shoreline.every(ofSameType)) continue;
605+
}
606+
607+
// Find a starting vertex that's on the boundary
608+
const cellVertices = cells.v[cellId];
609+
let startingVertex = null;
610+
611+
for (const vertexId of cellVertices) {
612+
const vertexCells = vertices.c[vertexId];
613+
if (vertexCells.some(ofDifferentType)) {
614+
startingVertex = vertexId;
615+
break;
616+
}
617+
}
618+
619+
if (startingVertex === null) continue;
620+
621+
// Use connectVertices to trace the boundary (reusing existing logic)
622+
const vertexChain = connectVertices({
623+
vertices,
624+
startingVertex,
625+
ofSameType,
626+
addToChecked: (cellId) => checkedCells.add(cellId),
627+
closeRing: false, // We'll close it manually after converting to coordinates
628+
});
629+
630+
if (vertexChain.length < 3) continue;
631+
632+
// Convert vertex chain to coordinates
633+
const coordinates = [];
634+
for (const vertexId of vertexChain) {
635+
const [x, y] = vertices.p[vertexId];
636+
coordinates.push(getCoordinates(x, y, 4));
637+
}
638+
639+
// Close the ring (first coordinate = last coordinate)
640+
if (coordinates.length > 0) {
641+
coordinates.push(coordinates[0]);
642+
}
643+
644+
// Only add ring if it has at least 4 positions (minimum for valid LinearRing)
645+
if (coordinates.length >= 4) {
646+
rings.push(coordinates);
647+
}
648+
}
649+
650+
return rings;
651+
}
652+
653+
// Filter and process zones
654+
zones.forEach(zone => {
655+
// Exclude hidden zones and zones with no cells
656+
if (zone.hidden || !zone.cells || zone.cells.length === 0) return;
657+
658+
const rings = getZonePolygonCoordinates(zone.cells);
659+
660+
// Skip if no valid rings were generated
661+
if (rings.length === 0) return;
662+
663+
const properties = {
664+
id: zone.i,
665+
name: zone.name,
666+
type: zone.type,
667+
color: zone.color,
668+
cells: zone.cells
669+
};
670+
671+
// If there's only one ring, use Polygon geometry
672+
if (rings.length === 1) {
673+
const feature = {
674+
type: "Feature",
675+
geometry: {type: "Polygon", coordinates: rings},
676+
properties
677+
};
678+
json.features.push(feature);
679+
} else {
680+
// Multiple disconnected components: use MultiPolygon
681+
// Each component is wrapped in its own array
682+
const multiPolygonCoordinates = rings.map(ring => [ring]);
683+
const feature = {
684+
type: "Feature",
685+
geometry: {type: "MultiPolygon", coordinates: multiPolygonCoordinates},
686+
properties
687+
};
688+
json.features.push(feature);
689+
}
690+
});
691+
692+
const fileName = getFileName("Zones") + ".geojson";
693+
downloadFile(JSON.stringify(json), fileName, "application/json");
694+
}

public/versioning.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
1414
*/
1515

16-
const VERSION = "1.112.3";
16+
const VERSION = "1.112.4";
1717
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
1818

1919
{
@@ -49,6 +49,7 @@ if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format o
4949
<li>New routes generation algorithm</li>
5050
<li>Routes overview tool</li>
5151
<li>Configurable longitude</li>
52+
<li>Export zones to GeoJSON</li>
5253
</ul>
5354
5455
<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>

src/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6152,6 +6152,7 @@
61526152
<button onclick="saveGeoJsonRoutes()" data-tip="Download routes data in GeoJSON format">routes</button>
61536153
<button onclick="saveGeoJsonRivers()" data-tip="Download rivers data in GeoJSON format">rivers</button>
61546154
<button onclick="saveGeoJsonMarkers()" data-tip="Download markers data in GeoJSON format">markers</button>
6155+
<button onclick="saveGeoJsonZones()" data-tip="Download zones data in GeoJSON format">zones</button>
61556156
</div>
61566157
<p>
61576158
GeoJSON format is used in GIS tools such as QGIS. Check out
@@ -8553,6 +8554,6 @@
85538554
<script defer src="modules/io/save.js?v=1.111.0"></script>
85548555
<script defer src="modules/io/load.js?v=1.111.0"></script>
85558556
<script defer src="modules/io/cloud.js?v=1.106.0"></script>
8556-
<script defer src="modules/io/export.js?v=1.108.13"></script>
8557+
<script defer src="modules/io/export.js?v=1.112.2"></script>
85578558
</body>
85588559
</html>

0 commit comments

Comments
 (0)