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
This commit is contained in:
barrulus 2025-08-15 19:00:03 +01:00
parent 9e8bc6e689
commit 83573c8936
6 changed files with 695 additions and 13 deletions

View file

@ -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
<!-- QML Style for Rivers -->
<symbol alpha="1" type="line" name="rivers">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="0,137,202,255"/>
<prop k="width" v="0.8"/>
<prop k="capstyle" v="round"/>
</layer>
</symbol>
```
#### Freshwater Lakes (`#freshwater`)
```xml
<!-- QML Style for Freshwater -->
<symbol alpha="1" type="fill" name="freshwater">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="202,227,247,255"/>
<prop k="outline_color" v="0,137,202,255"/>
<prop k="outline_width" v="1.01"/>
</layer>
</symbol>
```
#### Ocean Base (`#oceanBase`)
```xml
<!-- QML Style for Ocean -->
<symbol alpha="1" type="fill" name="ocean">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="180,210,243,255"/>
<prop k="outline_style" v="no"/>
</layer>
</symbol>
```
### Landmass and Terrain
#### Landmass (`#landmass`)
```xml
<!-- QML Style for Landmass -->
<symbol alpha="1" type="fill" name="landmass">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="238,246,251,255"/>
<prop k="outline_style" v="no"/>
</layer>
</symbol>
```
#### Ice (`#ice`)
```xml
<!-- QML Style for Ice -->
<symbol alpha="0.9" type="fill" name="ice">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="232,240,246,255"/>
<prop k="outline_color" v="232,240,246,255"/>
<prop k="outline_width" v="1"/>
</layer>
</symbol>
```
### 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
<!-- Hillshade Layer Style -->
<rasterrenderer opacity="0.6" type="hillshade">
<rasterproperties>
<mDrawingStyle>SingleBandGray</mDrawingStyle>
<mColorShadingAlgorithm>0</mColorShadingAlgorithm>
<mInvertColor>false</mInvertColor>
<mGrayBand>1</mGrayBand>
</rasterproperties>
</rasterrenderer>
```
#### 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
<!-- Mountain Icons (height > 70) -->
<symbol alpha="1" type="marker" name="mountains">
<layer class="SvgMarker" enabled="1">
<prop k="size" v="8"/>
<prop k="name" v="mountain.svg"/>
<prop k="color" v="139,69,19,255"/>
<prop k="angle" v="0"/>
</layer>
</symbol>
<!-- Hill Icons (height 50-70) -->
<symbol alpha="1" type="marker" name="hills">
<layer class="SvgMarker" enabled="1">
<prop k="size" v="5"/>
<prop k="name" v="hill.svg"/>
<prop k="color" v="101,67,33,255"/>
<prop k="angle" v="0"/>
</layer>
</symbol>
```
**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
<!-- Low elevation -->
<symbol alpha="0.3" type="fill" name="low">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="144,238,144,76"/>
</layer>
</symbol>
<!-- High elevation -->
<symbol alpha="0.5" type="fill" name="high">
<layer class="SimpleFill" enabled="1">
<prop k="color" v="139,69,19,127"/>
</layer>
</symbol>
```
#### 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
<!-- Elevation Color Ramp -->
<colorrampshader colorRampType="INTERPOLATED">
<item alpha="255" value="0" color="#4A90E2" label="Ocean"/>
<item alpha="255" value="20" color="#90EE90" label="Coast"/>
<item alpha="255" value="40" color="#F4A460" label="Hills"/>
<item alpha="255" value="60" color="#8B4513" label="Mountains"/>
<item alpha="255" value="80" color="#696969" label="High Peaks"/>
</colorrampshader>
```
### Political Boundaries
#### State Borders (`#stateBorders`)
```xml
<!-- QML Style for State Borders -->
<symbol alpha="1" type="line" name="state_borders">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="0,0,0,255"/>
<prop k="width" v="1.01"/>
<prop k="capstyle" v="flat"/>
</layer>
</symbol>
```
#### Province Borders (`#provinceBorders`)
```xml
<!-- QML Style for Province Borders -->
<symbol alpha="0.8" type="line" name="province_borders">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="0,0,0,255"/>
<prop k="width" v="0.69"/>
<prop k="capstyle" v="round"/>
</layer>
</symbol>
```
### 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
<!-- QML Style for Royal Roads -->
<symbol alpha="1" type="line" name="royal_roads">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="255,44,44,255"/>
<prop k="width" v="1.2"/>
<prop k="capstyle" v="flat"/>
</layer>
</symbol>
```
#### Market Roads (type = 'market')
```xml
<!-- QML Style for Market Roads -->
<symbol alpha="1" type="line" name="market_roads">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="255,100,44,255"/>
<prop k="width" v="0.9"/>
<prop k="capstyle" v="flat"/>
</layer>
</symbol>
```
#### Local Roads (type = 'local')
```xml
<!-- QML Style for Local Roads -->
<symbol alpha="1" type="line" name="local_roads">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="200,100,50,255"/>
<prop k="width" v="0.6"/>
<prop k="capstyle" v="flat"/>
</layer>
</symbol>
```
#### Footpaths (type = 'footpath')
```xml
<!-- QML Style for Footpaths -->
<symbol alpha="1" type="line" name="footpaths">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="159,81,34,255"/>
<prop k="width" v="0.43"/>
<prop k="capstyle" v="flat"/>
<prop k="customdash" v="2;1"/>
<prop k="use_custom_dash" v="1"/>
</layer>
</symbol>
```
#### Major Sea Routes (type = 'majorSea')
```xml
<!-- QML Style for Major Sea Routes -->
<symbol alpha="1" type="line" name="major_searoutes">
<layer class="SimpleLine" enabled="1">
<prop k="color" v="0,137,202,255"/>
<prop k="width" v="0.6"/>
<prop k="capstyle" v="round"/>
<prop k="customdash" v="1;2"/>
<prop k="use_custom_dash" v="1"/>
</layer>
</symbol>
```
#### 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
<!-- QML Style for Capital Cities -->
<symbol alpha="1" type="marker" name="capitals">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="255,215,0,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.4"/>
<prop k="size" v="5"/>
<prop k="name" v="star"/>
</layer>
</symbol>
```
#### Port Cities (port = true)
```xml
<!-- QML Style for Port Cities -->
<symbol alpha="1" type="marker" name="ports">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="0,137,202,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.3"/>
<prop k="size" v="4"/>
<prop k="name" v="diamond"/>
</layer>
</symbol>
```
#### Fortified Cities (citadel = true OR walls = true)
```xml
<!-- QML Style for Fortified Cities -->
<symbol alpha="1" type="marker" name="fortified">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="139,69,19,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.3"/>
<prop k="size" v="4"/>
<prop k="name" v="square"/>
</layer>
</symbol>
```
#### Religious Centers (temple = true)
```xml
<!-- QML Style for Religious Centers -->
<symbol alpha="1" type="marker" name="religious">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="128,0,128,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.3"/>
<prop k="size" v="4"/>
<prop k="name" v="cross"/>
</layer>
</symbol>
```
#### Trading Centers (plaza = true)
```xml
<!-- QML Style for Trading Centers -->
<symbol alpha="1" type="marker" name="trading">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="255,165,0,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.3"/>
<prop k="size" v="3.5"/>
<prop k="name" v="pentagon"/>
</layer>
</symbol>
```
#### Shanty Towns (shanty = true)
```xml
<!-- QML Style for Shanty Towns -->
<symbol alpha="0.8" type="marker" name="shanty">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="139,131,120,255"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.2"/>
<prop k="size" v="2.5"/>
<prop k="name" v="triangle"/>
</layer>
</symbol>
```
#### Regular Settlements (default)
```xml
<!-- QML Style for Regular Settlements -->
<symbol alpha="0.7" type="marker" name="settlements">
<layer class="SimpleMarker" enabled="1">
<prop k="color" v="0,0,0,179"/>
<prop k="outline_color" v="0,0,0,255"/>
<prop k="outline_width" v="0.24"/>
<prop k="size" v="3"/>
<prop k="name" v="circle"/>
</layer>
</symbol>
```
#### Settlement Labels
```xml
<!-- QML Style for Settlement Labels -->
<text-style fontFamily="Arial" fontSize="5" fontSizeUnit="Point">
<text-color alpha="255" r="0" g="0" b="0"/>
<text-buffer bufferDraw="1" bufferSize="1" bufferSizeUnits="Point">
<buffer-color alpha="255" r="255" g="255" b="255"/>
</text-buffer>
</text-style>
```
## 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

View file

@ -6051,6 +6051,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="saveGeoJsonBurgs()" data-tip="Download burgs data in GeoJSON format">burgs</button>
</div>
<p>
GeoJSON format is used in GIS tools such as QGIS. Check out

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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");
}

View file

@ -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 => {