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