-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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() | ||
| 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( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why semicolons when the output is named as a CSV file? Comma-separated values (later) I see you're using this for most/all output files. I'm not going to comment on each occurance. as an aside, I've never understood why people use the idiom if the format string is already defined, it makes sense, sure. but that's so seldom the case with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a case of Europeans doing European things: https://discord.com/channels/793331351645323264/1340358673003712654/1540610890192134195 However, I agree that this should be documented for the benefit of others.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW, the rule for when you use a semicolon is "if the decimal separator is a comma, then the CSV separator is a semicolon, otherwise it's a comma". Microsoft's locale processing does this for you (they have a Most CSV libraries will accept either. Arguably, the best approach is to grab the current locale, identify its decimal separator, and from that infer whether a comma or a semicolon is most appropriate. std::locale current_locale = std::locale{""};
auto const& numeric_facet = std::use_facet<std::numpunct<char>>(current_locale);
char decimal_sep = numeric_facet.decimal_point();
char list_sep = decimal_sep == ',' ? ';' : ',';
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really would prefer if output of the world export tools would not depend on the user's locale. As far as I know, no other behavior of DFHack depends on it. If only you knew how much grief Microsoft's locale-dependent interpretation of CSV has caused on this side of the Atlantic... Moreover, while semicolons do not naturally occur in our data, commas very much do The commas inside the WKT geometries obviously affects all layer files, including those generated by the C++ plugin. Currently, all exports get away without doing any escaping. If you want commas, you also will get escaping.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. <meme> Um, ackchyually, </meme> We've got a couple of locale-specific numbers. The trade overlays format prices per locale, and the notifications floating overlay formats time-since-last-save per locale. Which is just quibbling really. Commas crop up in the exported data; I think that's a good enough reason to go with semicolons. All good. |
||
| 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 | ||
| 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 | ||
Uh oh!
There was an error while loading. Please reload this page.