From d0e8207cbcb90a47af23d55bf5d6446eafc293e9 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:38:01 +0200 Subject: [PATCH 1/8] region map export using neither GDAL nor boost --- library/include/df/custom/coord2d.methods.inc | 2 +- plugins/CMakeLists.txt | 1 + plugins/export-map.cpp | 572 ++++++++++++++++++ 3 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 plugins/export-map.cpp diff --git a/library/include/df/custom/coord2d.methods.inc b/library/include/df/custom/coord2d.methods.inc index 512149ce36..c828b0c47e 100644 --- a/library/include/df/custom/coord2d.methods.inc +++ b/library/include/df/custom/coord2d.methods.inc @@ -1,4 +1,4 @@ -coord2d(uint16_t _x, uint16_t _y) : x(_x), y(_y) {} +coord2d(int16_t _x, int16_t _y) : x(_x), y(_y) {} bool isValid() const { return x >= 0; } void clear() { x = y = -30000; } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8f230ff583..f87f9002ce 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -71,6 +71,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES gdal) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp new file mode 100644 index 0000000000..0a06ff2130 --- /dev/null +++ b/plugins/export-map.cpp @@ -0,0 +1,572 @@ +#include "Debug.h" +#include "Error.h" +#include "PluginManager.h" +#include "MiscUtils.h" + +#include "modules/Maps.h" +#include "modules/Translation.h" + +#include "df/entity_raw.h" +#include "df/creature_raw.h" +#include "df/historical_entity.h" +#include "df/world.h" +#include "df/map_block.h" +#include "df/world_data.h" +#include "df/world_site.h" +#include "df/region_map_entry.h" +#include "df/world_region.h" +#include "df/world_landmass.h" +#include "df/world_region_details.h" + +#include "gdal/ogrsf_frmts.h" + +#include +#include +#include +#include +#include +#include +#include + + +using std::string; +using std::vector; + +using namespace DFHack; + +DFHACK_PLUGIN("export-map"); + +REQUIRE_GLOBAL(world); + +namespace DFHack { + DBG_DECLARE(exportmap, log); +} + +static command_result do_command(color_ostream &out, vector ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} + +auto setGeometry(OGRFeature *feature, double x, double y, double dimx, double dimy = 0) { + if (dimy == 0) { dimy = dimx; } + auto poly = new OGRPolygon(); + auto boundary = new OGRLinearRing(); + y = -y; // in GIS negative y-coordinates mean further south + boundary->addPoint(x,y); + boundary->addPoint(x,y-dimy); + boundary->addPoint(x+dimx,y-dimy); + boundary->addPoint(x+dimx,y); + boundary->closeRings(); + //the "Directly" variants assume ownership of the objects created above + poly->addRingDirectly(boundary); + feature->SetGeometryDirectly( poly ); +} + +df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { + switch (dir) { + case 1: world_x-- ; world_y++; break; + case 2: ; world_y++; break; + case 3: world_x++ ; world_y++; break; + case 4: world_x-- ; ; break; + // case 5 induces no change + case 6: world_x++ ; ; break; + case 7: world_x-- ; world_y--; break; + case 8: ; world_y--; break; + case 9: world_x++ ; world_y--; break; + } + world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); + world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); + return df::coord2d(world_x, world_y); +} + +auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { + OGRFieldDefn field( name.c_str() , type ); + if (subtype != OFSTNone) { + field.SetSubType(subtype); + } + if (width != 0) { + field.SetWidth(width); + } + // this should create a copy internally + if( layer->CreateField( &field ) != OGRERR_NONE ){ + throw CR_FAILURE; + } +} + +// PROJ.4 description of EPSG:3857 (https://epsg.io/3857) +static const char* EPSG_3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"; + +const char* describe_surroundings(int savagery, int evilness) { + constexpr std::arraysurroundings{ + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + }; + auto savagery_index = savagery < 33 ? 0 : (savagery > 65 ? 2 : 1); + auto evilness_index = evilness < 33 ? 0 : (evilness > 65 ? 2 : 1); + return surroundings[3 * evilness_index + savagery_index]; +} + +// static int wdim = 768; // dimension of a world tile +static int rdim = 48; // dimension of a region tile + +static command_result export_region_tiles(color_ostream &out); +static command_result export_sites(color_ostream &out); + +static command_result do_command(color_ostream &out, vector ¶meters) +{ + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()){ + out.printerr("This command requires a world to be loaded\n"); + return CR_WRONG_USAGE; + } + + if (parameters.size() && parameters[0] == "sites") + { + return export_sites(out); + } + else + { + return export_region_tiles(out); + } +} + +static command_result export_sites(color_ostream &out) +{ + out.print("exporting sites... "); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // set up coordinate system + OGRSpatialReference CRS; + if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { + out.printerr("could not set up coordinate system"); + return CR_FAILURE; + } + + // set up output driver + GDALAllRegister(); + const char *driver_name = "SQLite"; + const char *extension = "sqlite"; + auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); + if (!driver) { + out.printerr("could not find sqlite driver"); + return CR_FAILURE; + } + + // create a dataset and associate it to a file + std::string sites("sites."); + sites.append(extension); + const char* options[] = { "SPATIALITE=YES", nullptr }; + auto dataset = driver->Create( sites.c_str(), 0, 0, 0, GDT_Unknown, options); + if (!dataset) { + out.printerr("could not create dataset"); + return CR_FAILURE; + } + + // create a layer for the biome data + // const char* format[] = { "FORMAT=WKT", nullptr }; + auto layer = dataset->CreateLayer( "world_sites", &CRS, wkbPolygon, nullptr ); + if (!layer) { + out.printerr("could not create layer"); + return CR_FAILURE; + } + + try { + create_field(layer, "site_id", OFTInteger); + create_field(layer, "civ_id", OFTInteger); + create_field(layer, "created_year", OFTInteger); + create_field(layer, "cur_owner_id", OFTInteger); + + create_field(layer, "type", OFTString, 15); + + create_field(layer, "site_name_df", OFTString, 100); + create_field(layer, "site_name_en", OFTString, 100); + + create_field(layer, "civ_name_df", OFTString, 100); + create_field(layer, "civ_name_en", OFTString, 100); + + create_field(layer, "site_government_df", OFTString, 100); + create_field(layer, "site_government_en", OFTString, 100); + + create_field(layer, "owner_race", OFTString, 15); + + // create_field(layer, "local_ruler", OFTString, 100); + + } + catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + + if (dataset->StartTransaction() != OGRERR_NONE) { + out.printerr("could not start a transaction\n"); + } + + for (auto const site : world->world_data->sites) + { + + auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + + setGeometry( + feature, + site->global_min_x * rdim, + site->global_min_y * rdim, + (site->global_max_x - site->global_min_x + 1) * rdim, + (site->global_max_y - site->global_min_y + 1) * rdim + ); + feature->SetField( "site_id", site->id ); + feature->SetField( "type", ENUM_KEY_STR(world_site_type, site->type).c_str() ); + #define SET_FIELD(name) feature->SetField( #name, site->name) + SET_FIELD(civ_id); + SET_FIELD(created_year); + SET_FIELD(cur_owner_id); + #undef SET_FIELD + + #define TRANSLATE_NAME(field_name, name_object)\ + feature->SetField((#field_name"_df"), DF2UTF(Translation::translateName(&name_object, false)).c_str());\ + feature->SetField((#field_name"_en"), DF2UTF(Translation::translateName(&name_object, true)).c_str()); + + TRANSLATE_NAME(site_name, site->name) + + auto civ = df::historical_entity::find(site->civ_id); + if (civ) { TRANSLATE_NAME(civ_name,civ->name) } + + auto owner = df::historical_entity::find(site->cur_owner_id); + if (owner) { + TRANSLATE_NAME(site_government,owner->name) + auto race = df::creature_raw::find(owner->race); + if (!race){ + race = df::creature_raw::find(civ->race); + } + if (race) { + feature->SetField( "owner_race", race->name[2].c_str() ); + } + } + + + // this updates the feature with the id it receives in the layer + if( layer->CreateFeature( feature ) != OGRERR_NONE ) + return CR_FAILURE; + // but we don't care and destroy the feature + OGRFeature::DestroyFeature( feature ); + } + + dataset->CommitTransaction(); + + GDALClose( dataset ); + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} + +/********************************************************************** */ + +template<> +struct std::hash +{ + std::size_t operator()(const df::coord2d& pos) const noexcept + { + // hashing is easy, if values are smaller than the hash + return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; + } +}; + +using coord = df::coord2d; + +enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; + +direction turn_left(direction dir) { + return (direction)(((int)dir+1) % 4); +} + +direction turn_right(direction dir) { + return (direction)(((int)dir+3) % 4); +} + +df::coord2d as_offset(direction dir) { + switch (dir) { + case direction::North: + return { 0, -1 }; + case direction::West: + return { -1, 0 }; + case direction::South: + return { 0, 1 }; + case direction::East: + return { 1, 0 }; + default: + abort(); + } +} + +coord advance(coord pos, direction dir) { + return pos + as_offset(dir); +} + +auto print_path(std::vector path, int scale = rdim) { + assert(path.size()); + // create the path in clockwise direction, so that it becomes counterclockwise with the y-flip + std::ranges::reverse(path); + std::ostringstream out; + out << scale * path[0].x << " " << -scale * path[0].y; + for (size_t i = 1; i < path.size(); ++i) { + out << "," << scale * path[i].x << " " << -scale * path[i].y; + } + return out.str(); +} + +std::pair ahead(const std::vector &component, coord pos, direction dir) { + auto test = [&](int16_t x, int16_t y){ + coord offset{x,y}; + return std::find(component.begin(), component.end(), pos + offset) != component.end(); + }; + + switch (dir) { + case direction::North: + return { test(-1,-1), test(0,-1)}; + case direction::West: + return { test(-1,0), test(-1,-1)}; + case direction::South: + return { test(0,0), test(-1, 0)}; + case direction::East: + return { test(0,-1), test(0, 0)}; + default: + abort(); + } +} + +static command_result export_region_tiles(color_ostream &out) +{ + out.print("%lu / %d region map tiles loaded\n", + world->world_data->midmap_data.region_details.size(), + world->world_data->world_width * world->world_data->world_height + ); + out.print("exporting map... \n"); + out.flush(); + const auto start{std::chrono::steady_clock::now()}; + + // ensure that we have an output file + std::string filename("map.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { + return CR_FAILURE; + } + out_file << "world_x; world_y; num_tiles; num_components; biome_type; boundary_wkt" << std::endl; + + /* + try { + create_field(layer, "region_id", OFTInteger); + create_field(layer, "region_name_en", OFTString, 100); + create_field(layer, "region_name_df", OFTString, 100); + create_field(layer, "landmass_id", OFTInteger); + create_field(layer, "landmass_name_en", OFTString, 100); + create_field(layer, "landmass_name_df", OFTString, 100); + + create_field(layer, "biome_type", OFTString, 32); + create_field(layer, "surroundings", OFTString, 16); + create_field(layer, "elevation", OFTInteger); + + create_field(layer, "evilness", OFTInteger); + create_field(layer, "savagery", OFTInteger); + create_field(layer, "volcanism", OFTInteger); + create_field(layer, "drainage", OFTInteger); + create_field(layer, "temperature", OFTInteger); + create_field(layer, "vegetation", OFTInteger); + create_field(layer, "rainfall", OFTInteger); + create_field(layer, "snowfall", OFTInteger); + create_field(layer, "salinity", OFTInteger); + + create_field(layer, "reanimating", OFTInteger, 0, OFSTBoolean); + create_field(layer, "has_bogeymen", OFTInteger, 0, OFSTBoolean); + + } catch (const DFHack::command_result& r) { + out.printerr("could not create fields for output layer"); + return r; + } + */ + + + + // iterating over the region details allows the user to do partial map exports + // by manually scrolling on the embark site selection + + std::unordered_map> world_tile_region; + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto world_x = region_details->pos.x; + auto world_y = region_details->pos.y; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) { + + // auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); + // setGeometry( + // feature, + // (double)(world_x * wdim + region_x * rdim), + // (double)(world_y * wdim + region_y * rdim), + // rdim + // ); + + // get the world tile coordinates used for the biome information of the local region tile + auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + world_tile_region[biome_tile].emplace_back(16 * world_x + region_x, 16 * world_y + region_y); + + } + } + } + + out.print("processing %ld world tile regions\n", world_tile_region.size()); + + auto region_order = [](df::coord2d p1, df::coord2d p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); + }; + static std::array directions{ + as_offset(direction::North), + as_offset(direction::West), + as_offset(direction::South), + as_offset(direction::East) + }; + + for (auto& [biome_tile, region] : world_tile_region) + { + assert(region.size() > 0); + // compute the connected components of the world tile region + std::ranges::sort(region, region_order); + + std::vector component_assignment; + component_assignment.resize(region.size(),0); + std::deque agenda; + + auto current_component = 0; + for (size_t i = 0; i < region.size(); ++i) { + if (component_assignment[i]) { + continue; + } else { + ++current_component; + component_assignment[i] = current_component; + agenda.push_back(i); + } + while(!agenda.empty()) { + auto pos_idx = agenda.front(); agenda.pop_front(); + auto pos = region[pos_idx]; + + for (auto const offset : directions) { + // FIXME: use something O(log n) instead of the O(n) find + auto n_it = std::find(region.begin(), region.end(), pos + offset); + auto n_idx = std::distance(region.begin(), n_it); + if (n_it != region.end() && component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } + } + } + } + + // assert that all parts of the region are accounted for + assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + + std::vector> components; + components.resize(current_component); + for (size_t i = 0; i < region.size(); ++i) { + components.at(component_assignment.at(i) - 1).push_back(region.at(i)); + } + + std::vector> paths; + for (auto const &component : components) { + + auto start = component.at(0); + std::vector path; + path.push_back(start); + + auto current_direction = direction::South; + auto current_position = advance(start,current_direction); + + while (current_position != start) + { + auto [left, right] = ahead(component, current_position, current_direction); + if (left && right) { + // in front of a wall: turn right + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + else if (!left && !right) { + // no walls ahead: turn left + path.push_back(current_position); + current_direction = turn_left(current_direction); + } + assert(! (!left && right)); // shape has a hole or is not connected + current_position = advance(current_position, current_direction); + } + // close the path + path.push_back(current_position); + paths.push_back(std::move(path)); + path.clear(); + } + + out_file << biome_tile.x << ";" << biome_tile.y << ";" << region.size() << ";" << components.size() << ";"; + out_file << ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)) << ";"; + assert(paths.size() > 0); + if (paths.size() == 1) { + out_file << "POLYGON((" << print_path(paths[0]) << "))" << std::endl; + } else { + out_file << "MULTIPOLYGON(" << "((" << print_path(paths[0]) << "))"; + for (size_t i = 1; i < paths.size(); ++i) { + out_file << ",((" << print_path(paths[i]) << "))"; + } + out_file << ")" << std::endl; + } + } + + + // feature->SetField( "biome_type", ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_x,biome_y)).c_str() ); + + // gets supplementary information from the world tile + // auto& region_map_entry = world->world_data->region_map[biome_x][biome_y]; + // #define SET_FIELD(name) feature->SetField( #name, region_map_entry.name) + // SET_FIELD(region_id); + // SET_FIELD(landmass_id); + // SET_FIELD(evilness); + // SET_FIELD(savagery); + // SET_FIELD(volcanism); + // SET_FIELD(drainage); + // SET_FIELD(temperature); + // SET_FIELD(vegetation); + // SET_FIELD(rainfall); + // SET_FIELD(snowfall); + // SET_FIELD(salinity); + // #undef SET_FIELD + + // feature->SetField( "surroundings", describe_surroundings(region_map_entry.savagery, region_map_entry.evilness)); + + // auto region = df::world_region::find(region_map_entry.region_id); + // if (region) { + // auto region_name_en = DF2UTF(Translation::translateName(®ion->name, true)); + // feature->SetField( "region_name_en", region_name_en.c_str()); + // auto region_name_df = DF2UTF(Translation::translateName(®ion->name, false)); + // feature->SetField( "region_name_df", region_name_df.c_str()); + // feature->SetField("reanimating", region->reanimating); + // feature->SetField("has_bogeymen", region->has_bogeymen); + // } + // auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + // if (landmass) { + // auto landmass_name_en = DF2UTF(Translation::translateName(&landmass->name, true)); + // feature->SetField( "landmass_name_en", landmass_name_en.c_str()); + // auto landmass_name_df = DF2UTF(Translation::translateName(&landmass->name, false)); + // feature->SetField( "landmass_name_df", landmass_name_df.c_str()); + // } + + + const auto finish{std::chrono::steady_clock::now()}; + const std::chrono::duration elapsed_seconds{finish - start}; + out.print("done in %f ms !\n", elapsed_seconds.count()); + return CR_OK; +} From d7a04d6c2f2d60c77711b4ef452e085bd6f2e2f8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:35:23 +0200 Subject: [PATCH 2/8] clean up and reinstate export of the remaining biome and region values --- plugins/export-map.cpp | 131 ++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 28 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 0a06ff2130..8d4b4c7039 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -272,6 +272,40 @@ static command_result export_sites(color_ostream &out) /********************************************************************** */ +template Callable> +void print_vector( + std::ostream &out, + const std::vector &elements, + Callable&& print_element, + const char* prefix = "[", + const char* separator = ", ", + const char* suffix = "]" +){ + out << prefix; + if (elements.size()) { + print_element(out, elements[0]); + for (size_t i = 1; i < elements.size(); ++i) { + out << separator; + print_element(out, elements[i]); + } + } + out << suffix; +} + +template +void print_vector( + std::ostream &out, + const std::vector &elements, + const char* prefix = "[", + const char* separator = ", ", + const char* suffix = "]" +){ + auto print_element = [](std::ostream &out, const T& e) { out << e; }; + print_vector(out, elements, print_element, prefix, separator, suffix); +} + + + template<> struct std::hash { @@ -313,22 +347,22 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } -auto print_path(std::vector path, int scale = rdim) { +auto print_path(std::ostream &out, const std::vector &path) { + auto scale = rdim; assert(path.size()); - // create the path in clockwise direction, so that it becomes counterclockwise with the y-flip - std::ranges::reverse(path); - std::ostringstream out; - out << scale * path[0].x << " " << -scale * path[0].y; - for (size_t i = 1; i < path.size(); ++i) { - out << "," << scale * path[i].x << " " << -scale * path[i].y; - } - return out.str(); + auto print_point = [scale](std::ostream &out, const coord &pos){ + out << scale * pos.x << " " << -scale * pos.y;}; + print_vector(out, path, print_point, "(", ",", ")"); } +auto region_order = [](df::coord2d p1, df::coord2d p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); +}; + std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; - return std::find(component.begin(), component.end(), pos + offset) != component.end(); + return std::ranges::binary_search(component, pos + offset, region_order); }; switch (dir) { @@ -361,7 +395,17 @@ static command_result export_region_tiles(color_ostream &out) if (!out_file) { return CR_FAILURE; } - out_file << "world_x; world_y; num_tiles; num_components; biome_type; boundary_wkt" << std::endl; + + vector headings = { + "world_x", "world_y", "num_tiles", "num_components", "biome_type", + "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", + "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity", + "surroundings", "elevation", "reanimating", "has_bogeymen" + }; + print_vector(out_file, headings,"",";",";boundary_wkt\n" ); + + + /* try { @@ -426,9 +470,7 @@ static command_result export_region_tiles(color_ostream &out) out.print("processing %ld world tile regions\n", world_tile_region.size()); - auto region_order = [](df::coord2d p1, df::coord2d p2) { - return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); - }; + static std::array directions{ as_offset(direction::North), as_offset(direction::West), @@ -461,7 +503,10 @@ static command_result export_region_tiles(color_ostream &out) for (auto const offset : directions) { // FIXME: use something O(log n) instead of the O(n) find - auto n_it = std::find(region.begin(), region.end(), pos + offset); + auto lower = std::ranges::lower_bound(region, pos + offset, region_order); + auto n_it = (*lower == pos + offset) ? lower : region.end(); + + // auto n_it = std::find(region.begin(), region.end(), pos + offset); auto n_idx = std::distance(region.begin(), n_it); if (n_it != region.end() && component_assignment[n_idx] == 0) { component_assignment[n_idx] = current_component; @@ -487,7 +532,7 @@ static command_result export_region_tiles(color_ostream &out) std::vector path; path.push_back(start); - auto current_direction = direction::South; + auto current_direction = direction::East; auto current_position = advance(start,current_direction); while (current_position != start) @@ -496,14 +541,14 @@ static command_result export_region_tiles(color_ostream &out) if (left && right) { // in front of a wall: turn right path.push_back(current_position); - current_direction = turn_right(current_direction); + current_direction = turn_left(current_direction); } else if (!left && !right) { // no walls ahead: turn left path.push_back(current_position); - current_direction = turn_left(current_direction); + current_direction = turn_right(current_direction); } - assert(! (!left && right)); // shape has a hole or is not connected + assert(! (left && !right)); // shape has a hole or is not connected current_position = advance(current_position, current_direction); } // close the path @@ -511,18 +556,48 @@ static command_result export_region_tiles(color_ostream &out) paths.push_back(std::move(path)); path.clear(); } - - out_file << biome_tile.x << ";" << biome_tile.y << ";" << region.size() << ";" << components.size() << ";"; - out_file << ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)) << ";"; assert(paths.size() > 0); + + auto& region_map_entry = world->world_data->region_map[biome_tile.x][biome_tile.y]; + auto world_region = df::world_region::find(region_map_entry.region_id); + auto landmass = df::world_landmass::find(region_map_entry.landmass_id); + + auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; + print_csv( + // "world_x", "world_y", "num_tiles", "num_components", "biome_type", + biome_tile.x, + biome_tile.y, + region.size(), + components.size(), + ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_tile.x, biome_tile.y)), + // "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df" + region_map_entry.region_id, + world_region ? DF2UTF(Translation::translateName(&world_region->name, true)) : "NONE", + world_region ? DF2UTF(Translation::translateName(&world_region->name, false)) : "NONE", + region_map_entry.landmass_id, + landmass ? DF2UTF(Translation::translateName(&landmass->name, true)) : "NONE", + landmass ? DF2UTF(Translation::translateName(&landmass->name, false)) : "NONE", + // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity" + region_map_entry.evilness, + region_map_entry.savagery, + region_map_entry.volcanism, + region_map_entry.drainage, + region_map_entry.temperature, + region_map_entry.vegetation, + region_map_entry.rainfall, + region_map_entry.snowfall, + region_map_entry.salinity, + // "surroundings", "elevation", "reanimating", "has_bogeymen" + describe_surroundings(region_map_entry.savagery, region_map_entry.evilness), + region_map_entry.elevation, + world_region->reanimating, + world_region->has_bogeymen + ); + // output geometry if (paths.size() == 1) { - out_file << "POLYGON((" << print_path(paths[0]) << "))" << std::endl; + print_vector(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); } else { - out_file << "MULTIPOLYGON(" << "((" << print_path(paths[0]) << "))"; - for (size_t i = 1; i < paths.size(); ++i) { - out_file << ",((" << print_path(paths[i]) << "))"; - } - out_file << ")" << std::endl; + print_vector(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); } } From d899a2a0fdd821c8323afc0d1b2a6d50f2b9f975 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:16:38 +0200 Subject: [PATCH 3/8] comments and cleanup --- library/include/MiscUtils.h | 36 ++++++ plugins/export-map.cpp | 239 ++++++++++++------------------------ 2 files changed, 114 insertions(+), 161 deletions(-) diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 8568a55c0a..62a09c98bf 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -395,6 +395,42 @@ inline bool static_add_to_map(CT *pmap, typename CT::key_type key, typename CT:: /* * MISC */ +template< + std::ranges::input_range Range, + std::invocable> Callable> +void print_range( + std::ostream &out, + const Range &elements, + Callable&& print_element, + const std::string &prefix = "[", + const std::string &separator = ", ", + const std::string &suffix = "]" +){ + out << prefix; + auto it = std::ranges::begin(elements); + auto end = std::ranges::end(elements); + + if (it != end) { + print_element(out, *it); + for (++it; it != end; ++it) { + out << separator; + print_element(out, *it); + } + } + out << suffix; +} + +template +void print_range( + std::ostream &out, + const Range& elements, + const std::string &prefix = "[", + const std::string &separator = ", ", + const std::string &suffix = "]" +){ + auto print_element = [](std::ostream &out, auto& e) { out << e; }; + print_range(out, elements, print_element, prefix, separator, suffix); +} DFHACK_EXPORT bool split_string(std::vector *out, const std::string &str, const std::string &separator, diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 8d4b4c7039..0e0db0e83e 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -39,9 +39,23 @@ DFHACK_PLUGIN("export-map"); REQUIRE_GLOBAL(world); namespace DFHack { - DBG_DECLARE(exportmap, log); + DBG_DECLARE(exportmap, log, DebugCategory::LINFO); + DBG_DECLARE(exportmap, warning, DebugCategory::LWARNING); } +template<> +struct std::hash +{ + std::size_t operator()(const df::coord2d& pos) const noexcept + { + // hashing is easy, if values are smaller than the hash + return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; + } +}; + +// were only dealing with 2D coordinates in this file +using coord = df::coord2d; + static command_result do_command(color_ostream &out, vector ¶meters); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { @@ -84,7 +98,7 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { } world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); - return df::coord2d(world_x, world_y); + return { world_x, world_y }; } auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { @@ -272,52 +286,12 @@ static command_result export_sites(color_ostream &out) /********************************************************************** */ -template Callable> -void print_vector( - std::ostream &out, - const std::vector &elements, - Callable&& print_element, - const char* prefix = "[", - const char* separator = ", ", - const char* suffix = "]" -){ - out << prefix; - if (elements.size()) { - print_element(out, elements[0]); - for (size_t i = 1; i < elements.size(); ++i) { - out << separator; - print_element(out, elements[i]); - } - } - out << suffix; -} - -template -void print_vector( - std::ostream &out, - const std::vector &elements, - const char* prefix = "[", - const char* separator = ", ", - const char* suffix = "]" -){ - auto print_element = [](std::ostream &out, const T& e) { out << e; }; - print_vector(out, elements, print_element, prefix, separator, suffix); -} - -template<> -struct std::hash -{ - std::size_t operator()(const df::coord2d& pos) const noexcept - { - // hashing is easy, if values are smaller than the hash - return ((std::size_t)pos.x << 16) | (std::size_t)pos.y; - } +bool region_order(coord p1, coord p2) { + return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); }; -using coord = df::coord2d; - enum class direction : int { North = 0, West = 1, South = 2, East = 3 }; direction turn_left(direction dir) { @@ -328,7 +302,7 @@ direction turn_right(direction dir) { return (direction)(((int)dir+3) % 4); } -df::coord2d as_offset(direction dir) { +coord as_offset(direction dir) { switch (dir) { case direction::North: return { 0, -1 }; @@ -352,12 +326,10 @@ auto print_path(std::ostream &out, const std::vector &path) { assert(path.size()); auto print_point = [scale](std::ostream &out, const coord &pos){ out << scale * pos.x << " " << -scale * pos.y;}; - print_vector(out, path, print_point, "(", ",", ")"); + print_range(out, path, print_point, "(", ",", ")"); } -auto region_order = [](df::coord2d p1, df::coord2d p2) { - return p1.y < p2.y || (p1.y == p2.y && p1.x < p2.x); -}; + std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ @@ -396,74 +368,31 @@ static command_result export_region_tiles(color_ostream &out) return CR_FAILURE; } + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv below vector headings = { "world_x", "world_y", "num_tiles", "num_components", "biome_type", "region_id", "region_name_en", "region_name_df", "landmass_id", "landmass_name_en", "landmass_name_df", - "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity", + "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity", "surroundings", "elevation", "reanimating", "has_bogeymen" }; - print_vector(out_file, headings,"",";",";boundary_wkt\n" ); - - - - - /* - try { - create_field(layer, "region_id", OFTInteger); - create_field(layer, "region_name_en", OFTString, 100); - create_field(layer, "region_name_df", OFTString, 100); - create_field(layer, "landmass_id", OFTInteger); - create_field(layer, "landmass_name_en", OFTString, 100); - create_field(layer, "landmass_name_df", OFTString, 100); - - create_field(layer, "biome_type", OFTString, 32); - create_field(layer, "surroundings", OFTString, 16); - create_field(layer, "elevation", OFTInteger); - - create_field(layer, "evilness", OFTInteger); - create_field(layer, "savagery", OFTInteger); - create_field(layer, "volcanism", OFTInteger); - create_field(layer, "drainage", OFTInteger); - create_field(layer, "temperature", OFTInteger); - create_field(layer, "vegetation", OFTInteger); - create_field(layer, "rainfall", OFTInteger); - create_field(layer, "snowfall", OFTInteger); - create_field(layer, "salinity", OFTInteger); - - create_field(layer, "reanimating", OFTInteger, 0, OFSTBoolean); - create_field(layer, "has_bogeymen", OFTInteger, 0, OFSTBoolean); - - } catch (const DFHack::command_result& r) { - out.printerr("could not create fields for output layer"); - return r; - } - */ + print_range(out_file, headings,"",";",";boundary_wkt\n" ); + /* Preprocessing: cluster region tiles by the world tile used for the biome information */ + // map world tile coord -> vector of region tiles referencing of world title + std::unordered_map> world_tile_region; // iterating over the region details allows the user to do partial map exports // by manually scrolling on the embark site selection - - std::unordered_map> world_tile_region; - for (auto const region_details : world->world_data->midmap_data.region_details) { auto world_x = region_details->pos.x; auto world_y = region_details->pos.y; for (int region_x = 0; region_x < 16; ++region_x) { - for (int region_y = 0; region_y < 16; ++region_y) { - - // auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); - // setGeometry( - // feature, - // (double)(world_x * wdim + region_x * rdim), - // (double)(world_y * wdim + region_y * rdim), - // rdim - // ); - - // get the world tile coordinates used for the biome information of the local region tile + for (int region_y = 0; region_y < 16; ++region_y) + { auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); world_tile_region[biome_tile].emplace_back(16 * world_x + region_x, 16 * world_y + region_y); - } } } @@ -481,18 +410,26 @@ static command_result export_region_tiles(color_ostream &out) for (auto& [biome_tile, region] : world_tile_region) { assert(region.size() > 0); - // compute the connected components of the world tile region - std::ranges::sort(region, region_order); + // sorting the region provides O(log n) membership test. + std::ranges::sort(region, region_order); + + /* Phase I : compute the connected components of the world tile region */ + + // component_assignment[i] is the component id of region[i] std::vector component_assignment; component_assignment.resize(region.size(),0); + + // (indices of) blocks in the current component that have been discovered but not yet explored std::deque agenda; + unsigned int current_component = 0; - auto current_component = 0; for (size_t i = 0; i < region.size(); ++i) { if (component_assignment[i]) { + // skip region tiles that have already been assigned a component continue; } else { + // start a new component for tiles that haven't been assigned yet ++current_component; component_assignment[i] = current_component; agenda.push_back(i); @@ -501,37 +438,47 @@ static command_result export_region_tiles(color_ostream &out) auto pos_idx = agenda.front(); agenda.pop_front(); auto pos = region[pos_idx]; - for (auto const offset : directions) { - // FIXME: use something O(log n) instead of the O(n) find - auto lower = std::ranges::lower_bound(region, pos + offset, region_order); - auto n_it = (*lower == pos + offset) ? lower : region.end(); - - // auto n_it = std::find(region.begin(), region.end(), pos + offset); - auto n_idx = std::distance(region.begin(), n_it); - if (n_it != region.end() && component_assignment[n_idx] == 0) { - component_assignment[n_idx] = current_component; - agenda.push_back(n_idx); + for (const auto& offset : directions) { + auto lb = std::ranges::lower_bound(region, pos + offset, region_order); + if (lb != region.end() && *lb == pos + offset) { + auto n_idx = std::distance(region.begin(), lb); + if (component_assignment[n_idx] == 0) { + component_assignment[n_idx] = current_component; + agenda.push_back(n_idx); + } } } } } - // assert that all parts of the region are accounted for + // check that all parts of the region are accounted for assert(std::ranges::all_of(component_assignment, [](int comp){ return comp > 0;})); + // distribute region tiles according to their component assignment (preserves region order) std::vector> components; components.resize(current_component); for (size_t i = 0; i < region.size(); ++i) { components.at(component_assignment.at(i) - 1).push_back(region.at(i)); } + /* Phase II : create paths by clockwise traversal along the outside of every component */ + /** + * Note: DF uses "picture coordinates" (positive y values go "south") + * while in GIS software positve y values go "north". Thus, [print_path] + * negates the y-coordinates, turning the clockwise traversals into + * counterclockwise traversals as specified by WKT. + * https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry + */ + std::vector> paths; for (auto const &component : components) { + // start at the NW corner of the west-most tile of the northmost row... auto start = component.at(0); std::vector path; path.push_back(start); + // ... ensuring that a step to the east is a valid clockwise step along the boundary. auto current_direction = direction::East; auto current_position = advance(start,current_direction); @@ -548,7 +495,14 @@ static command_result export_region_tiles(color_ostream &out) path.push_back(current_position); current_direction = turn_right(current_direction); } - assert(! (left && !right)); // shape has a hole or is not connected + else if (left && !right) { + // diagonal step: turn right (following the outline of the inclusion) + // this does not seem to happen with the maps currently generated by DF + DEBUG(warning,out).print("Region has self-intersecting outline"); + path.push_back(current_position); + current_direction = turn_right(current_direction); + } + // case !left && right requires no turn; advance the position in all cases current_position = advance(current_position, current_direction); } // close the path @@ -558,6 +512,8 @@ static command_result export_region_tiles(color_ostream &out) } assert(paths.size() > 0); + /* Phase III: output the CSV line */ + auto& region_map_entry = world->world_data->region_map[biome_tile.x][biome_tile.y]; auto world_region = df::world_region::find(region_map_entry.region_id); auto landmass = df::world_landmass::find(region_map_entry.landmass_id); @@ -577,7 +533,7 @@ static command_result export_region_tiles(color_ostream &out) region_map_entry.landmass_id, landmass ? DF2UTF(Translation::translateName(&landmass->name, true)) : "NONE", landmass ? DF2UTF(Translation::translateName(&landmass->name, false)) : "NONE", - // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "snowfall", "salinity" + // "evilness", "savagery", "volcanism", "drainage", "temperature", "vegetation", "rainfall", "salinity" region_map_entry.evilness, region_map_entry.savagery, region_map_entry.volcanism, @@ -585,7 +541,6 @@ static command_result export_region_tiles(color_ostream &out) region_map_entry.temperature, region_map_entry.vegetation, region_map_entry.rainfall, - region_map_entry.snowfall, region_map_entry.salinity, // "surroundings", "elevation", "reanimating", "has_bogeymen" describe_surroundings(region_map_entry.savagery, region_map_entry.evilness), @@ -593,55 +548,17 @@ static command_result export_region_tiles(color_ostream &out) world_region->reanimating, world_region->has_bogeymen ); - // output geometry + + // output geometry as WKT if (paths.size() == 1) { - print_vector(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); + print_range(out_file, paths, print_path , "POLYGON(", ",", ")\n" ); } else { - print_vector(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); + print_range(out_file, paths, print_path , "MULTIPOLYGON((", "),(", "))\n" ); } } - - // feature->SetField( "biome_type", ENUM_KEY_STR(biome_type,Maps::getBiomeType(biome_x,biome_y)).c_str() ); - - // gets supplementary information from the world tile - // auto& region_map_entry = world->world_data->region_map[biome_x][biome_y]; - // #define SET_FIELD(name) feature->SetField( #name, region_map_entry.name) - // SET_FIELD(region_id); - // SET_FIELD(landmass_id); - // SET_FIELD(evilness); - // SET_FIELD(savagery); - // SET_FIELD(volcanism); - // SET_FIELD(drainage); - // SET_FIELD(temperature); - // SET_FIELD(vegetation); - // SET_FIELD(rainfall); - // SET_FIELD(snowfall); - // SET_FIELD(salinity); - // #undef SET_FIELD - - // feature->SetField( "surroundings", describe_surroundings(region_map_entry.savagery, region_map_entry.evilness)); - - // auto region = df::world_region::find(region_map_entry.region_id); - // if (region) { - // auto region_name_en = DF2UTF(Translation::translateName(®ion->name, true)); - // feature->SetField( "region_name_en", region_name_en.c_str()); - // auto region_name_df = DF2UTF(Translation::translateName(®ion->name, false)); - // feature->SetField( "region_name_df", region_name_df.c_str()); - // feature->SetField("reanimating", region->reanimating); - // feature->SetField("has_bogeymen", region->has_bogeymen); - // } - // auto landmass = df::world_landmass::find(region_map_entry.landmass_id); - // if (landmass) { - // auto landmass_name_en = DF2UTF(Translation::translateName(&landmass->name, true)); - // feature->SetField( "landmass_name_en", landmass_name_en.c_str()); - // auto landmass_name_df = DF2UTF(Translation::translateName(&landmass->name, false)); - // feature->SetField( "landmass_name_df", landmass_name_df.c_str()); - // } - - const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %f ms !\n", elapsed_seconds.count()); + out.print("done in %f s !\n", elapsed_seconds.count()); return CR_OK; } From 7043d2c2dcd14a2ecd6bd97fb17e3462bce11504 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:14:26 +0200 Subject: [PATCH 4/8] convert site export to CSV and remove GDAL dependency --- plugins/CMakeLists.txt | 2 +- plugins/export-map.cpp | 227 +++++++++++++---------------------------- 2 files changed, 73 insertions(+), 156 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f87f9002ce..fbc2c9901d 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -71,7 +71,7 @@ if(BUILD_SUPPORTED) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) - dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique LINK_LIBRARIES gdal) + dfhack_plugin(export-map export-map.cpp COMPILE_FLAGS_GCC -fno-gnu-unique) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) dfhack_plugin(fix-occupancy fix-occupancy.cpp LINK_LIBRARIES lua) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 0e0db0e83e..f2887f3859 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -56,32 +56,20 @@ struct std::hash // were only dealing with 2D coordinates in this file using coord = df::coord2d; -static command_result do_command(color_ostream &out, vector ¶meters); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { - DEBUG(log,out).print("initializing %s\n", plugin_name); - - commands.push_back(PluginCommand( - plugin_name, - "Export the world map.", - do_command)); - - return CR_OK; -} +// static int wdim = 768; // dimension of a world tile +static int rdim = 48; // dimension of a region tile -auto setGeometry(OGRFeature *feature, double x, double y, double dimx, double dimy = 0) { - if (dimy == 0) { dimy = dimx; } - auto poly = new OGRPolygon(); - auto boundary = new OGRLinearRing(); - y = -y; // in GIS negative y-coordinates mean further south - boundary->addPoint(x,y); - boundary->addPoint(x,y-dimy); - boundary->addPoint(x+dimx,y-dimy); - boundary->addPoint(x+dimx,y); - boundary->closeRings(); - //the "Directly" variants assume ownership of the objects created above - poly->addRingDirectly(boundary); - feature->SetGeometryDirectly( poly ); +/** + * Takes a vector of coordinates interpreted as global region tile coordinates + * (i.e. 16 region tiles per world tile) and emits a WKT path in GIS-compatible + * local tile coordinates (negative y-coordinates, 48 stepts per region tile) + */ +auto print_path(std::ostream &out, const std::vector &path) { + auto scale = rdim; + assert(path.size()); + auto print_point = [scale](std::ostream &out, const coord &pos){ + out << scale * pos.x << " " << -scale * pos.y;}; + print_range(out, path, print_point, "(", ",", ")"); } df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { @@ -101,23 +89,6 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { return { world_x, world_y }; } -auto create_field(OGRLayer *layer, std::string name, OGRFieldType type, int width = 0, OGRFieldSubType subtype = OFSTNone) { - OGRFieldDefn field( name.c_str() , type ); - if (subtype != OFSTNone) { - field.SetSubType(subtype); - } - if (width != 0) { - field.SetWidth(width); - } - // this should create a copy internally - if( layer->CreateField( &field ) != OGRERR_NONE ){ - throw CR_FAILURE; - } -} - -// PROJ.4 description of EPSG:3857 (https://epsg.io/3857) -static const char* EPSG_3857 = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"; - const char* describe_surroundings(int savagery, int evilness) { constexpr std::arraysurroundings{ "Serene", "Mirthful", "Joyous Wilds", @@ -129,8 +100,18 @@ const char* describe_surroundings(int savagery, int evilness) { return surroundings[3 * evilness_index + savagery_index]; } -// static int wdim = 768; // dimension of a world tile -static int rdim = 48; // dimension of a region tile + +static command_result do_command(color_ostream &out, vector ¶meters); +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(log,out).print("initializing %s\n", plugin_name); + + commands.push_back(PluginCommand( + plugin_name, + "Export the world map.", + do_command)); + + return CR_OK; +} static command_result export_region_tiles(color_ostream &out); static command_result export_sites(color_ostream &out); @@ -154,133 +135,75 @@ static command_result do_command(color_ostream &out, vector ¶meters) } } +/********************************************************************** */ + static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); out.flush(); const auto start{std::chrono::steady_clock::now()}; - // set up coordinate system - OGRSpatialReference CRS; - if (CRS.importFromProj4(EPSG_3857) != OGRERR_NONE) { - out.printerr("could not set up coordinate system"); - return CR_FAILURE; - } - - // set up output driver - GDALAllRegister(); - const char *driver_name = "SQLite"; - const char *extension = "sqlite"; - auto driver = GetGDALDriverManager()->GetDriverByName(driver_name); - if (!driver) { - out.printerr("could not find sqlite driver"); - return CR_FAILURE; - } - - // create a dataset and associate it to a file - std::string sites("sites."); - sites.append(extension); - const char* options[] = { "SPATIALITE=YES", nullptr }; - auto dataset = driver->Create( sites.c_str(), 0, 0, 0, GDT_Unknown, options); - if (!dataset) { - out.printerr("could not create dataset"); - return CR_FAILURE; - } - - // create a layer for the biome data - // const char* format[] = { "FORMAT=WKT", nullptr }; - auto layer = dataset->CreateLayer( "world_sites", &CRS, wkbPolygon, nullptr ); - if (!layer) { - out.printerr("could not create layer"); + // ensure that we have an output file + std::string filename("sites.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { return CR_FAILURE; } - try { - create_field(layer, "site_id", OFTInteger); - create_field(layer, "civ_id", OFTInteger); - create_field(layer, "created_year", OFTInteger); - create_field(layer, "cur_owner_id", OFTInteger); - - create_field(layer, "type", OFTString, 15); - - create_field(layer, "site_name_df", OFTString, 100); - create_field(layer, "site_name_en", OFTString, 100); - - create_field(layer, "civ_name_df", OFTString, 100); - create_field(layer, "civ_name_en", OFTString, 100); - - create_field(layer, "site_government_df", OFTString, 100); - create_field(layer, "site_government_en", OFTString, 100); - - create_field(layer, "owner_race", OFTString, 15); - - // create_field(layer, "local_ruler", OFTString, 100); + // If you change anything in this vector, don't forget to change the + // corresponding comments and arguments in the call to print_csv below + vector headings = { + "site_id", "civ_id", "created_year", "cur_owner_id", "type", + "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + }; + print_range(out_file, headings,"",";",";boundary_wkt\n" ); - } - catch (const DFHack::command_result& r) { - out.printerr("could not create fields for output layer"); - return r; - } - - if (dataset->StartTransaction() != OGRERR_NONE) { - out.printerr("could not start a transaction\n"); - } + #define TRANSLATE_DF_EN(guard, name_object)\ + guard ? DF2UTF(Translation::translateName(&name_object, false)) : "NONE",\ + guard ? DF2UTF(Translation::translateName(&name_object, true)) : "NONE" for (auto const site : world->world_data->sites) { - - auto feature = OGRFeature::CreateFeature( layer->GetLayerDefn() ); - - setGeometry( - feature, - site->global_min_x * rdim, - site->global_min_y * rdim, - (site->global_max_x - site->global_min_x + 1) * rdim, - (site->global_max_y - site->global_min_y + 1) * rdim - ); - feature->SetField( "site_id", site->id ); - feature->SetField( "type", ENUM_KEY_STR(world_site_type, site->type).c_str() ); - #define SET_FIELD(name) feature->SetField( #name, site->name) - SET_FIELD(civ_id); - SET_FIELD(created_year); - SET_FIELD(cur_owner_id); - #undef SET_FIELD - - #define TRANSLATE_NAME(field_name, name_object)\ - feature->SetField((#field_name"_df"), DF2UTF(Translation::translateName(&name_object, false)).c_str());\ - feature->SetField((#field_name"_en"), DF2UTF(Translation::translateName(&name_object, true)).c_str()); - - TRANSLATE_NAME(site_name, site->name) - auto civ = df::historical_entity::find(site->civ_id); - if (civ) { TRANSLATE_NAME(civ_name,civ->name) } - auto owner = df::historical_entity::find(site->cur_owner_id); - if (owner) { - TRANSLATE_NAME(site_government,owner->name) - auto race = df::creature_raw::find(owner->race); - if (!race){ - race = df::creature_raw::find(civ->race); - } - if (race) { - feature->SetField( "owner_race", race->name[2].c_str() ); + + df::creature_raw *race = nullptr; + if (owner){ + race = df::creature_raw::find(owner->race); + DEBUG(warning, out).print("owner (%d) of site (%d) has undefined race (%d)", owner->id, site->id, owner->race); + if (!race) { + df::creature_raw::find(civ->race); } } - - // this updates the feature with the id it receives in the layer - if( layer->CreateFeature( feature ) != OGRERR_NONE ) - return CR_FAILURE; - // but we don't care and destroy the feature - OGRFeature::DestroyFeature( feature ); + auto print_csv = [&out_file](auto ...args){ ([&]{ out_file << args << ";" ; }() ,...); }; + print_csv( + // "site_id", "civ_id", "created_year", "cur_owner_id", "type", + site->id, + site->civ_id, + site->created_year, + site->cur_owner_id, + ENUM_KEY_STR(world_site_type, site->type), + // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" + TRANSLATE_DF_EN(true, site->name), + TRANSLATE_DF_EN(civ, civ->name), + TRANSLATE_DF_EN(owner, owner->name), + race ? race->name[2] : "NONE" + ); + const vector path{ + coord(site->global_min_x, site->global_min_y), + coord(site->global_max_x+1, site->global_min_y), + coord(site->global_max_x+1, site->global_max_y+1), + coord(site->global_min_x, site->global_max_y+1), + coord(site->global_min_x, site->global_min_y) + }; + print_range(out_file, std::vector>{path}, print_path , "POLYGON(", ",", ")\n" ); } - dataset->CommitTransaction(); - GDALClose( dataset ); const auto finish{std::chrono::steady_clock::now()}; const std::chrono::duration elapsed_seconds{finish - start}; - out.print("done in %f ms !\n", elapsed_seconds.count()); + out.print("done in %.2fs !\n", elapsed_seconds.count()); return CR_OK; } @@ -321,13 +244,7 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } -auto print_path(std::ostream &out, const std::vector &path) { - auto scale = rdim; - assert(path.size()); - auto print_point = [scale](std::ostream &out, const coord &pos){ - out << scale * pos.x << " " << -scale * pos.y;}; - print_range(out, path, print_point, "(", ",", ")"); -} + From 3468e6f20265f7f823f928be50cfe14d29fd4ec8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 3 Apr 2025 21:15:32 +0200 Subject: [PATCH 5/8] use proper site classification and remove gdal include --- plugins/export-map.cpp | 87 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index f2887f3859..f21570d2d7 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -13,13 +13,12 @@ #include "df/map_block.h" #include "df/world_data.h" #include "df/world_site.h" +#include "df/site_map_infost.h" #include "df/region_map_entry.h" #include "df/world_region.h" #include "df/world_landmass.h" #include "df/world_region_details.h" -#include "gdal/ogrsf_frmts.h" - #include #include #include @@ -137,6 +136,87 @@ static command_result do_command(color_ostream &out, vector ¶meters) /********************************************************************** */ +const char* classify_site(df::world_site *site) { + using wst = df::enums::world_site_type::world_site_type; + switch (site->type) { + case wst::PlayerFortress: + case wst::MountainHalls: + if (site->min_depth == 0 && (0 < site->max_depth)){ + return "fortress"; + } + if (site->min_depth > 0) { + return "mountain halls"; + } + return "hillocks"; + + case wst::DarkFortress: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "fortress" : "pits"; + } + + case wst::Cave: + return "cave"; + + case wst::ForestRetreat: + return "forest retreat"; + + case wst::Town: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "town" : "hamlet"; + } + + case wst::ImportantLocation: + return "important location"; + + case wst::LairShrine: + if (site->subtype_info) { + switch (site->subtype_info->lair_type) { + case df::enums::lair_type::LABYRINTH: + return "labyrinth"; + case df::enums::lair_type::SHRINE: + return "shrine"; + default: + break; + } + } + return "lair"; + + case wst::Fortress: + if (site->subtype_info) { + switch (site->subtype_info->fortress_type) { + case df::enums::fortress_type::TOWER: + return "tower"; + case df::enums::fortress_type::MONASTERY: + return "monastery"; + case df::enums::fortress_type::FORT: + return "fort"; + default: + return "castle"; + } + } + return "fortress"; + + case wst::Camp: + return "camp"; + + case wst::Monument: + if (site->subtype_info) { + switch (site->subtype_info->monument_type) { + case df::enums::monument_type::TOMB: + return "tomb"; + case df::enums::monument_type::VAULT: + return "vault"; + default: + break; + } + } + return "monument"; + + default: + return "site"; + } +} + static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); @@ -183,7 +263,8 @@ static command_result export_sites(color_ostream &out) site->civ_id, site->created_year, site->cur_owner_id, - ENUM_KEY_STR(world_site_type, site->type), + //ENUM_KEY_STR(world_site_type, site->type), + classify_site(site), // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" TRANSLATE_DF_EN(true, site->name), TRANSLATE_DF_EN(civ, civ->name), From 5856c803a1131f1465a39380ba7166644e53acba Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:12:58 +0200 Subject: [PATCH 6/8] move sites classification to map module and additional cleanup --- library/include/modules/Maps.h | 9 +++ library/modules/Maps.cpp | 86 ++++++++++++++++++++++++ plugins/export-map.cpp | 117 +++++---------------------------- 3 files changed, 110 insertions(+), 102 deletions(-) diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c..c57dab4ea0 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -42,6 +42,7 @@ distribution. #include "df/flow_type.h" #include "df/tile_dig_designation.h" #include "df/tiletype.h" +#include "df/world_site.h" namespace df { struct block_square_event; @@ -399,6 +400,14 @@ DFHACK_EXPORT bool removeTileAquifer(int32_t x, int32_t y, int32_t z); inline bool removeTileAquifer(df::coord pos) { return removeTileAquifer(pos.x, pos.y, pos.z); } DFHACK_EXPORT int removeAreaAquifer(df::coord pos1, df::coord pos2, std::function filter = [](df::coord pos, df::map_block *block) { return true; }); + + +/** + * A single function does not merit a "Sites" module, hence we collect site functions here in the meantime. + */ + +// Get the classification string (e.g. "town", "hillocs", "tower", etc.) for a site +DFHACK_EXPORT const char* getSiteTypeName(df::world_site *site); } } #endif diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index 54fb43c810..1b7b854deb 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -1265,3 +1265,89 @@ int Maps::removeAreaAquifer(df::coord pos1, df::coord pos2, std::functiontype) { + case wst::PlayerFortress: + case wst::MountainHalls: + if (site->min_depth == 0 && (0 < site->max_depth)){ + return "fortress"; + } + if (site->min_depth > 0) { + return "mountain halls"; + } + return "hillocks"; + + case wst::DarkFortress: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "fortress" : "pits"; + } + + case wst::Cave: + return "cave"; + + case wst::ForestRetreat: + return "forest retreat"; + + case wst::Town: { + bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); + return has_market ? "town" : "hamlet"; + } + + case wst::ImportantLocation: + return "important location"; + + case wst::LairShrine: + if (site->subtype_info) { + switch (site->subtype_info->lair_type) { + case df::enums::lair_type::LABYRINTH: + return "labyrinth"; + case df::enums::lair_type::SHRINE: + return "shrine"; + default: + break; + } + } + return "lair"; + + case wst::Fortress: + if (site->subtype_info) { + switch (site->subtype_info->fortress_type) { + case df::enums::fortress_type::TOWER: + return "tower"; + case df::enums::fortress_type::MONASTERY: + return "monastery"; + case df::enums::fortress_type::FORT: + return "fort"; + default: + return "castle"; + } + } + return "fortress"; + + case wst::Camp: + return "camp"; + + case wst::Monument: + if (site->subtype_info) { + switch (site->subtype_info->monument_type) { + case df::enums::monument_type::TOMB: + return "tomb"; + case df::enums::monument_type::VAULT: + return "vault"; + default: + break; + } + } + return "monument"; + + default: + return "site"; + } +} diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index f21570d2d7..973ed169ed 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -1,32 +1,31 @@ #include "Debug.h" #include "Error.h" -#include "PluginManager.h" #include "MiscUtils.h" +#include "PluginManager.h" #include "modules/Maps.h" #include "modules/Translation.h" -#include "df/entity_raw.h" #include "df/creature_raw.h" +#include "df/entity_raw.h" #include "df/historical_entity.h" -#include "df/world.h" #include "df/map_block.h" -#include "df/world_data.h" -#include "df/world_site.h" -#include "df/site_map_infost.h" #include "df/region_map_entry.h" -#include "df/world_region.h" +#include "df/site_map_infost.h" +#include "df/world_data.h" #include "df/world_landmass.h" #include "df/world_region_details.h" +#include "df/world_region.h" +#include "df/world.h" -#include -#include -#include -#include #include -#include +#include +#include #include - +#include +#include +#include +#include using std::string; using std::vector; @@ -83,8 +82,8 @@ df::coord2d get_world_index(int16_t world_x, int16_t world_y, int8_t dir) { case 8: ; world_y--; break; case 9: world_x++ ; world_y--; break; } - world_x = std::min(std::max((int16_t)0,world_x),(int16_t)(world->world_data->world_width - 1)); - world_y = std::min(std::max((int16_t)0,world_y),(int16_t)(world->world_data->world_height - 1)); + world_x = std::clamp(world_x,(int16_t)0,(int16_t)(world->world_data->world_width - 1)); + world_y = std::clamp(world_y,(int16_t)0,(int16_t)(world->world_data->world_height - 1)); return { world_x, world_y }; } @@ -136,87 +135,6 @@ static command_result do_command(color_ostream &out, vector ¶meters) /********************************************************************** */ -const char* classify_site(df::world_site *site) { - using wst = df::enums::world_site_type::world_site_type; - switch (site->type) { - case wst::PlayerFortress: - case wst::MountainHalls: - if (site->min_depth == 0 && (0 < site->max_depth)){ - return "fortress"; - } - if (site->min_depth > 0) { - return "mountain halls"; - } - return "hillocks"; - - case wst::DarkFortress: { - bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); - return has_market ? "fortress" : "pits"; - } - - case wst::Cave: - return "cave"; - - case wst::ForestRetreat: - return "forest retreat"; - - case wst::Town: { - bool has_market = site->flag.is_set(df::enums::site_flag_type::HAS_MARKET); - return has_market ? "town" : "hamlet"; - } - - case wst::ImportantLocation: - return "important location"; - - case wst::LairShrine: - if (site->subtype_info) { - switch (site->subtype_info->lair_type) { - case df::enums::lair_type::LABYRINTH: - return "labyrinth"; - case df::enums::lair_type::SHRINE: - return "shrine"; - default: - break; - } - } - return "lair"; - - case wst::Fortress: - if (site->subtype_info) { - switch (site->subtype_info->fortress_type) { - case df::enums::fortress_type::TOWER: - return "tower"; - case df::enums::fortress_type::MONASTERY: - return "monastery"; - case df::enums::fortress_type::FORT: - return "fort"; - default: - return "castle"; - } - } - return "fortress"; - - case wst::Camp: - return "camp"; - - case wst::Monument: - if (site->subtype_info) { - switch (site->subtype_info->monument_type) { - case df::enums::monument_type::TOMB: - return "tomb"; - case df::enums::monument_type::VAULT: - return "vault"; - default: - break; - } - } - return "monument"; - - default: - return "site"; - } -} - static command_result export_sites(color_ostream &out) { out.print("exporting sites... "); @@ -263,8 +181,7 @@ static command_result export_sites(color_ostream &out) site->civ_id, site->created_year, site->cur_owner_id, - //ENUM_KEY_STR(world_site_type, site->type), - classify_site(site), + DFHack::Maps::getSiteTypeName(site), // "site_name_df", "site_name_en", "civ_name_df", "civ_name_en", "site_government_df", "site_government_en", "owner_race" TRANSLATE_DF_EN(true, site->name), TRANSLATE_DF_EN(civ, civ->name), @@ -325,10 +242,6 @@ coord advance(coord pos, direction dir) { return pos + as_offset(dir); } - - - - std::pair ahead(const std::vector &component, coord pos, direction dir) { auto test = [&](int16_t x, int16_t y){ coord offset{x,y}; From b53b44f584deb2ee262b1ade9b6b28c1bc9d73fd Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 4 May 2025 20:23:10 +0200 Subject: [PATCH 7/8] first version of river export in C++ --- plugins/export-map.cpp | 161 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 1 deletion(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index 973ed169ed..c817ecf989 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -16,6 +16,7 @@ #include "df/world_landmass.h" #include "df/world_region_details.h" #include "df/world_region.h" +#include "df/world_river.h" #include "df/world.h" #include @@ -23,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -54,7 +57,7 @@ struct std::hash // were only dealing with 2D coordinates in this file using coord = df::coord2d; -// static int wdim = 768; // dimension of a world tile +static int wdim = 768; // dimension of a world tile static int rdim = 48; // dimension of a region tile /** @@ -113,6 +116,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector ¶meters) { @@ -127,6 +131,9 @@ static command_result do_command(color_ostream &out, vector ¶meters) { return export_sites(out); } + else if (parameters.size() && parameters[0] == "rivers") { + return export_rivers(out); + } else { return export_region_tiles(out); @@ -473,3 +480,155 @@ static command_result export_region_tiles(color_ostream &out) out.print("done in %f s !\n", elapsed_seconds.count()); return CR_OK; } + +/********************************************************************** */ + +// used for global coordinates at local tile granularity (129*768 = 99072 doesn't fit into df::coord2d) +struct gcoord { + int x,y; +}; + +struct river_tile { + using polygon_t = std::list; + polygon_t polygon; + //std::optional north, south, west, east; +}; + +struct gate { + int active,min,max; + + static gate get(const df::world_region_details *const region_details, int region_x, int region_y, direction dir) { + auto& vertical = region_details->rivers_vertical; + auto& horizontal = region_details->rivers_horizontal; + switch (dir) { + case direction::North: + return { vertical.active[region_x][region_y], vertical.x_min[region_x][region_y], vertical.x_max[region_x][region_y] }; + case direction::West: + return { horizontal.active[region_x][region_y], horizontal.y_min[region_x][region_y], horizontal.y_max[region_x][region_y] }; + case direction::South: + return { vertical.active[region_x][region_y+1], vertical.x_min[region_x][region_y+1], vertical.x_max[region_x][region_y+1] }; + case direction::East: + return { horizontal.active[region_x+1][region_y], horizontal.y_min[region_x+1][region_y], horizontal.y_max[region_x+1][region_y] }; + default: + assert(false); + return {}; + } + } + + bool is_valid() const { + return active != 0 && min != -30000 && max != -30000; + } +}; + + +bool is_land(const df::world_region_details *const region_details, int16_t region_x, int16_t region_y) { + CHECK_NULL_POINTER(region_details); + auto [world_x, world_y] = region_details->pos; + auto biome_tile = get_world_index(world_x, world_y, region_details->biome[region_x][region_y]); + auto region_map_entry = Maps::getRegionBiome(biome_tile); + CHECK_NULL_POINTER(region_map_entry); + return region_map_entry->elevation >= 100 && !region_map_entry->flags.is_set(df::enums::region_map_entry_flags::is_lake); +} + +static command_result export_rivers(color_ostream &out) +{ + // ensure that we have an output file + std::string filename("rivers.csv"); + std::ofstream out_file(filename, std::ios::out | std::ios::trunc); + if (!out_file) { + return CR_FAILURE; + } + + // create lookup table for rivers based on world tile coordinates + std::unordered_map world_river; + + // assign river end first, so that it can be overridden by proper path elements + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + world_river[river->end_pos] = r_idx; + } + + for (size_t r_idx = 0; r_idx < df::global::world->world_data->rivers.size(); ++r_idx) { + auto river = df::global::world->world_data->rivers[r_idx]; + for (size_t i = 0; i < river->path.size(); ++i) { + auto pos = river->path[i]; + world_river[pos] = r_idx; + } + } + + // river idx -> region tile coord -> tile + std::unordered_map> tile_index; + + for (auto const region_details : world->world_data->midmap_data.region_details) { + auto [world_x, world_y] = region_details->pos; + for (int region_x = 0; region_x < 16; ++region_x) { + for (int region_y = 0; region_y < 16; ++region_y) + { + gcoord base = { world_x * wdim + region_x * rdim, world_y * wdim + region_y * rdim }; + + auto north = gate::get(region_details, region_x, region_y, direction::North); + auto west = gate::get(region_details, region_x, region_y, direction::West); + auto south = gate::get(region_details, region_x, region_y, direction::South); + auto east = gate::get(region_details, region_x, region_y, direction::East); + + // skip tiles without any gates + if (!(north.is_valid() || west.is_valid() || south.is_valid() || east.is_valid())) + continue; + + // skip any river tiles that are on oceans or lakes + if (!is_land(region_details, region_x, region_y)) + continue; + + river_tile tile; + + if (north.is_valid()) { + tile.polygon.emplace_back(base.x + north.max, base.y); + tile.polygon.emplace_back(base.x + north.min, base.y); + } + if (west.is_valid()) { + tile.polygon.emplace_back(base.x, base.y + west.min); + tile.polygon.emplace_back(base.x, base.y + west.max); + } + if (south.is_valid()) { + tile.polygon.emplace_back(base.x + south.min, base.y + rdim); + tile.polygon.emplace_back(base.x + south.max, base.y + rdim); + } + if (east.is_valid()) { + tile.polygon.emplace_back(base.x + rdim, base.y + east.max); + tile.polygon.emplace_back(base.x + rdim, base.y + east.min); + } + + auto r_idx = world_river.at({world_x, world_y}); + tile_index[r_idx][ coord(world_x * 16 + region_x, world_y * 16 + region_y) ] = std::move(tile); + } + } + } + + + // generate output + out_file << "name_df;name_en;geometry_wkt\n"; + + for (auto& [r_idx, river_index] : tile_index) { + auto river = world->world_data->rivers.at(r_idx); + out_file << DF2UTF(Translation::translateName(&river->name, false)) << ";"; + out_file << DF2UTF(Translation::translateName(&river->name, true)) << ";"; + out_file << "MULTIPOLYGON("; + bool first = true; + for (auto &[tile_pos, tile] : river_index) { + tile.polygon.emplace_back(*tile.polygon.begin()); + auto print_position = [](std::ostream &out, gcoord pos) { + out << pos.x << " " << -pos.y; + }; + if (first) { + first = false; + } else { + out_file << ","; + } + print_range(out_file, tile.polygon, print_position, "((", ",", "))"); + } + out_file << ")\n"; + } + + + return CR_OK; +} From 95eab7ffdab2927187b11e2350d9ab44a5ec9785 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 4 May 2025 20:57:48 +0200 Subject: [PATCH 8/8] default to exporting all layers --- plugins/export-map.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/export-map.cpp b/plugins/export-map.cpp index c817ecf989..4c72af1b90 100644 --- a/plugins/export-map.cpp +++ b/plugins/export-map.cpp @@ -134,9 +134,14 @@ static command_result do_command(color_ostream &out, vector ¶meters) else if (parameters.size() && parameters[0] == "rivers") { return export_rivers(out); } + else if (parameters.size() && parameters[0] == "regions") { + return export_region_tiles(out); + } else { - return export_region_tiles(out); + auto ok_region = export_region_tiles(out); + auto ok_sites = ok_region == CR_OK ? export_sites(out) : ok_region; + return ok_sites == CR_OK ? export_rivers(out) : ok_sites; } }