osmairportdb.cpp 15.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
    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"

20
#include <osm/geomath.h>
21 22 23 24 25
#include <osm/xmlparser.h>

#include <QDebug>
#include <QFile>

26 27
enum {
    StationClusterDistance = 100, // in meter
28
    StationToTerminalDistance = 75, // in meter
29 30
};

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
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
50
    OSM::for_each(m_dataset, [this](auto elem) { loadAirport(elem); }, OSM::IncludeRelations | OSM::IncludeWays);
51 52

    // find all terminal buildings, and add them to their airports
53
    OSM::for_each(m_dataset, [this](auto elem) { loadTerminal(elem); });
54 55

    // load railway stations
56
    OSM::for_each(m_dataset, [this](auto elem) { loadStation(elem); });
57 58 59
    for (auto &a : m_iataMap) {
        filterStations(a.second);
    }
60 61

    qDebug() << "airports:" << m_iataMap.size();
62 63
    qDebug() << "  with a single terminal:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminals.size() == 1; } );
    qDebug() << "  with multiple terminals:" << std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) { return a.second.terminals.size() > 1; } );
64 65 66 67 68
    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) {
69
        return a.second.stations.size() == 1 || a.second.terminals.size() == 1 || a.second.terminalEntrances.size() == 1;
70 71
    });
    qDebug() << "  with conflicting features:" <<  std::count_if(m_iataMap.begin(), m_iataMap.end(), [](const auto &a) {
72 73
        return a.second.stations.size() != 1 && a.second.terminals.size() != 1 && a.second.terminalEntrances.size() != 1 &&
            !(a.second.stations.empty() && a.second.terminals.empty() && a.second.terminalEntrances.empty());
74 75 76
    });
}

77
void OSMAirportDb::loadAirport(OSM::Element elem)
78
{
79 80 81 82 83 84 85 86 87 88 89 90 91 92
    const auto aeroway = elem.tagValue(QLatin1String("aeroway"));
    if (aeroway != QLatin1String("aerodrome")) {
        return;
    }

    // filter out airports we aren't interested in
    // not strictly needed here, but it reduces the diagnostic noise
    const auto disused = elem.tagValue(QLatin1String("disused"));
    const auto militayLanduse = elem.tagValue(QLatin1String("landuse")) == QLatin1String("military");
    if (!disused.isEmpty() || militayLanduse) {
        return;
    }

    const auto iata = elem.tagValue(QLatin1String("iata"));
93 94 95 96
    if (iata.isEmpty()) {
        return;
    }

97 98 99
    // osmconvert gives us wrong values e.g. for FRA, so we need to do this ourselves...
    elem.recomputeBoundingBox(m_dataset);

100 101 102 103
    // semicolon list split
    if (iata.contains(QLatin1Char(';'))) {
        const auto iatas = iata.split(QLatin1Char(';'), Qt::SkipEmptyParts);
        for (const auto &iata : iatas) {
104
            loadAirport(elem, iata);
105
        }
106 107
    } else {
        loadAirport(elem, iata);
108 109 110
    }
}

111
static QPolygonF polygonFromOuterPath(const std::vector<const OSM::Node*> &path)
112
{
113 114
    if (path.empty()) {
        return {};
115 116
    }

117 118 119 120 121 122 123 124 125 126 127 128 129 130
    QPolygonF subPoly, result;
    subPoly.push_back(QPointF(path[0]->coordinate.latF(), path[0]->coordinate.lonF()));
    OSM::Id firstNode = path[0]->id;
    for (auto it = std::next(path.begin()); it != path.end(); ++it) {
        if (firstNode == 0) { // starting a new loop
            firstNode = (*it)->id;
            subPoly.push_back(QPointF((*it)->coordinate.latF(), (*it)->coordinate.lonF()));
        } else if ((*it)->id == firstNode) { // just closed a loop, so this is not a line on the path
            subPoly.push_back(QPointF((*it)->coordinate.latF(), (*it)->coordinate.lonF()));
            firstNode = 0;
            result = result.united(subPoly);
            subPoly.clear();
        } else {
            subPoly.push_back(QPointF((*it)->coordinate.latF(), (*it)->coordinate.lonF()));
131 132
        }
    }
133 134 135 136
    if (!subPoly.empty()) {
        result = result.united(subPoly);
    }
    return result;
137 138
}

