Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Template for new versions:

## New Tools

- `gui/export-world-map`: New GUI tool to configure world map exports from the embark selection screen.

## New Features

## Fixes
Expand Down
24 changes: 24 additions & 0 deletions docs/gui/export-world-map.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
gui/export-world-map
====================

.. dfhack-tool::
:summary: Export world map data for GIS and other external tools.
:tags: inspection embark map

This tool provides a GUI for the `export-world-map` plugin and a number of
additional exports written in Lua. Moreover, this tool automates the process of
scrolling around the embark map to generate the region tiles used for the
exports.

The additional exports provided are:

* Roads and tunnels
* Geological layers and the veins included in them (pure data layer)
* Animal and plant populations of regions (pure data layer)

Usage
-----

::

gui/export-world-map
219 changes: 219 additions & 0 deletions gui/export-world-map.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
---@diagnostic disable: missing-fields

local gui = require('gui')
local widgets = require('gui.widgets')

local roads = reqscript('internal/export-world-map/export-roads')
local layers = reqscript('internal/export-world-map/export-layers')
local pops = reqscript('internal/export-world-map/export-pops')

---@type df.viewscreen_choose_start_sitest
local viewscreen = dfhack.gui.getDFViewscreen(true)

if not df.viewscreen_choose_start_sitest:is_instance(viewscreen) then
qerror("Tool must be used while choosing an embark location.")
return
end

ExportMap = defclass(ExportMap, widgets.Window)
ExportMap.ATTRS {
frame_title='Export World Map',
frame={w=58, h=25},
resizable=false,
}

local function getLoadedTileRatio()
return #df.global.world.world_data.midmap_data.region_details,
df.global.world.world_data.world_width * df.global.world.world_data.world_height
end

function ExportMap:updateRatio()
cur,max = getLoadedTileRatio()
self.subviews.midmap_ratio:setText(
("%d out of %d region tiles loaded"):format(cur,max)
)
end

function ExportMap:initializeMapScan()
local pixel_x = df.global.gps.screen_pixel_x
local pixel_y = df.global.gps.screen_pixel_y

