-
Notifications
You must be signed in to change notification settings - Fork 235
new tool: gui/export-world-map
#1606
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| 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 (GeoJSON) | ||
| * Geological layers and the veins included in them (semicolon-delimited pure data layer) | ||
| * Animal and plant populations of regions (semicolon-delimited pure data layer) | ||
|
|
||
| The data layers are in "long" format (e.g. ``BIRD_KEA`` is a field in the | ||
| ``raw_id`` column instead of a column header). Since these layers do not have | ||
| any geometry information, they need to be joined with the ``region`` layer of | ||
| `export-world-map`. The first column of each file (e.g. ``geo_index`` for | ||
| geological layers) match as the corresponding column from the region export. | ||
|
|
||
| Usage | ||
| ----- | ||
|
|
||
| :: | ||
|
|
||
| gui/export-world-map | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| ---@diagnostic disable: missing-fields | ||
|
SilasD marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
|
SilasD marked this conversation as resolved.
|
||
|
|
||
| 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, | ||
|
SilasD marked this conversation as resolved.
|
||
| 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 | ||
| local tile_x = dfhack.screen.inGraphicsMode() and 16 or df.global.gps.tile_pixel_x | ||
| local tile_y = dfhack.screen.inGraphicsMode() and 16 or df.global.gps.tile_pixel_y | ||
|
|
||
| -- conservative approximation of screen dimensions in tiles | ||
| self.screen_w = (pixel_x // tile_x) - 2 | ||
| self.screen_h = (pixel_y // tile_y) - 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 | ||
|
SilasD marked this conversation as resolved.
|
||
| 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 | ||
|
|
||
| ---generate wrapper for invoking 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 = 22, h = 1 , t = 3 }, | ||
| label = "Load Region Details!", | ||
| 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 = #exports}, | ||
| 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 = #exports + 10, 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 = #exports + 11, 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 = #exports + 13 }, | ||
| 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() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( | ||
|
SilasD marked this conversation as resolved.
|
||
| 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 | ||
|
chdoc marked this conversation as resolved.
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| --@ 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 == df.world_population_type.Tree or | ||
| pop.type == df.world_population_type.Grass or | ||
| pop.type == df.world_population_type.Bush | ||
| 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 | ||
|
SilasD marked this conversation as resolved.
|
||
| 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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.