From 83573c8936c6b6009c1f9dfbc91fb1b6df6c5f38 Mon Sep 17 00:00:00 2001
From: barrulus
Date: Fri, 15 Aug 2025 19:00:03 +0100
Subject: [PATCH] Enhance population counting accuracy and add comprehensive
geoJSON exports
Population System Fixes:
- Fix province, culture, and religion population calculations to exclude rural population from cells without burgs
- Burgs now represent ALL population for their cells (both urban and rural components)
- Eliminate double-counting where cell populations were incorrectly added to burg populations
Export Enhancements:
- Add complete geoJSON export for burgs with all settlement properties
- Enhance routes geoJSON export to include type and feature metadata
- Add missing length and width properties to rivers geoJSON export
- Fix burg coordinate system to match CSV export format with xWorld/yWorld fields
UI Improvements:
- Add burgs export button to geoJSON export interface
- Fix vite module loading issue by adding type="module" to notes-editor.js script tag
Documentation:
- Create comprehensive QGIS Style Conversion guide with route types, burg features, and relief rendering methods
- Add WKT coordinate reference system definition for Fantasy Map Cartesian CRS
- Include rule-based styling examples and data processing workflows
---
QGIS Style Conversion from Fantasy Map.md | 622 ++++++++++++++++++++
index.html | 1 +
modules/dynamic/editors/cultures-editor.js | 4 +-
modules/dynamic/editors/religions-editor.js | 4 +-
modules/io/export.js | 73 ++-
modules/ui/provinces-editor.js | 4 +-
6 files changed, 695 insertions(+), 13 deletions(-)
create mode 100644 QGIS Style Conversion from Fantasy Map.md
diff --git a/QGIS Style Conversion from Fantasy Map.md b/QGIS Style Conversion from Fantasy Map.md
new file mode 100644
index 00000000..bee14236
--- /dev/null
+++ b/QGIS Style Conversion from Fantasy Map.md
@@ -0,0 +1,622 @@
+# QGIS Style Conversion from Fantasy Map JSON
+
+## Overview
+
+This document converts the fantasy map styling JSON to QGIS-compatible styles. The original JSON contains SVG/CSS-style properties that need to be translated to QGIS symbology.
+
+## Layer Style Conversions
+
+### Water Bodies
+
+#### Rivers (`#rivers`)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Freshwater Lakes (`#freshwater`)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Ocean Base (`#oceanBase`)
+
+```xml
+
+
+
+
+
+
+
+```
+
+### Landmass and Terrain
+
+#### Landmass (`#landmass`)
+
+```xml
+
+
+
+
+
+
+
+```
+
+#### Ice (`#ice`)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### Relief and Terrain
+
+The Fantasy Map Generator uses SVG icons placed based on elevation to create relief effects. QGIS can replicate this using several approaches:
+
+#### Method 1: Hillshade from Elevation Data
+
+Create a Digital Elevation Model (DEM) from the cells GeoJSON data:
+
+1. **Import cells GeoJSON** with elevation data in the `height` property
+2. **Convert to raster**: Vector → Conversion Tools → Rasterize
+ - Use `height` field for raster values
+ - Set appropriate resolution (e.g., 100m)
+3. **Generate hillshade**: Raster → Analysis → Hillshade
+ - Z factor: 1.0
+ - Azimuth: 315° (northwest)
+ - Altitude: 45°
+
+```xml
+
+
+
+ SingleBandGray
+ 0
+ false
+ 1
+
+
+```
+
+#### Method 2: Icon-Based Relief (Fantasy Map Style)
+
+Replicate the original icon-based relief using rule-based point symbols:
+
+**Step 1**: Create point layer from cell centroids
+- Vector → Geometry Tools → Centroids
+- Filter: `"height" >= 50` (land areas only)
+
+**Step 2**: Configure rule-based symbology
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Step 3**: Rule expressions
+- Mountains: `"height" > 70`
+- Hills: `"height" >= 50 AND "height" <= 70`
+
+#### Method 3: Density-Controlled Relief Points
+
+For scattered relief icons (matching FMG's Poisson distribution):
+
+**Step 1**: Use Geometry Generator with point symbols
+- Symbol type: Point
+- Geometry type: Point
+- Expression for scattered points:
+
+```sql
+-- Generate multiple points per cell based on elevation
+CASE
+ WHEN "height" > 70 THEN
+ -- Mountains: 2-4 points per cell
+ array_to_string(
+ array_foreach(
+ generate_series(1, floor("height"/30)),
+ point_on_surface(
+ translate($geometry,
+ rand(-50,50),
+ rand(-50,50)
+ )
+ )
+ ), ','
+ )
+ WHEN "height" >= 50 THEN
+ -- Hills: 1-2 points per cell
+ point_on_surface($geometry)
+ ELSE
+ NULL
+END
+```
+
+#### Method 4: Hybrid Approach (Recommended)
+
+Combine multiple techniques for best results:
+
+1. **Base layer**: Hillshade raster (opacity 30%)
+2. **Mid layer**: Graduated cell polygons by elevation
+3. **Top layer**: Scattered point symbols for major peaks
+
+**Graduated Elevation Symbology**:
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Relief Color Ramps
+
+For elevation-based coloring:
+
+**Height Classes**:
+- 0-20: Ocean (blue tones)
+- 20-40: Lowlands (green tones)
+- 40-60: Hills (yellow-brown tones)
+- 60-80: Mountains (brown tones)
+- 80+: High peaks (gray-white tones)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### Political Boundaries
+
+#### State Borders (`#stateBorders`)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Province Borders (`#provinceBorders`)
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+### Transportation
+
+Routes can be styled based on their `type` property using QGIS rule-based styling. The available route types are:
+
+- **royal**: Major roads connecting capitals and important cities
+- **market**: Trade routes connecting market towns
+- **local**: Secondary roads for regional connectivity
+- **footpath**: Walking trails and paths
+- **majorSea**: Major shipping routes between ports
+
+#### Royal Roads (type = 'royal')
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Market Roads (type = 'market')
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Local Roads (type = 'local')
+
+```xml
+
+
+
+
+
+
+
+
+```
+
+#### Footpaths (type = 'footpath')
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Major Sea Routes (type = 'majorSea')
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Legacy Route Groups
+
+For backward compatibility, routes can also be styled by `group` property:
+
+- **roads**: All land-based routes (royal, market, local)
+- **trails**: Walking paths (footpath)
+- **searoutes**: All sea-based routes (majorSea)
+
+### Settlements
+
+Burgs can be styled based on their boolean properties using QGIS rule-based styling. The available burg feature types are:
+
+- **capital**: State capitals (administrative centers)
+- **port**: Coastal settlements with harbors
+- **citadel**: Fortified settlements with citadels
+- **walls**: Settlements with defensive walls
+- **plaza**: Settlements with central plazas
+- **temple**: Settlements with religious temples
+- **shanty**: Settlements with shanty town districts
+
+#### Capital Cities (capital = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Port Cities (port = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Fortified Cities (citadel = true OR walls = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Religious Centers (temple = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Trading Centers (plaza = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Shanty Towns (shanty = true)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Regular Settlements (default)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Settlement Labels
+
+```xml
+
+
+
+
+
+
+
+```
+
+## Implementation Steps
+
+### 1. Create Layer Structure
+
+```
+Project Root/
+├── Water Bodies/
+│ ├── Rivers
+│ ├── Lakes
+│ └── Ocean
+├── Landmass/
+│ ├── Base Land
+│ └── Ice
+├── Relief & Terrain/
+│ ├── Elevation DEM (raster)
+│ ├── Hillshade (raster)
+│ ├── Relief Icons (points)
+│ └── Elevation Polygons
+├── Political/
+│ ├── State Borders
+│ └── Province Borders
+├── Transportation/
+│ ├── Routes (by type)
+│ └── Routes (by group)
+└── Settlements/
+ ├── Burgs (by feature type)
+ └── Settlement Labels
+```
+
+### 2. Apply Styles in QGIS
+
+1. **Load your vector layers** into QGIS
+2. **Right-click layer** → Properties → Symbology
+3. **Copy the XML** from above into a text editor
+4. **Save as .qml file** (e.g., `rivers.qml`)
+5. **Load style** in layer properties → Style → Load Style
+
+### 3. Color Reference Table
+
+|Original Color|RGB Values|QGIS Color Code|
+|---|---|---|
+|`#000000`|0,0,0|`0,0,0,255`|
+|`#0089ca`|0,137,202|`0,137,202,255`|
+|`#cae3f7`|202,227,247|`202,227,247,255`|
+|`#eef6fb`|238,246,251|`238,246,251,255`|
+|`#ff2c2c`|255,44,44|`255,44,44,255`|
+|`#9f5122`|159,81,34|`159,81,34,255`|
+|`#b4d2f3`|180,210,243|`180,210,243,255`|
+
+### 4. Layer Ordering (Bottom to Top)
+
+1. Ocean Base
+2. Landmass
+3. Elevation DEM (raster)
+4. Hillshade (30% opacity)
+5. Elevation Polygons (graduated colors)
+6. Freshwater Bodies
+7. Ice
+8. Province Borders
+9. State Borders
+10. Transportation Routes (by type or group)
+11. Relief Icons (mountains/hills)
+12. Settlement Icons (by feature type)
+13. Settlement Labels
+
+## Notes
+
+- **Opacity values** from the JSON (like 0.8, 0.9) translate to QGIS alpha values
+- **Stroke-dasharray** properties become custom dash patterns in QGIS
+- **Filter effects** like blur and drop shadows need to be recreated using QGIS effects
+- **Font families** may need substitution if not available in your system
+- **Coordinate system** should be set to the Fantasy Map Cartesian CRS (WKT format below)
+
+## Coordinate Reference System (CRS)
+
+The Fantasy Map Generator uses a custom Cartesian coordinate system. Use this WKT definition in QGIS:
+
+```
+ENGCRS["Fantasy Map Cartesian (meters)",
+ EDATUM["Fantasy Map Datum"],
+ CS[Cartesian,2],
+ AXIS["easting (X)",east,
+ ORDER[1],
+ LENGTHUNIT["metre",1]],
+ AXIS["northing (Y)",north,
+ ORDER[2],
+ LENGTHUNIT["metre",1]]]
+```
+
+### Setting up the CRS in QGIS:
+1. Go to **Settings** → **Custom Projections**
+2. Click **+** to add a new CRS
+3. Set **Name**: `Fantasy Map Cartesian`
+4. Set **Format**: `WKT (Recommended)`
+5. Paste the WKT definition above
+6. Click **OK** and **Apply**
+
+## Data Requirements for Relief
+
+### Essential GeoJSON Exports
+
+To implement relief rendering, you need these exports from Fantasy Map Generator:
+
+1. **Cells GeoJSON** (`cells.geojson`)
+ - Contains elevation data in `height` property
+ - Provides cell polygons for DEM generation
+ - Includes biome information for terrain variation
+
+2. **Burgs GeoJSON** (`burgs.geojson`)
+ - Settlement locations for reference
+ - Population data for symbol sizing
+
+### Processing Workflow
+
+1. **Import cells.geojson** into QGIS
+2. **Create DEM raster**: Vector → Conversion Tools → Rasterize
+ - Field: `height`
+ - Resolution: 50-200m (depending on map detail)
+ - Output extent: Use layer extent
+3. **Generate hillshade**: Raster → Analysis → Hillshade
+4. **Create relief points**: Vector → Geometry Tools → Centroids
+5. **Apply symbology** using the styles below
+
+## Rule-Based Styling
+
+### Route Styling by Type
+To style routes by their `type` property:
+1. Right-click route layer → Properties → Symbology
+2. Change from "Single Symbol" to "Rule-based"
+3. Add rules with expressions like: `"type" = 'royal'`
+4. Apply the corresponding symbol for each type
+
+### Burg Styling by Features
+To style burgs by their feature properties:
+1. Right-click burg layer → Properties → Symbology
+2. Change to "Rule-based" styling
+3. Create rules with expressions like:
+ - `"capital" = 1` for capitals
+ - `"port" = 1` for ports
+ - `"citadel" = 1 OR "walls" = 1` for fortified cities
+4. Set priority order (capitals first, then ports, etc.)
+
+### Graduated Symbols by Population
+To size burg symbols by population:
+1. Right-click burg layer → Properties → Symbology
+2. Change to "Graduated"
+3. Set **Value**: `population`
+4. Choose appropriate **Method** and **Classes**
+5. Adjust symbol sizes in the range
+
+## Advanced Features
+
+For complex effects like the texture overlay (`#texture`) and fogging (`#fogging`), consider:
+
+- Using **Raster layers** with blend modes
+- **Layer effects** in symbology
+- **Custom SVG symbols** for complex markers
+- **Expression-based styling** for dynamic effects
\ No newline at end of file
diff --git a/index.html b/index.html
index f3e9db21..fff89677 100644
--- a/index.html
+++ b/index.html
@@ -6051,6 +6051,7 @@
+
GeoJSON format is used in GIS tools such as QGIS. Check out
diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js
index faf0c38c..2de31f78 100644
--- a/modules/dynamic/editors/cultures-editor.js
+++ b/modules/dynamic/editors/cultures-editor.js
@@ -113,10 +113,8 @@ function culturesCollectStatistics() {
if (burgId) {
// Burg represents ALL population for this cell (stored in thousands)
cultures[cultureId].urban += burgs[burgId].population;
- } else {
- // Only count cells.pop for unsettled areas (no burg present)
- cultures[cultureId].rural += cells.pop[i];
}
+ // No population in cells without burgs - all population is in burgs
}
}
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index f73bbce6..68dc8540 100644
--- a/modules/dynamic/editors/religions-editor.js
+++ b/modules/dynamic/editors/religions-editor.js
@@ -123,10 +123,8 @@ function religionsCollectStatistics() {
if (burgId) {
// Burg represents ALL population for this cell (stored in thousands)
religions[religionId].urban += burgs[burgId].population;
- } else {
- // Only count cells.pop for unsettled areas (no burg present)
- religions[religionId].rural += cells.pop[i];
}
+ // No population in cells without burgs - all population is in burgs
}
}
diff --git a/modules/io/export.js b/modules/io/export.js
index 6c259552..5cc06069 100644
--- a/modules/io/export.js
+++ b/modules/io/export.js
@@ -652,12 +652,12 @@ function saveGeoJsonCells() {
function saveGeoJsonRoutes() {
const metersPerPixel = getMetersPerPixel();
- const features = pack.routes.map(({i, points, group, name = null}) => {
+ const features = pack.routes.map(({i, points, group, name = null, type, feature}) => {
const coordinates = points.map(([x, y]) => getFantasyCoordinates(x, y, 2));
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
- properties: {id: i, group, name}
+ properties: {id: i, group, name, type, feature}
};
});
@@ -682,14 +682,14 @@ function saveGeoJsonRoutes() {
function saveGeoJsonRivers() {
const metersPerPixel = getMetersPerPixel();
const features = pack.rivers.map(
- ({i, cells, points, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}) => {
+ ({i, cells, points, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, length, width, name, type}) => {
if (!cells || cells.length < 2) return;
const meanderedPoints = Rivers.addMeandering(cells, points);
const coordinates = meanderedPoints.map(([x, y]) => getFantasyCoordinates(x, y, 2));
return {
type: "Feature",
geometry: {type: "LineString", coordinates},
- properties: {id: i, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, name, type}
+ properties: {id: i, source, mouth, parent, basin, widthFactor, sourceWidth, discharge, length, width, name, type}
};
}
).filter(f => f); // Remove undefined entries
@@ -749,4 +749,69 @@ function saveGeoJsonMarkers() {
const fileName = getFileName("Markers") + ".geojson";
downloadFile(JSON.stringify(json), fileName, "application/json");
+}
+
+function saveGeoJsonBurgs() {
+ const metersPerPixel = getMetersPerPixel();
+ const valid = pack.burgs.filter(b => b.i && !b.removed);
+
+ const features = valid.map(b => {
+ const coordinates = getFantasyCoordinates(b.x, b.y, 2);
+ const province = pack.cells.province[b.cell];
+ const temperature = grid.cells.temp[pack.cells.g[b.cell]];
+
+ // Calculate world coordinates same as CSV export
+ const xWorld = b.x * metersPerPixel;
+ const yWorld = -b.y * metersPerPixel;
+
+ return {
+ type: "Feature",
+ geometry: {type: "Point", coordinates},
+ properties: {
+ id: b.i,
+ name: b.name,
+ province: province ? pack.provinces[province].name : null,
+ provinceFull: province ? pack.provinces[province].fullName : null,
+ state: pack.states[b.state].name,
+ stateFull: pack.states[b.state].fullName,
+ culture: pack.cultures[b.culture].name,
+ religion: pack.religions[pack.cells.religion[b.cell]].name,
+ population: rn(b.population * populationRate * urbanization),
+ populationRaw: b.population,
+ xWorld: rn(xWorld, 2),
+ yWorld: rn(yWorld, 2),
+ xPixel: b.x,
+ yPixel: b.y,
+ elevation: parseInt(getHeight(pack.cells.h[b.cell])),
+ temperature: convertTemperature(temperature),
+ temperatureLikeness: getTemperatureLikeness(temperature),
+ capital: !!b.capital,
+ port: !!b.port,
+ citadel: !!b.citadel,
+ walls: !!b.walls,
+ plaza: !!b.plaza,
+ temple: !!b.temple,
+ shanty: !!b.shanty,
+ emblem: b.coa || null,
+ cell: b.cell
+ }
+ };
+ });
+
+ const json = {
+ type: "FeatureCollection",
+ features,
+ metadata: {
+ crs: "Fantasy Map Cartesian (meters)",
+ mapName: mapName.value,
+ scale: {
+ distance: distanceScale,
+ unit: distanceUnitInput.value,
+ meters_per_pixel: metersPerPixel
+ }
+ }
+ };
+
+ const fileName = getFileName("Burgs") + ".geojson";
+ downloadFile(JSON.stringify(json), fileName, "application/json");
}
\ No newline at end of file
diff --git a/modules/ui/provinces-editor.js b/modules/ui/provinces-editor.js
index 954c8e2b..a4e781cc 100644
--- a/modules/ui/provinces-editor.js
+++ b/modules/ui/provinces-editor.js
@@ -91,10 +91,8 @@ function editProvinces() {
// Burg represents ALL population for this cell (stored in thousands)
provinces[p].urban += burgs[cells.burg[i]].population;
provinces[p].burgs.push(cells.burg[i]);
- } else {
- // Only count cells.pop for unsettled areas (no burg present)
- provinces[p].rural += cells.pop[i];
}
+ // No population in cells without burgs - all population is in burgs
}
provinces.forEach(p => {