139
void OSMAirportDb::loadAirport(OSM::Element elem, const QString &iataCode)
140
{
141 142
    if (iataCode.size() != 3 || !std::all_of(iataCode.begin(), iataCode.end(), [](const auto c) { return c.isUpper(); })) {
        qWarning() << "IATA code format violation:" << iataCode << elem.url();
143 144 145 146
        return;
    }

    const auto it = m_iataMap.find(iataCode);
147 148
    if (it != m_iataMap.end() && !OSM::intersects((*it).second.bbox, elem.boundingBox())) {
        qWarning() << "Duplicate IATA code:" << iataCode << (*it).second.source << elem.url();
149 150 151
        return;
    }

152 153 154 155
    const auto poly = polygonFromOuterPath(elem.outerPath(m_dataset));
    if (it != m_iataMap.end()) {
        (*it).second.bbox = OSM::unite(elem.boundingBox(), (*it).second.bbox);
        (*it).second.airportPolygon = (*it).second.airportPolygon.united(poly);
156
    } else {
157 158 159 160 161
        OSMAirportData airport;
        airport.source = elem.url();
        airport.bbox = elem.boundingBox();
        airport.airportPolygon = poly;
        m_iataMap[iataCode] = std::move(airport);
162 163 164
    }
}

165
void OSMAirportDb::loadTerminal(OSM::Element elem)
166
{
167 168
    const auto aeroway = elem.tagValue(QLatin1String("aeroway"));
    if (aeroway != QLatin1String("terminal")) {
169 170 171
        return;
    }

172 173 174 175 176 177 178 179 180 181
    // filter out freight terminals
    const auto usage = elem.tagValue(QLatin1String("usage"));
    const auto traffic_mode = elem.tagValue(QLatin1String("traffic_mode"));
    const auto building = elem.tagValue(QLatin1String("building"));
    const auto industrial = elem.tagValue(QLatin1String("industrial"));
    if (usage == QLatin1String("freight")
        || traffic_mode == QLatin1String("freigt")
        || building == QLatin1String("industrial")
        || !industrial.isEmpty()) {
        return;
182 183 184 185
    }

    // find matching airport
    for (auto it = m_iataMap.begin(); it != m_iataMap.end(); ++it) {
186
        if (!OSM::intersects((*it).second.bbox, elem.boundingBox())) {
187 188
            continue;
        }
189 190 191
        // check against the exact airport boundary, not just the bounding box,
        // this excludes terminal buildings from adjacent sites we don't care about
        // example: the Airbus delivery buildings next to TLS
192
        if (!(*it).second.airportPolygon.intersects(QRectF(QPointF(elem.boundingBox().min.latF(), elem.boundingBox().min.lonF()), QPointF(elem.boundingBox().max.latF(), elem.boundingBox().max.lonF())))) {
193 194
            continue;
        }
195
        //qDebug() << "found terminal for airport:" << elem.url() << (*it).first << (*it).second.source;
196
        (*it).second.terminals.push_back(elem);
197 198

        // look for entrances to terminals
199
        for (auto node : elem.outerPath(m_dataset)) {
200 201

            // filter out inaccessible entrances, or gates
202 203
            const auto access = OSM::tagValue(*node, QLatin1String("access"));
            const auto aeroway = OSM::tagValue(*node, QLatin1String("gate"));
204 205 206 207
            if (access == QLatin1String("private") || access == QLatin1String("no") || aeroway == QLatin1String("gate")) {
                continue;
            }

208
            const auto entrance = OSM::tagValue(*node, QLatin1String("entrance"));
209 210
            if (entrance == QLatin1String("yes") || entrance == QLatin1String("main")) {
                //qDebug() << "  found entrance for terminal:" << (*nodeIt).url() << entrance << access;
211
                (*it).second.terminalEntrances.push_back(node->coordinate);
212 213 214 215 216
            }
        }
    }
}

217
void OSMAirportDb::loadStation(OSM::Element elem)
218
{
219
    const auto railway = elem.tagValue(QLatin1String("railway"));
220
    if (railway != QLatin1String("station") && railway != QLatin1String("halt") && railway != QLatin1String("tram_stop")) {
221 222 223 224
        return;
    }

    // try to filter out airport-interal transport systems, those are typically airside and thus not what we want
225
    const auto station = elem.tagValue(QLatin1String("station"));
226 227 228 229 230
    if (station == QLatin1String("monorail")) {
        return;
    }

    for (auto it = m_iataMap.begin(); it != m_iataMap.end(); ++it) {
231 232
        const auto &airport = (*it).second;

233 234
        // we need the exact path here, the bounding box can contain a lot more stuff
        // the bounding box check is just for speed
235
        if (!OSM::contains(airport.bbox, elem.center())) {
236 237
            continue;
        }
238

239
        const auto onPremises = airport.airportPolygon.containsPoint(QPointF(elem.center().latF(), elem.center().lonF()), Qt::WindingFill);
240 241
        // one would assume that terminals are always within the airport bounds, but that's not the case
        // they sometimes expand beyond them. A station inside a terminal is however most likely something relevant for us
242 243
        const auto inTerminal = std::any_of(airport.terminals.begin(), airport.terminals.end(), [&elem](const auto &terminal) {
            return OSM::contains(terminal.boundingBox(), elem.center());
244 245
        });

246 247 248 249 250 251
        // distance of the station to the terminal outer polygon
        uint32_t distanceToTerminal = std::numeric_limits<uint32_t>::max();
        for (auto terminal : airport.terminals) {
            const auto outerPath = terminal.outerPath(m_dataset);
            distanceToTerminal = std::min(distanceToTerminal, OSM::distance(outerPath, elem.center()));
        }
252

253 254
        if (onPremises || inTerminal || distanceToTerminal < StationToTerminalDistance) {
            qDebug() << "found station for airport:" << elem.url() << (*it).first << (*it).second.source << onPremises << inTerminal << distanceToTerminal;
255
            (*it).second.stations.push_back(elem);
256
        }
257 258 259
    }
}

