Commit 4b5489d0 authored by Volker Krause's avatar Volker Krause
Browse files

Initial experiments to improve airport coordinates based on OSM data

So far the coordinates we use for navigating to/from airports come from
Wikidata, and are typically somewhere on the center of the whole airport
area, typically a runway. That's far from ideal for navigation, as that's
neither a location you want to or event can get to, and in extreme cases
this even leads to navigation "snapping" to the opposite side of the
airport entirely.

Instead we want a coordinate around somewhere around the entrance. So far
there's three types of information from OSM we consider here:
* terminal buildings (or rather the center point of their bounding box)
* entrance nodes on terminal building polygons (unfortunately not reliably
available in the input data).
* railway stations on the premise of the airport.

This improves the result for many smaller or mid-sized airports
considerably already. What this cannot improve is the situation
at large airports with widely spread terminals (LHR, CDG, MXP, etc).
Those however cannot meaningfully represented by a single coordinate
anyway.

Code isn't hooked up yet, this is just for local experiments at this
point.
parent 1595b341
......@@ -51,6 +51,8 @@ find_package(PhoneNumber OPTIONAL_COMPONENTS PhoneNumber QUIET)
set_package_properties("PhoneNumber" PROPERTIES PURPOSE "Parsing and geo-coding of phone numbers.")
find_package(OpenSSL 1.1)
set_package_properties("OpenSSL" PROPERTIES TYPE OPTIONAL PURPOSE "VDV ticket decoding." URL "https://openssl.org")
find_package(OsmTools)
set_package_properties(OsmTools PROPERTIES TYPE OPTIONAL PURPOSE "Needed only for regenereating the airport database (ie. you most likely don't need this)")
if (NOT ANDROID)
set_package_properties(KF5CalendarCore PROPERTIES TYPE REQUIRED)
......
# Copyright (c) 2018 Volker Krause <vkrause@kde.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
find_program(OSMCONVERT_EXECUTABLE osmconvert)
find_program(OSMFILTER_EXECUTABLE osmfilter)
find_program(OSMUPDATE_EXECUTABLE osmupdate)
find_program(WGET_EXECUTABLE wget) # needed by osmupdate
find_program(RSYNC_EXECUTABLE rsync) # needed for the initial download
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(OsmTools
FOUND_VAR OsmTools_FOUND
REQUIRED_VARS OSMCONVERT_EXECUTABLE OSMFILTER_EXECUTABLE OSMUPDATE_EXECUTABLE WGET_EXECUTABLE RSYNC_EXECUTABLE
)
if (OSMCONVERT_EXECUTABLE AND NOT TARGET OSM::convert)
add_executable(OSM::convert IMPORTED)
set_target_properties(OSM::convert PROPERTIES IMPORTED_LOCATION ${OSMCONVERT_EXECUTABLE})
endif()
if (OSMFILTER_EXECUTABLE AND NOT TARGET OSM::filter)
add_executable(OSM::filter IMPORTED)
set_target_properties(OSM::filter PROPERTIES IMPORTED_LOCATION ${OSMFILTER_EXECUTABLE})
endif()
if (OSMUPDATE_EXECUTABLE AND NOT TARGET OSM::update)
add_executable(OSM::update IMPORTED)
set_target_properties(OSM::update PROPERTIES IMPORTED_LOCATION ${OSMUPDATE_EXECUTABLE})
endif()
set_package_properties(OsmTools PROPERTIES
URL "https://gitlab.com/osm-c-tools/osmctools"
DESCRIPTION "Tols to convert, filter and update OpenStreetMap data files"
)
mark_as_advanced(OSMCONVERT_EXECUTABLE OSMFILTER_EXECUTABLE OSMCONVERT_EXECUTABLE WGET_EXECUTABLE)
set(OSM_PLANET_DIR "" CACHE PATH "Directory containing the planet-latest.o5m file, and enough space to store processing results in.")
set(OSM_MIRROR "ftp5.gwdg.de/pub/misc/openstreetmap/planet.openstreetmap.org" CACHE STRING "Base URL of the preferred OSM download mirror.")
# create initial download and incremental update targets for the OSM planet file
if (OSM_PLANET_DIR)
set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM ON) # avoid cleaning the expensive full planet files
add_custom_command(
OUTPUT ${OSM_PLANET_DIR}/planet-latest.osm.pbf
COMMAND ${RSYNC_EXECUTABLE} -Lvz --partial --progress rsync://${OSM_MIRROR}/pbf/planet-latest.osm.pbf ${OSM_PLANET_DIR}/planet-latest.osm.pbf
WORKING_DIRECTORY ${OSM_PLANET_DIR}
COMMENT "Downloading full OSM plant file (~60GB)"
)
add_custom_command(
OUTPUT ${OSM_PLANET_DIR}/planet-latest.o5m
COMMAND OSM::convert ${OSM_PLANET_DIR}/planet-latest.osm.pbf --drop-author --drop-version --out-o5m -o=${OSM_PLANET_DIR}/planet-latest.o5m
WORKING_DIRECTORY ${OSM_PLANET_DIR}
COMMENT "Converting full OSM planet file to o5m format (takes ~30min and needs ~80GB of extra disk space)"
)
add_custom_target(osm-update-planet
COMMAND OSM::update --base-url=https://${OSM_MIRROR}/replication --day --verbose ${OSM_PLANET_DIR}/planet-latest.o5m ${OSM_PLANET_DIR}/new-planet-latest.o5m
COMMAND ${CMAKE_COMMAND} -E rename ${OSM_PLANET_DIR}/new-planet-latest.o5m ${OSM_PLANET_DIR}/planet-latest.o5m
WORKING_DIRECTORY ${OSM_PLANET_DIR}
DEPENDS ${OSM_PLANET_DIR}/planet-latest.o5m
COMMENT "Updating OSM planet file"
)
endif()
if (TARGET OSM::convert)
# Convert the given input file to the output file with determining the output format
# from the file extension
# Arguments:
# INPUT input file, assumed to be in OSM_PLANET_DIR
# OUTPUT output file, assumed to be in OSM_PLANET_DIR
# ADD_BBOX bool, enable injection of bounding box tags
function(osm_convert)
set(optionArgs ADD_BBOX)
set(oneValueArgs INPUT OUTPUT)
cmake_parse_arguments(osm_convert "${optionArgs}" "${oneValueArgs}" "" ${ARGN})
get_filename_component(format ${osm_convert_OUTPUT} LAST_EXT)
string(SUBSTRING ${format} 1 -1 format)
set(extra_args "")
if (osm_convert_ADD_BBOX)
set(extra_args "--add-bbox-tags")
endif()
add_custom_command(
OUTPUT ${OSM_PLANET_DIR}/${osm_convert_OUTPUT}
COMMAND OSM::convert ${OSM_PLANET_DIR}/${osm_convert_INPUT} --drop-author --drop-version ${extra_args} --out-${format} -o=${OSM_PLANET_DIR}/${osm_convert_OUTPUT}
WORKING_DIRECTORY ${OSM_PLANET_DIR}
COMMENT "Converting ${osm_convert_INPUT} to ${format} format"
DEPENDS ${OSM_PLANET_DIR}/${osm_convert_INPUT}
)
endfunction()
endif()
if (TARGET OSM::filter)
# Filter the given input file by the given filter arguments
# Arguments:
# INPUT input file, assumed to be in OSM_PLANET_DIR (default is planet-latest.o5m)
# OUTPUT output file, assumed to be in OSM_PLANET_DIR
# FILTER filter arguments for osmfilter
function(osm_filter)
set(oneValueArgs INPUT OUTPUT)
set(multiValueArgs FILTER)
cmake_parse_arguments(osm_filter "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
get_filename_component(format ${osm_filter_OUTPUT} LAST_EXT)
string(SUBSTRING ${format} 1 -1 format)
if (NOT osm_filter_INPUT)
set(osm_filter_INPUT planet-latest.o5m)
endif()
add_custom_command(
OUTPUT ${OSM_PLANET_DIR}/${osm_filter_OUTPUT}
COMMAND OSM::filter ${OSM_PLANET_DIR}/${osm_filter_INPUT} --drop-author --drop-version ${osm_filter_FILTER} --out-${format} -o=${OSM_PLANET_DIR}/${osm_filter_OUTPUT}
WORKING_DIRECTORY ${OSM_PLANET_DIR}
COMMENT "Filtering ${osm_filter_INPUT}"
DEPENDS ${OSM_PLANET_DIR}/${osm_filter_INPUT}
)
endfunction()
endif()
add_subdirectory(cli)
if (TARGET Qt5::Network)
if (TARGET Qt5::Network AND NOT CMAKE_CROSSCOMPILING AND OSM_PLANET_DIR AND OsmTools_FOUND)
add_subdirectory(knowledgedb-generator)
endif()
add_subdirectory(vdv/certs)
......
......@@ -31,6 +31,15 @@ add_custom_command(
${CMAKE_CURRENT_SOURCE_DIR}/timezones.qgs
)
# extract all elements we are interested in (airports, terminals, stations at airports)
osm_filter(OUTPUT airports.o5m FILTER --keep=\"iata=* or aeroway=terminal or public_transport=station or railway=station\")
# augment bounding box information
osm_convert(INPUT airports.o5m OUTPUT airports-bbox.o5m ADD_BBOX ON)
# convert the output to OSM XML format, so the code generator can consume it
osm_convert(INPUT airports-bbox.o5m OUTPUT airports-bbox.osm)
set(outfiles "")
macro(generate_db dbtype outfile)
add_custom_command(
......
/*
Copyright (C) 2020 Volker Krause <vkrause@kde.org>
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 <https://www.gnu.org/licenses/>.
*/
#include "osmairportdb.h"
#include <osm/xmlparser.h>
#include <QDebug>
#include <QFile>
template <typename T>
static bool isActiveAirport(const T &elem)
{
// filter out airports we aren't interested in
// not strictly needed here, but it reduces the diagnostic noise
const auto disused = OSM::tagValue(elem, QLatin1String("disused"));
const auto militayLanduse = OSM::tagValue(elem, QLatin1String("landuse")) == QLatin1String("military");
if (!disused.isEmpty() || militayLanduse) {
return false;
}
const auto aeroway = OSM::tagValue(elem, QLatin1String("aeroway"));
return aeroway == QLatin1String("aerodrome");
}
template <typename T>
static bool isTerminal(const T &elem)
{
const auto aeroway = OSM::tagValue(elem, QLatin1String("aeroway"));
return aeroway == QLatin1String("terminal");
}
void OSMAirportDb::load(const QString &path)
{
QFile f(path);
if (!f.open(QFile::ReadOnly)) {
qCritical() << "Failed to open OSM input file!" << f.errorString() << f.fileName();
return;
}
OSM::XmlParser p(&m_dataset);
p.parse(&f);
qDebug() << "nodes:" << m_dataset.nodes.size();
qDebug() << "ways:" << m_dataset.ways.size();
qDebug() << "relations:" << m_dataset.relations.size();
// find all airports
// those can be present in multiple forms
// as a single node: we don't care, this doesn't improve coordinate information for our use-case
// as a single way for the outer shape
// as a relation representing a multi-polygon outer shape
for (const auto &rel : m_dataset.relations) {
loadAirport(rel);
}
for (const auto &way : m_dataset.ways) {
if (isActiveAirport(way)) {
loadAirport(way);
}
}
// resolve multi-path polygons
for (auto &it : m_iataMap) {
resolveAirportPaths(it.second);
}
// find all terminal buildings, and add them to their airports
for (const auto &rel : m_dataset.relations) {
loadTerminal(rel);
}
for (const auto &way : m_dataset.ways) {
if (isTerminal(way)) {
loadTerminal(way);
}
}
// load railway stations
for (const auto &node : m_dataset.nodes) {
loadStation(node);
}
qDebug() << "airports:" << m_iataMap.size();
qDebug() << " with a single terminal:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminalBboxes.size() == 1; } );
qDebug() << " with multiple terminals:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminalBboxes.size() > 1; } );
qDebug() << " with a single entrance:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminalEntrances.size() == 1; } );
qDebug() << " with multiple entrances:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminalEntrances.size() > 1; } );
qDebug() << " with a single station:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.stations.size() == 1; } );
qDebug() << " with multiple stations:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.stations.size() > 1; } );
qDebug() << " with at least one singular feature:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) {
return a.second.stations.size() == 1 || a.second.terminalBboxes.size() == 1 || a.second.terminalEntrances.size() == 1;
});
qDebug() << " with conflicting features:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) {
return a.second.stations.size() != 1 && a.second.terminalBboxes.size() != 1 && a.second.terminalEntrances.size() != 1 &&
!(a.second.stations.empty() && a.second.terminalBboxes.empty() && a.second.terminalEntrances.empty());
});
}
template<typename T> void OSMAirportDb::loadAirport(const T &elem)
{
const auto iata = OSM::tagValue(elem, QLatin1String("iata"));
if (iata.isEmpty()) {
return;
}
// semicolon list split
if (iata.contains(QLatin1Char(';'))) {
const auto iatas = iata.split(QLatin1Char(';'), Qt::SkipEmptyParts);
for (const auto &iata : iatas) {
auto e = elem;
OSM::setTagValue(e, QStringLiteral("iata"), iata);
loadAirport(e);
}
return;
}
if (iata.size() != 3 || !std::all_of(iata.begin(), iata.end(), [](const auto c) { return c.isUpper(); })) {
qWarning() << "IATA code format violation:" << iata << elem.url();
return;
}
loadAirport(elem, iata);
}
void OSMAirportDb::loadAirport(const OSM::Relation &elem, const QString &iataCode)
{
if (!isActiveAirport(elem)) {
return;
}
const auto it = m_iataMap.find(iataCode);
if (it != m_iataMap.end()) {
qWarning() << "Duplicate relation for IATA code:" << iataCode << (*it).second.source << elem.url();
return;
}
OSMAirportData airport;
airport.source = elem.url();
airport.bbox = elem.bbox;
m_iataMap[iataCode] = std::move(airport);
// we assume type == multipolygon here
for (const auto &member : elem.members) {
if (member.role != QLatin1String("outer")) {
continue;
}
const auto it = std::lower_bound(m_dataset.ways.begin(), m_dataset.ways.end(), member.id);
if (it != m_dataset.ways.end() && (*it).id == member.id) {
loadAirport(*it, iataCode);
}
}
}
void OSMAirportDb::loadAirport(const OSM::Way &elem, const QString &iataCode)
{
if (elem.nodes.empty()) {
qWarning() << "Empty way element!" << elem.url();
return;
}
const auto it = m_iataMap.find(iataCode);
if (it != m_iataMap.end()) {
// check if this overlaps, then it's just multiple parts of the same airport, otherwise this is a suspected IATA code duplication
if ((*it).second.bbox.isValid() && !OSM::intersects((*it).second.bbox, elem.bbox)) {
// TODO we probably want to exclude the entire code as invalid then
qWarning() << "duplicate IATA code?" << (*it).first << elem.url() << (*it).second.source;
} else {
//qDebug() << "merging airport parts:" << iataCode << (*it).second.source << elem.url();
(*it).second.bbox = OSM::unite((*it).second.bbox, elem.bbox);
if (elem.isClosed()) {
QVector<QPointF> points;
points.reserve(elem.nodes.size());
appendPointsFromWay(points, elem.nodes.begin(), elem.nodes.end());
(*it).second.airportPolygon = (*it).second.airportPolygon.united(QPolygonF(points));
} else {
(*it).second.airportPaths.push_back(&elem);
}
}
return;
}
//qDebug() << iata << elem.bbox << elem.id;
OSMAirportData airport;
airport.source = elem.url();
airport.bbox = elem.bbox;
if (elem.isClosed()) {
QVector<QPointF> points;
points.reserve(elem.nodes.size());
appendPointsFromWay(points, elem.nodes.begin(), elem.nodes.end());
airport.airportPolygon = QPolygonF(points);
} else {
airport.airportPaths.push_back(&elem);
}
m_iataMap[iataCode] = std::move(airport);
}
void OSMAirportDb::loadTerminal(const OSM::Relation& elem)
{
if (!isTerminal(elem)) {
return;
}
// we assume type == multipolygon here
for (const auto &member : elem.members) {
if (member.role != QLatin1String("outer")) {
continue;
}
const auto it = std::lower_bound(m_dataset.ways.begin(), m_dataset.ways.end(), member.id);
if (it != m_dataset.ways.end() && (*it).id == member.id) {
loadTerminal(*it);
}
}
}
void OSMAirportDb::loadTerminal(const OSM::Way &elem)
{
// find matching airport
for (auto it = m_iataMap.begin(); it != m_iataMap.end(); ++it) {
if (!OSM::intersects((*it).second.bbox, elem.bbox)) {
continue;
}
//qDebug() << "found terminal for airport:" << elem.url() << (*it).first << (*it).second.source;
(*it).second.terminalBboxes.push_back(elem.bbox);
// look for entrances to terminals
for (const auto &nodeId : elem.nodes) {
const auto nodeIt = std::lower_bound(m_dataset.nodes.begin(), m_dataset.nodes.end(), nodeId);
if (nodeIt == m_dataset.nodes.end() || (*nodeIt).id != nodeId) {
continue;
}
// filter out inaccessible entrances, or gates
const auto access = OSM::tagValue(*nodeIt, QLatin1String("access"));
const auto aeroway = OSM::tagValue(*nodeIt, QLatin1String("gate"));
if (access == QLatin1String("private") || access == QLatin1String("no") || aeroway == QLatin1String("gate")) {
continue;
}
const auto entrance = OSM::tagValue(*nodeIt, QLatin1String("entrance"));
if (entrance == QLatin1String("yes") || entrance == QLatin1String("main")) {
//qDebug() << " found entrance for terminal:" << (*nodeIt).url() << entrance << access;
(*it).second.terminalEntrances.push_back((*nodeIt).coordinate);
}
}
}
}
void OSMAirportDb::loadStation(const OSM::Node &elem)
{
const auto railway = OSM::tagValue(elem, QLatin1String("railway"));
if (railway != QLatin1String("station")) {
return;
}
// try to filter out airport-interal transport systems, those are typically airside and thus not what we want
const auto station = OSM::tagValue(elem, QLatin1String("station"));
if (station == QLatin1String("monorail")) {
return;
}
for (auto it = m_iataMap.begin(); it != m_iataMap.end(); ++it) {
// we need the exact path here, the bounding box can contain a lot more stuff
// the bounding box check is just for speed
if (!OSM::contains((*it).second.bbox, elem.coordinate)
|| !(*it).second.airportPolygon.containsPoint(QPointF(elem.coordinate.latF(), elem.coordinate.lonF()), Qt::WindingFill))
{
continue;
}
//qDebug() << "found station for airport:" << elem.url() << (*it).first << (*it).second.source;
(*it).second.stations.push_back(elem.coordinate);
}
}
OSM::Coordinate OSMAirportDb::lookup(const QString &iata, float lat, float lon)
{
const auto it = m_iataMap.find(iata);
if (it == m_iataMap.end()) {
//qDebug() << "No airport with IATA code:" << iata;
return {};
}
const OSM::Coordinate wdCoord(lat, lon);
const auto &airport = (*it).second;
if (!OSM::contains(airport.bbox, wdCoord)) {
qDebug() << "Airport" << iata << "is not where we expect it to be!?" << airport.source << airport.bbox << lat << lon;
return {};
}
// single entrance
qDebug() << "Optimizing" << iata << airport.source << lat << lon << airport.bbox;
qDebug() << " entrances:" << airport.terminalEntrances.size() << "terminals:" << airport.terminalBboxes.size() << "stations:" << airport.stations.size();
if (airport.terminalEntrances.size() == 1) { // ### this works for small airports, but for larger ones this is often due to missing data
qDebug() << " by entrance:" << airport.terminalEntrances[0];
return airport.terminalEntrances[0];
}
// single terminal
if (airport.terminalBboxes.size() == 1) {
qDebug() << " by terminal:" << airport.terminalBboxes[0].center();
return airport.terminalBboxes[0].center();
}
// single on-premise station
if (airport.stations.size() == 1) {
qDebug() << " by station:" << airport.stations[0];
return airport.stations[0];
}
// multiple terminals: take the center of the sum of all bounding boxes, and TODO check the result isn't ridiculously large
if (airport.terminalBboxes.size() > 1) {
const auto terminalBbox = std::accumulate(airport.terminalBboxes.begin(), airport.terminalBboxes.end(), OSM::BoundingBox(), OSM::unite);
// if the original coordinate is outside the terminal bounding box, this is highly likely an improvement,
// otherwise we cannot be sure (see MUC, where the Wikidata coordinate is ideal).
if (!OSM::contains(terminalBbox, wdCoord)) {
qDebug() << " by terminal bbox center:" << terminalBbox.center();
return terminalBbox.center();
}
}
return {};
}
template <typename Iter>
void OSMAirportDb::appendPointsFromWay(QVector<QPointF>& points, const Iter& nodeBegin, const Iter &nodeEnd) const
{
points.reserve(points.size() + std::distance(nodeBegin, nodeEnd));
for (auto it = nodeBegin; it != nodeEnd; ++it) {
const auto nodeIt = std::lower_bound(m_dataset.nodes.begin(), m_dataset.nodes.end(), (*it));
if (nodeIt == m_dataset.nodes.end() || (*nodeIt).id != (*it)) {
continue;
}
points.push_back(QPointF((*nodeIt).coordinate.latF(), (*nodeIt).coordinate.lonF()));
}
}
OSM::Id OSMAirportDb::appendNextPath(QVector<QPointF> &points, OSM::Id startNode, OSMAirportData &airport) const
{
if (airport.airportPaths.empty()) {
return {};
}
for (auto it = airport.airportPaths.begin() + 1; it != airport.airportPaths.end(); ++it) {
assert(!(*it)->nodes.empty()); // ensured above
//qDebug() << " looking at:" << (*it)->nodes.front() << (*it)->url();
if ((*it)->nodes.front() == startNode) {
appendPointsFromWay(points, (*it)->nodes.begin(), (*it)->nodes.end());
const auto lastNodeId = (*it)->nodes.back();
airport.airportPaths.erase(it);
return lastNodeId;
}
// path segments can also be backwards
if ((*it)->nodes.back() == startNode) {
appendPointsFromWay(points, (*it)->nodes.rbegin(), (*it)->nodes.rend());
const auto lastNodeId = (*it)->nodes.front();
airport.airportPaths.erase(it);
return lastNodeId;
}
}
return {};
}
void OSMAirportDb::resolveAirportPaths(OSMAirportData &airport) const
{
//qDebug() << "resolving polygon for" << airport.source << airport.bbox << airport.airportPaths.size();
for (auto it = airport.airportPaths.begin(); it != airport.airportPaths.end();) {
assert(!(*it)->nodes.empty()); // ensured above
if (airport.airportPaths.size() == 1) {
qWarning() << " open airport polgyon:" << airport.source << (*it)->url();
return;
}
QVector<QPointF> points;
appendPointsFromWay(points, (*it)->nodes.begin(), (*it)->nodes.end());
const auto startNode = (*it)->nodes.front();
auto lastNode = (*it)->nodes.back();
//qDebug() << " starting:" << startNode << lastNode << (*it)->url();
do {
lastNode = appendNextPath(points, lastNode, airport);
//qDebug() << " next:" << lastNode;
} while (lastNode && lastNode != startNode);
if (lastNode != startNode) {
qWarning() << " open airport polygon:" << airport.source << (*it)->url();
} else {
airport.airportPolygon = airport.airportPolygon.united(QPolygonF(points));
}
it = airport.airportPaths.erase(it);
}
//qDebug() << " polygon:" << airport.airportPolygon.size() << airport.airportPolygon.boundingRect();