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
18 changes: 18 additions & 0 deletions docs/gui/export-world-map.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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.

Usage
-----

::

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

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

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

---@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()
self.subviews.indicator_label:setText("working ...")

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

self.subviews.indicator_label:setText("")
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 }
}

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 = 6},
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 = 15, 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 = 16, 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 = 18 },
label = "Run Map Exports!",
on_click = self:callback('startExports'),
enabled = function()
local cur,max = getLoadedTileRatio()
return cur == max
end
},
widgets.Label{
view_id = 'indicator_label',
frame={ t = 20 , h = 1 },
text = ""
}
}
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()
74 changes: 74 additions & 0 deletions internal/export-world-map/export-roads.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
--@ module = true
local json = require('json')
local util = reqscript('internal/export-world-map/util')

local function getConstructionAndGeometryType(construction)
if df.world_construction_roadst:is_instance(construction) then
local subtype = ""
if df.item_type[construction.square_obj[0]["item_type"]] == "BLOCKS" then
subtype = "paved"
else
subtype = "dirt"
end
return "road", "LineString", subtype
elseif df.world_construction_bridgest:is_instance(construction) then
return "bridge", "Point", ""
elseif df.world_construction_tunnelst:is_instance(construction) then
return "tunnel", "LineString", ""
else
qerror("unknown construction type")
end
end

local function gatherFeatures()
local features = {}
for _, construction in ipairs(df.global.world.world_data.constructions.list) do
local type, geometry_type, subtype = getConstructionAndGeometryType(construction)

local coordinates
if geometry_type == "Point" then
-- only world tile - not sure where to get region tile coordinates from
-- perhaps it gets them from neighbouring roads?
coordinates = {
construction.square_pos["x"][0]*768,
-(construction.square_pos["y"][0]*768)
}
else
coordinates = {}
for _,square_obj in ipairs(construction.square_obj) do
for i=0,#square_obj.embark_x-1 do
table.insert(coordinates, {
square_obj.region_pos["x"]*768+square_obj.embark_x[i]*48+24,
-(square_obj.region_pos["y"]*768+square_obj.embark_y[i]*48+24)
})
end
end
end

table.insert(features, {
type = "Feature",
properties = {
id = construction.id,
name_df = dfhack.df2utf(dfhack.translation.translateName(construction["name"],false)),
name_en = dfhack.df2utf(dfhack.translation.translateName(construction["name"],true)),
construction_type = type,
construction_subtype = subtype,
},
geometry = {
type = geometry_type,
coordinates = coordinates
}
})
end
return features
end

-- export roads as GeoJson
function export(by_world, by_date)
return function()
json.encode_file(
{ type = "FeatureCollection", features = gatherFeatures() },
util.getOutputFolder(by_world, by_date).."roads.geojson"
)
end
end
20 changes: 20 additions & 0 deletions internal/export-world-map/util.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--@ module = true

if not dfhack_flags.module then
qerror('this script cannot be called directly')
end

local plugin = require('plugins.export-world-map')

---get folder to place output files
---@param by_world boolean
---@param by_date boolean
---@return string
function getOutputFolder(by_world, by_date)
local by_world = by_world or by_date
return ('%s/map-export/%s%s'):format(
dfhack.getConfigPath(),
by_world and plugin.getWorldFolderName()..'/' or '',
by_date and plugin.getDateFolderName()..'/' or ''
)
end
Loading