diff --git a/src/map/CMakeLists.txt b/src/map/CMakeLists.txt index 0119220..b993f49 100644 --- a/src/map/CMakeLists.txt +++ b/src/map/CMakeLists.txt @@ -1,82 +1,83 @@ flex_target(mapcssscanner style/mapcsslexer.l ${CMAKE_CURRENT_BINARY_DIR}/mapcsslexer.cpp DEFINES_FILE ${CMAKE_CURRENT_BINARY_DIR}/mapcssscanner.h COMPILE_FLAGS "--nounistd" ) bison_target(mapcssparser style/mapcssparser.y ${CMAKE_CURRENT_BINARY_DIR}/mapcssparser_p.cpp DEFINES_FILE ${CMAKE_CURRENT_BINARY_DIR}/mapcssparser_p.h ) add_flex_bison_dependency(mapcssscanner mapcssparser) add_library(KOSMIndoorMap STATIC assets/assets.qrc loader/boundarysearch.cpp loader/mapdata.cpp loader/maploader.cpp + loader/marblegeometryassembler.cpp loader/tilecache.cpp renderer/hitdetector.cpp renderer/painterrenderer.cpp renderer/view.cpp scene/scenecontroller.cpp scene/scenegeometry.cpp scene/scenegraph.cpp scene/scenegraphitem.cpp style/mapcsscondition.cpp style/mapcssdeclaration.cpp style/mapcssparser.cpp style/mapcssresult.cpp style/mapcssrule.cpp style/mapcssselector.cpp style/mapcssstate.cpp style/mapcssstyle.cpp ${BISON_mapcssparser_OUTPUTS} ${FLEX_mapcssscanner_OUTPUTS} ) target_include_directories(KOSMIndoorMap PRIVATE $) target_include_directories(KOSMIndoorMap PUBLIC $) target_link_libraries(KOSMIndoorMap PUBLIC Qt5::Gui KOSM PRIVATE Qt5::Network ) ecm_generate_headers(KOSMIndoorMap_Loader_FORWARDING_HEADERS HEADER_NAMES MapLoader MapData PREFIX KOSMIndoorMap REQUIRED_HEADERS KOSMIndoorMap_Loader_HEADERS RELATIVE loader ) ecm_generate_headers(KOSMIndoorMap_Renderer_FORWARDING_HEADERS HEADER_NAMES HitDetector PainterRenderer View PREFIX KOSMIndoorMap REQUIRED_HEADERS KOSMIndoorMap_Renderer_HEADERS RELATIVE renderer ) ecm_generate_headers(KOSMIndoorMap_Scene_FORWARDING_HEADERS HEADER_NAMES SceneController SceneGraph PREFIX KOSMIndoorMap REQUIRED_HEADERS KOSMIndoorMap_Scene_HEADERS RELATIVE scene ) ecm_generate_headers(KOSMIndoorMap_Style_FORWARDING_HEADERS HEADER_NAMES MapCSSParser MapCSSStyle PREFIX KOSMIndoorMap REQUIRED_HEADERS KOSMIndoorMap_Style_HEADERS RELATIVE style ) diff --git a/src/map/loader/maploader.cpp b/src/map/loader/maploader.cpp index 369f442..68ca004 100644 --- a/src/map/loader/maploader.cpp +++ b/src/map/loader/maploader.cpp @@ -1,173 +1,175 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "maploader.h" #include "boundarysearch.h" #include #include #include #include #include enum { TileZoomLevel = 17 }; inline void initResources() // needs to be outside of a namespace { Q_INIT_RESOURCE(assets); } using namespace KOSMIndoorMap; MapLoader::MapLoader(QObject *parent) : QObject(parent) { initResources(); connect(&m_tileCache, &TileCache::tileLoaded, this, &MapLoader::downloadFinished); } MapLoader::~MapLoader() = default; void MapLoader::loadFromO5m(const QString &fileName) { QElapsedTimer loadTime; loadTime.start(); QFile f(fileName); if (!f.open(QFile::ReadOnly)) { qCritical() << f.fileName() << f.errorString(); return; } const auto data = f.map(0, f.size()); OSM::DataSet ds; OSM::O5mParser p(&ds); p.parse(data, f.size()); m_data.setDataSet(std::move(ds)); qDebug() << "o5m loading took" << loadTime.elapsed() << "ms"; Q_EMIT done(); } void MapLoader::loadForCoordinate(double lat, double lon) { m_tileBbox = {}; m_pendingTiles.clear(); m_boundarySearcher.init(OSM::Coordinate(lat, lon)); const auto tile = Tile::fromCoordinate(lat, lon, TileZoomLevel); m_pendingTiles.push_back(tile); m_loadedTiles = QRect(tile.x, tile.y, 1, 1); downloadTiles(); } MapData&& MapLoader::takeData() { return std::move(m_data); } void MapLoader::downloadTiles() { for (const auto &tile : m_pendingTiles) { m_tileCache.ensureCached(tile); } if (m_tileCache.pendingDownloads() == 0) { loadTiles(); } else { Q_EMIT isLoadingChanged(); } } void MapLoader::downloadFinished() { if (m_tileCache.pendingDownloads() > 0) { return; } loadTiles(); } void MapLoader::loadTiles() { QElapsedTimer loadTime; loadTime.start(); OSM::O5mParser p(&m_dataSet); + //p.setMergeBuffer(&m_mergeBuffer); for (const auto &tile : m_pendingTiles) { const auto fileName = m_tileCache.cachedTile(tile); qDebug() << fileName; QFile f(fileName); if (!f.open(QFile::ReadOnly)) { qWarning() << f.fileName() << f.errorString(); break; } const auto data = f.map(0, f.size()); p.parse(data, f.size()); + //m_marbleMerger.merge(&m_dataSet, &m_mergeBuffer); m_tileBbox = OSM::unite(m_tileBbox, tile.boundingBox()); } m_pendingTiles.clear(); const auto bbox = m_boundarySearcher.boundingBox(m_dataSet); qDebug() << "needed bbox:" << bbox << "got:" << m_tileBbox << m_loadedTiles; // expand left and right if (bbox.min.longitude < m_tileBbox.min.longitude) { m_loadedTiles.setLeft(m_loadedTiles.left() - 1); for (int y = m_loadedTiles.top(); y <= m_loadedTiles.bottom(); ++y) { m_pendingTiles.push_back(Tile(m_loadedTiles.left(), y, TileZoomLevel)); } } if (bbox.max.longitude > m_tileBbox.max.longitude) { m_loadedTiles.setRight(m_loadedTiles.right() + 1); for (int y = m_loadedTiles.top(); y <= m_loadedTiles.bottom(); ++y) { m_pendingTiles.push_back(Tile(m_loadedTiles.right(), y, TileZoomLevel)); } } // expand top/bottom: note that geographics and slippy map tile coordinates have a different understanding on what is "top" if (bbox.max.latitude > m_tileBbox.max.latitude) { m_loadedTiles.setTop(m_loadedTiles.top() - 1); for (int x = m_loadedTiles.left(); x <= m_loadedTiles.right(); ++x) { m_pendingTiles.push_back(Tile(x, m_loadedTiles.top(), TileZoomLevel)); } } if (bbox.min.latitude < m_tileBbox.min.latitude) { m_loadedTiles.setBottom(m_loadedTiles.bottom() + 1); for (int x = m_loadedTiles.left(); x <= m_loadedTiles.right(); ++x) { m_pendingTiles.push_back(Tile(x, m_loadedTiles.bottom(), TileZoomLevel)); } } if (!m_pendingTiles.empty()) { downloadTiles(); return; } m_data.setDataSet(std::move(m_dataSet)); m_data.setBoundingBox(bbox); qDebug() << "o5m loading took" << loadTime.elapsed() << "ms"; Q_EMIT isLoadingChanged(); Q_EMIT done(); } bool MapLoader::isLoading() const { return m_tileCache.pendingDownloads() > 0; } diff --git a/src/map/loader/maploader.h b/src/map/loader/maploader.h index 5b8d39f..930f55f 100644 --- a/src/map/loader/maploader.h +++ b/src/map/loader/maploader.h @@ -1,77 +1,81 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KOSMINDOORMAP_MAPLOADER_H #define KOSMINDOORMAP_MAPLOADER_H #include "boundarysearch.h" #include "mapdata.h" +#include "marblegeometryassembler.h" #include "tilecache.h" #include +#include #include #include namespace KOSMIndoorMap { /** Loader for OSM data for a single station or airport. */ class MapLoader : public QObject { Q_OBJECT /** Indicates we are downloading content. Use for progress display. */ Q_PROPERTY(bool isLoading READ isLoading NOTIFY isLoadingChanged) public: explicit MapLoader(QObject *parent = nullptr); ~MapLoader(); /** Load a single O5M file. */ void loadFromO5m(const QString &fileName); /** Load map for the given coordinates. * This can involve online access. */ Q_INVOKABLE void loadForCoordinate(double lat, double lon); /** Take out the completely loaded result. * Do this before loading the next map with the same loader. */ MapData&& takeData(); bool isLoading() const; Q_SIGNALS: /** Emitted when the requested data has been loaded. */ void done(); void isLoadingChanged(); private: void downloadTiles(); void downloadFinished(); void loadTiles(); OSM::DataSet m_dataSet; + OSM::DataSetMergeBuffer m_mergeBuffer; + MarbleGeometryAssembler m_marbleMerger; MapData m_data; TileCache m_tileCache; OSM::BoundingBox m_tileBbox; QRect m_loadedTiles; std::vector m_pendingTiles; BoundarySearch m_boundarySearcher; }; } #endif // KOSMINDOORMAP_MAPLOADER_H diff --git a/src/map/loader/marblegeometryassembler.cpp b/src/map/loader/marblegeometryassembler.cpp new file mode 100644 index 0000000..5ed2362 --- /dev/null +++ b/src/map/loader/marblegeometryassembler.cpp @@ -0,0 +1,179 @@ +/* + Copyright (C) 2020 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "marblegeometryassembler.h" + +using namespace KOSMIndoorMap; + +/** Compare two coordinate while accounting for floating point noise. */ +static bool fuzzyEquals(OSM::Coordinate lhs, OSM::Coordinate rhs) +{ + return std::abs((int32_t)lhs.latitude - (int32_t)rhs.latitude) < 10 && std::abs((int32_t)lhs.longitude - (int32_t)rhs.longitude) < 10; +} + +void MarbleGeometryAssembler::merge(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer) +{ + m_wayIdMap.clear(); + m_mxoidKey = dataSet->tagKey("mx:oid"); + + mergeNodes(dataSet, mergeBuffer); + mergeWays(dataSet, mergeBuffer); + mergeRelations(dataSet, mergeBuffer); + + mergeBuffer->clear(); +} + +void MarbleGeometryAssembler::mergeNodes(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer) +{ + // nodes we just take as-is, they are not split/renamed; at worst we get a few extra for split geometry + if (dataSet->nodes.empty()) { + dataSet->nodes = std::move(mergeBuffer->nodes); + std::sort(dataSet->nodes.begin(), dataSet->nodes.end()); + } else { + dataSet->nodes.reserve(dataSet->nodes.size() + mergeBuffer->nodes.size()); + std::move(mergeBuffer->nodes.begin(), mergeBuffer->nodes.end(), std::back_inserter(dataSet->nodes)); + } + std::sort(dataSet->nodes.begin(), dataSet->nodes.end()); +} + +void MarbleGeometryAssembler::mergeWays(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer) +{ + // for ways we do: + // 1. restore the original id + // 2. if a way with that id already exists, we merge with the geometry of the existing one + + for (auto &way : mergeBuffer->ways) { + if (way.id > 0) { // not a synthetic id + dataSet->addWay(std::move(way)); + continue; + } + + const OSM::Id mxoid = takeMxOid(way); + if (mxoid <= 0) { // shouldn't happen? + dataSet->addWay(std::move(way)); + continue; + } + + m_wayIdMap[way.id] = mxoid; + way.id = mxoid; + + const auto it = std::lower_bound(dataSet->ways.begin(), dataSet->ways.end(), way); + if (it != dataSet->ways.end() && (*it).id == way.id) { + mergeWay(dataSet, *it, way); + } else { + dataSet->ways.insert(it, std::move(way)); + } + } +} + +void MarbleGeometryAssembler::mergeWay(const OSM::DataSet *dataSet, OSM::Way &way, const OSM::Way &otherWay) const +{ + // for merging two ways: + // - non-synthetic nodes remain unchanged, ways can only be merged on synthetic nodes + // - synthetic nodes are duplicated in both sets, we need to merge them by coordinate comparison + // - synthetic nodes can be removed, and so can edges between two adjacent synthetic nodes + // - closed polygons at least have one shared edge (possibly multiple adjacent or non-adjacent ones) + // - lines can only be merged at the beginning or the end, a line crossing the same boundary multiple times would be split at every boundary intersection + // - we can assume polygon orientation is preserved by the splitting + + if (!way.isClosed() && !otherWay.isClosed()) { + mergeLine(dataSet, way, otherWay); + } else { + mergeArea(dataSet, way, otherWay); + } +} + +void MarbleGeometryAssembler::mergeLine(const OSM::DataSet *dataSet, OSM::Way &way, const OSM::Way &otherWay) const +{ + const auto begin1 = nodeForId(dataSet, way.nodes.front()); + const auto end1 = nodeForId(dataSet, way.nodes.back()); + const auto begin2 = nodeForId(dataSet, otherWay.nodes.front()); + const auto end2 = nodeForId(dataSet, otherWay.nodes.back()); + if (!begin1 || !end1 || !begin2 || !end2) { + qDebug() << "failed to find way nodes!?" << begin1 << end1 << begin2 << end2;; + return; + } + + // TODO drop the extra synthetic nodes + way.nodes.reserve(way.nodes.size() + otherWay.nodes.size()); + if (fuzzyEquals(end1->coordinate, begin2->coordinate)) { + std::copy(otherWay.nodes.begin(), otherWay.nodes.end(), std::back_inserter(way.nodes)); + } else if (fuzzyEquals(end1->coordinate, end2->coordinate)) { + std::copy(otherWay.nodes.rbegin(), otherWay.nodes.rend(), std::back_inserter(way.nodes)); + } else if (fuzzyEquals(begin1->coordinate, end2->coordinate)) { + way.nodes.insert(way.nodes.begin(), otherWay.nodes.begin(), otherWay.nodes.end()); + } else if (fuzzyEquals(begin1->coordinate, begin2->coordinate)) { + way.nodes.insert(way.nodes.begin(), otherWay.nodes.rbegin(), otherWay.nodes.rend()); + } else { + qDebug() << "unable to merge line:" << begin1->coordinate << end1->coordinate << begin2->coordinate << end2->coordinate; + } +} + +void MarbleGeometryAssembler::mergeArea(const OSM::DataSet* dataSet, OSM::Way &way, const OSM::Way &otherWay) const +{ + // TODO +} + +void MarbleGeometryAssembler::mergeRelations(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer) +{ + // for relations we do: + // 1. restore the original id + // 2. replace all member ids with the restored ids for ways/relations + // 3. if a relations with the restored id already exists, merge with its content + + for (auto &rel : mergeBuffer->relations) { + for (auto &member : rel.members) { + if (member.id >= 0) { // not a synthetic id + continue; + } + + if (member.type == OSM::Type::Way) { + const auto it = m_wayIdMap.find(member.id); + if (it != m_wayIdMap.end()) { + member.id = (*it).second; + } + } + } + + // ### temporary scaffolding + dataSet->addRelation(std::move(rel)); + } +} + +template +OSM::Id MarbleGeometryAssembler::takeMxOid(Elem &elem) const +{ + const auto it = std::lower_bound(elem.tags.begin(), elem.tags.end(), m_mxoidKey, [](const auto &lhs, const auto &rhs) { return lhs.key < rhs; }); + if (it != elem.tags.end() && (*it).key == m_mxoidKey) { + bool result = false; + const OSM::Id id = (*it).value.toLongLong(&result); + if (result) { + elem.tags.erase(it); + return id; + } + } + return {}; +} + +const OSM::Node* MarbleGeometryAssembler::nodeForId(const OSM::DataSet *dataSet, OSM::Id id) const +{ + const auto it = std::lower_bound(dataSet->nodes.begin(), dataSet->nodes.end(), id); + if (it != dataSet->nodes.end() && (*it).id == id) { + return &(*it); + } + return nullptr; +} diff --git a/src/map/loader/marblegeometryassembler.h b/src/map/loader/marblegeometryassembler.h new file mode 100644 index 0000000..aa1591f --- /dev/null +++ b/src/map/loader/marblegeometryassembler.h @@ -0,0 +1,54 @@ +/* + Copyright (C) 2020 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef KOSMINDOORMAP_MARBLEGEOMETRYASSEMBLER_H +#define KOSMINDOORMAP_MARBLEGEOMETRYASSEMBLER_H + +#include +#include + +#include + +namespace KOSMIndoorMap { + +/** Re-assemble broken up geometry in Marble vector tiles. */ +class MarbleGeometryAssembler +{ +public: + void merge(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer); + +private: + void mergeNodes(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer); + void mergeWays(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer); + void mergeRelations(OSM::DataSet *dataSet, OSM::DataSetMergeBuffer *mergeBuffer); + + void mergeWay(const OSM::DataSet *dataSet, OSM::Way &way, const OSM::Way &otherWay) const; + void mergeLine(const OSM::DataSet *dataSet, OSM::Way &way, const OSM::Way &otherWay) const; + void mergeArea(const OSM::DataSet *dataSet, OSM::Way &way, const OSM::Way &otherWay) const; + + template + OSM::Id takeMxOid(Elem &elem) const; + + const OSM::Node* nodeForId(const OSM::DataSet *dataSet, OSM::Id id) const; + + OSM::TagKey m_mxoidKey; + std::unordered_map m_wayIdMap; +}; + +} + +#endif // KOSMINDOORMAP_MARBLEGEOMETRYASSEMBLER_H