-- conservative approximation of screen dimensions in tiles
self.screen_w = (pixel_x // 16) - 2
self.screen_h = (pixel_y // 16) - 2

-- ensure that we are zoomed in
viewscreen.zoomed_in = true

viewscreen.zoom_cent_x = self.screen_w // 2
viewscreen.zoom_cent_y = self.screen_h // 2

self.done = false
end



-- keep scrolling around, until the entire world has been covered
function ExportMap:onRenderBody(painter)
self:updateRatio()
if self.done then
return
end
if viewscreen.zoom_cent_x + self.screen_w // 2 < df.global.world.world_data.world_width * 16 then
viewscreen.zoom_cent_x = viewscreen.zoom_cent_x + self.screen_w
elseif viewscreen.zoom_cent_y + self.screen_h // 2 < df.global.world.world_data.world_height * 16 then
viewscreen.zoom_cent_x = self.screen_w // 2
viewscreen.zoom_cent_y = viewscreen.zoom_cent_y + self.screen_h
else
self.done = true
end
end

---@param by_world boolean
---@param by_date boolean
---@param fn fun(boolean,boolean):(string|fun():nil)
local function invokeExport(by_world, by_date, fn)
local command = fn(by_world, by_date)
if type(command) == "string" then
dfhack.run_command(command)
else
command()
end
end

function ExportMap:startExports()
local by_date = self.subviews.by_date:getOptionValue()
local by_world = by_date or self.subviews.by_world:getOptionValue()

for _, export in ipairs(exports) do
if export.enabled then
invokeExport(by_world, by_date, export.command)
end
end
end

---launch command from the C++ plugin
---@param export string
---@return fun(boolean,boolean):string
local function pluginCommand(export)
return function(by_world, by_date)
-- grouping by date implies grouping by world
return ('export-world-map %s %s'):format(by_date and '--group-by-date' or (by_world and '--group-by-world' or ''), export)
end
end

exports = {
{ id = 1, key = 'regions', desc = 'Region Map Export (regions.csv)' , enabled = true, command = pluginCommand("regions") },
{ id = 2, key = 'rivers', desc = 'River Export (rivers.csv)' , enabled = true, command = pluginCommand("rivers") },
{ id = 3, key = 'sites', desc = 'Site Export (sites.csv)' , enabled = true, command = pluginCommand("sites") },
{ id = 4, key = 'elevation', desc = 'Elevation Grid Export (elevation.dat, elevation.vrt)' , enabled = true, command = pluginCommand("elevation") },
{ id = 5, key = 'roads', desc = 'Road Export (roads.geojson)' , enabled = true, command = roads.export },
{ id = 6, key = 'layers', desc = 'Export Geological Layers (layers.csv)' , enabled = true, command = layers.export },
{ id = 7, key = 'pops', desc = 'Export Plant and Animal Populations (*_pops.csv)' , enabled = true, command = pops.export }
}

local SELECTED_ICON = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN}
local DISABLED_ICON = dfhack.pen.parse{ch='x', fg=COLOR_RED}

function ExportMap:getChoices()
print("getChoices")
local choices = {}
for _, export in ipairs(exports) do
table.insert(choices, {
icon = function()
return export.enabled and SELECTED_ICON or DISABLED_ICON
end,
text = export.desc,
id = export.id
})
end
return choices
end

function ExportMap:toggleExport(_, choice)
exports[choice.id].enabled = not exports[choice.id].enabled
self:updateLayout()
end

function ExportMap:init()
self.done = true
self:addviews{
widgets.Label{
frame = { t = 1 },
view_id = 'midmap_ratio',
text = "counting..."
},
widgets.TextButton{
frame = { w = 19, h = 1 , t = 3 },
label = "Generate Midmaps!",
on_click = self:callback('initializeMapScan')
},
widgets.Divider{
frame = { t = 5, h = 1 },
frame_style_l = false,
frame_style_r = false
},
widgets.Label{
frame={ t = 7 , h = 1 },
text = "Select exports to place in dfhack-config/map-export:",
},
widgets.List{
frame={t=9, h = 7},
view_id = "export_list",
on_submit=self:callback("toggleExport"),
icon_width = 2,
choices = self:getChoices(),
},
widgets.CycleHotkeyLabel{
view_id = 'by_world',
key = 'CUSTOM_W',
frame = { w = 40, h = 1 , t = 17, l = 0 },
options = { { label = 'Yes' , value = true, pen = COLOR_LIGHTGREEN}, { label = 'No' , value = false} },
initial_option = false,
label = "Create folder for world name",
on_change = function(new, _)
if not new then
self.subviews.by_date:setOption(false)
end
end
},
widgets.CycleHotkeyLabel{
view_id = 'by_date',
key = 'CUSTOM_D',
frame = { w = 40, h = 1 , t = 18, l = 0 },
options = { { label = 'Yes' , value = true, pen = COLOR_LIGHTGREEN}, { label = 'No' , value = false} },
initial_option = false,
label = "Create subfolder for world date",
on_change = function(new, _)
if new then
self.subviews.by_world:setOption(true)
end
end
},
widgets.TextButton{
frame = { w = 18, h = 1 , t = 20 },
label = "Run Map Exports!",
on_click = self:callback('startExports'),
enabled = function()
local cur,max = getLoadedTileRatio()
return cur == max
end
}
}
self:updateRatio()
end

ExportMapScreen = defclass(ExportMapScreen, gui.ZScreen)
ExportMapScreen.ATTRS {
focus_path='PopulateMidmap',
}

function ExportMapScreen:init()
self:addviews{ExportMap{}}
end

function ExportMapScreen:onDismiss()
view = nil
end

view = view and view:raise() or ExportMapScreen{}:show()
38 changes: 38 additions & 0 deletions internal/export-world-map/export-layers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--@ module = true
local util = reqscript('internal/export-world-map/util')

function writeOutput(out)
local column_headers = "geo_index;type;material;top_height;bottom_height\n"
out:write(column_headers)

for _, geo_biome in ipairs(df.global.world.world_data.geo_biomes) do
for _, layer in ipairs(geo_biome.layers) do
out:write(('%s;%s;%s;%s;%s\n'):format(
geo_biome.index,
"LAYER",
df.inorganic_raw.find(layer.mat_index).id,
layer.top_height,
layer.bottom_height
))
for i = 0, #layer.vein_mat - 1 do
local material = df.inorganic_raw[layer.vein_mat[i]]
out:write(('%s;%s;%s;%s;%s\n'):format(
geo_biome.index,
df.inclusion_type[layer.vein_type[i]],
df.inorganic_raw.find(layer.vein_mat[i]).id,
layer.top_height,
layer.bottom_height
))
end
end
end
end

--- export geological layers
function export(by_world, by_date)
return function()
local out = io.open(util.getOutputFolder(by_world, by_date).."layers.csv", 'w')
writeOutput(out)
out:close()
end
end
59 changes: 59 additions & 0 deletions internal/export-world-map/export-pops.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--@ module = true
local util = reqscript('internal/export-world-map/util')

local plants_filename = "plant_pops.csv"
local creature_filename = "creature_pops.csv"
local vermin_filename = "vermin_pops.csv"
local column_headers = "region_id;population_type;raw_id;pop_count_min;pop_count_max\n"

function writeOutput(base_folder)
local plants = io.open(base_folder..plants_filename, 'w')
local creatures = io.open(base_folder..creature_filename, 'w')
local vermin = io.open(base_folder..vermin_filename, 'w')

plants:write(column_headers)
creatures:write(column_headers)
vermin:write(column_headers)

for _, region in ipairs(df.global.world.world_data.regions) do
for _, pop in ipairs(region.population) do
local raw_id
local out
if pop.type >= 5 then
raw_id = df.plant_raw.find(pop.plant).id
out = plants
else
raw_id = df.creature_raw.find(pop.plant).creature_id
if
pop.type == df.world_population_type.Vermin or
pop.type == df.world_population_type.VerminInnumerable
then
out = vermin
else
out = creatures
end
end

if out then
out:write(('%s;%s;%s;%s;%s\n'):format(
region.index,
df.world_population_type[pop.type],
raw_id,
pop.count_min,
pop.count_max
))
end
end
end

plants:close()
vermin:close()
creatures:close()
end

--- export animal and plant populations
function export(by_world, by_date)
return function()
writeOutput(util.getOutputFolder(by_world, by_date))
end
end
Loading
Loading