260 261 262 263 264
void OSMAirportDb::filterStations(OSMAirportData &airport)
{
    // if we have a full station, drop halts
    // TODO similar filters are probably needed for various tram/subway variants for on-premises transport lines
    auto it = std::partition(airport.stations.begin(), airport.stations.end(), [](auto station) {
265
        return station.tagValue(QLatin1String("railway")) == QLatin1String("station");
266 267 268 269 270 271 272
    });
    if (it != airport.stations.begin() && it != airport.stations.end()) {
        airport.stations.erase(it, airport.stations.end());
    }

    // "creative" way of separating "real" and on-premises stations: only real ones tend to have Wikidata tags
    it = std::partition(airport.stations.begin(), airport.stations.end(), [](auto station) {
273
        return !station.tagValue(QLatin1String("wikidata")).isEmpty();
274 275 276 277
    });
    if (it != airport.stations.begin() && it != airport.stations.end()) {
        airport.stations.erase(it, airport.stations.end());
    }
278 279 280 281 282 283 284 285 286 287

    // prioritize by number of platforms, if we have that information for all stations
    if (airport.stations.size() > 1 && std::all_of(airport.stations.begin(), airport.stations.end(), [](auto s) { return !s.tagValue(QLatin1String("platforms")).isEmpty(); })) {
        std::sort(airport.stations.begin(), airport.stations.end(), [](auto lhs, auto rhs) {
            return lhs.tagValue(QLatin1String("platforms")).toInt() > rhs.tagValue(QLatin1String("platforms")).toInt();
        });
        if (airport.stations[0].tagValue(QLatin1String("platforms")) != airport.stations[1].tagValue(QLatin1String("platforms"))) {
            airport.stations.erase(std::next(airport.stations.begin()), airport.stations.end());
        }
    }
288 289
}

290 291 292 293 294 295 296 297 298 299 300 301 302 303
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 {};
    }
304 305 306 307
    if (airport.terminals.empty() && airport.terminalEntrances.empty() && airport.stations.empty()) {
        // no details available for this airport
        return {};
    }
308 309

    qDebug() << "Optimizing" << iata << airport.source << lat << lon << airport.bbox;
310
    qDebug() << "  entrances:" << airport.terminalEntrances.size() << "terminals:" << airport.terminals.size() << "stations:" << airport.stations.size();
311

312
    // single station
313
    if (airport.stations.size() == 1) {
314 315 316 317 318 319 320 321 322 323 324 325 326
        qDebug() << "  by station:" << airport.stations[0].url();
        return airport.stations[0].center();
    }

    // multiple stations, but close together
    if (airport.stations.size() > 1) {
        auto stationBbox = std::accumulate(airport.stations.begin(), airport.stations.end(), OSM::BoundingBox(), [](OSM::BoundingBox lhs, OSM::Element rhs) {
            return OSM::unite(lhs, OSM::BoundingBox(rhs.boundingBox().center(), rhs.boundingBox().center()));
        });
        if (OSM::distance(stationBbox.min, stationBbox.max) < StationClusterDistance) {
            qDebug() << "  by clustered station:" << stationBbox;
            return stationBbox.center();
        }
327 328
    }

Volker Krause's avatar
Volker Krause committed
329 330 331 332 333 334 335
    // single entrance
    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
336 337 338
    if (airport.terminals.size() == 1) {
        qDebug() << "  by terminal:" << airport.terminals[0].url() << airport.terminals[0].center();
        return airport.terminals[0].center();
Volker Krause's avatar
Volker Krause committed
339 340
    }

341
    // multiple terminals: take the center of the sum of all bounding boxes, and TODO check the result isn't ridiculously large
342 343 344 345
    if (airport.terminals.size() > 1) {
        const auto terminalBbox = std::accumulate(airport.terminals.begin(), airport.terminals.end(), OSM::BoundingBox(), [](const auto &bbox, auto terminal) {
            return OSM::unite(bbox, terminal.boundingBox());
        });
346 347
        // 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).
348
        //qDebug() << "    considering terminal bbox:" << terminalBbox;
349 350 351 352 353 354 355 356
        if (!OSM::contains(terminalBbox, wdCoord)) {
            qDebug() << "  by terminal bbox center:" << terminalBbox.center();
            return terminalBbox.center();
        }
    }

    return {};
}