214 lines
7.1 KiB
C++
214 lines
7.1 KiB
C++
/*
|
|
* Copyright (C) 2015-2022 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include "KmlParser.h"
|
|
#include <libxml/parser.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <string>
|
|
#include <utility>
|
|
#include "StringParse.h"
|
|
using std::string;
|
|
|
|
// Coordinates can be nested arbitrarily deep within a Placemark, depending
|
|
// on the type of object (Point, LineString, Polygon) the Placemark contains
|
|
static xmlNode* findCoordinates(xmlNode* current) {
|
|
for (; current != nullptr; current = current->next) {
|
|
if (!strcmp((const char*)current->name, "coordinates")) {
|
|
return current;
|
|
}
|
|
xmlNode* children = findCoordinates(current->xmlChildrenNode);
|
|
if (children != nullptr) {
|
|
return children;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Coordinates have the following format:
|
|
// <coordinates> -112.265654928602,36.09447672602546,2357
|
|
// ...
|
|
// -112.2657374587321,36.08646312301303,2357
|
|
// </coordinates>
|
|
// often entirely contained in a single string, necessitating regex
|
|
static bool parseCoordinates(xmlNode* current, GpsFixArray* fixes) {
|
|
xmlNode* coordinates_node = findCoordinates(current);
|
|
bool result = true;
|
|
if (coordinates_node == nullptr ||
|
|
coordinates_node->xmlChildrenNode == nullptr ||
|
|
coordinates_node->xmlChildrenNode->content == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const char* coordinates =
|
|
(const char*)(coordinates_node->xmlChildrenNode->content);
|
|
int coordinates_len = strlen(coordinates);
|
|
int offset = 0, n = 0;
|
|
GpsFix new_fix;
|
|
while (3 == SscanfWithCLocale(coordinates + offset, "%f , %f , %f%n",
|
|
&new_fix.longitude, &new_fix.latitude,
|
|
&new_fix.elevation, &n)) {
|
|
fixes->push_back(new_fix);
|
|
offset += n;
|
|
}
|
|
|
|
// Only allow whitespace at the end of the string to remain unconsumed.
|
|
for (int i = offset; i < coordinates_len && result; ++i) {
|
|
result = isspace(coordinates[i]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static bool parseGxTrack(xmlNode* children, GpsFixArray* fixes) {
|
|
bool result = true;
|
|
for (xmlNode* current = children; result && current != nullptr;
|
|
current = current->next) {
|
|
if (current->ns && current->ns->prefix &&
|
|
!strcmp((const char*)current->ns->prefix, "gx") &&
|
|
!strcmp((const char*)current->name, "coord")) {
|
|
std::string coordinates{(const char*)current->xmlChildrenNode->content};
|
|
GpsFix new_fix;
|
|
result = (3 == SscanfWithCLocale(coordinates.c_str(), "%f %f %f",
|
|
&new_fix.longitude, &new_fix.latitude,
|
|
&new_fix.elevation));
|
|
fixes->push_back(new_fix);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool parsePlacemark(xmlNode* current, GpsFixArray* fixes) {
|
|
string description;
|
|
string name;
|
|
size_t ind = string::npos;
|
|
// not worried about case-sensitivity since .kml files
|
|
// are expected to be machine-generated
|
|
for (; current != nullptr; current = current->next) {
|
|
const bool hasContent =
|
|
current->xmlChildrenNode && current->xmlChildrenNode->content;
|
|
|
|
if (hasContent && !strcmp((const char*)current->name, "description")) {
|
|
description = (const char*)current->xmlChildrenNode->content;
|
|
} else if (hasContent && !strcmp((const char*)current->name, "name")) {
|
|
name = (const char*)current->xmlChildrenNode->content;
|
|
} else if (!strcmp((const char*)current->name, "Point") ||
|
|
!strcmp((const char*)current->name, "LineString") ||
|
|
!strcmp((const char*)current->name, "Polygon")) {
|
|
ind = (ind != string::npos ? ind : fixes->size());
|
|
if (!parseCoordinates(current->xmlChildrenNode, fixes)) {
|
|
return false;
|
|
}
|
|
} else if (current->ns && current->ns->prefix &&
|
|
!strcmp((const char*)current->ns->prefix, "gx") &&
|
|
!strcmp((const char*)current->name, "Track")) {
|
|
ind = (ind != string::npos ? ind : fixes->size());
|
|
if (!parseGxTrack(current->xmlChildrenNode, fixes)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ind == string::npos || ind >= fixes->size()) {
|
|
return false;
|
|
}
|
|
|
|
// only assign name and description to the first of the
|
|
// points to avoid needless repetition
|
|
(*fixes)[ind].description = std::move(description);
|
|
(*fixes)[ind].name = std::move(name);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Placemarks (aka locations) can be nested arbitrarily deep
|
|
static bool traverseSubtree(xmlNode* current, GpsFixArray* fixes,
|
|
string* error) {
|
|
for (; current; current = current->next) {
|
|
if (current->name != nullptr &&
|
|
!strcmp((const char*)current->name, "Placemark")) {
|
|
if (!parsePlacemark(current->xmlChildrenNode, fixes)) {
|
|
*error = "Location found with missing or malformed coordinates";
|
|
return false;
|
|
}
|
|
} else if (current->name != nullptr &&
|
|
strcmp((const char*)current->name, "text") != 0) {
|
|
// if it's not a Placemark we must go deeper
|
|
if (!traverseSubtree(current->xmlChildrenNode, fixes, error)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
error->clear();
|
|
return true;
|
|
}
|
|
|
|
bool KmlParser::parseFile(const char* filePath, GpsFixArray* fixes,
|
|
string* error) {
|
|
// This initializes the library and checks potential ABI mismatches between
|
|
// the version it was compiled for and the actual shared library used.
|
|
LIBXML_TEST_VERSION
|
|
|
|
xmlDocPtr doc = xmlReadFile(filePath, nullptr, 0);
|
|
if (doc == nullptr) {
|
|
*error = "KML document not parsed successfully.";
|
|
xmlFreeDoc(doc);
|
|
return false;
|
|
}
|
|
|
|
xmlNodePtr cur = xmlDocGetRootElement(doc);
|
|
if (cur == nullptr) {
|
|
*error = "Could not get root element of parsed KML file.";
|
|
xmlFreeDoc(doc);
|
|
xmlCleanupParser();
|
|
return false;
|
|
}
|
|
bool isWellFormed = traverseSubtree(cur, fixes, error);
|
|
|
|
xmlFreeDoc(doc);
|
|
xmlCleanupParser();
|
|
|
|
return isWellFormed;
|
|
}
|
|
|
|
bool KmlParser::parseString(const char* str, int len, GpsFixArray* fixes,
|
|
string* error) {
|
|
// This initializes the library and checks potential ABI mismatches between
|
|
// the version it was compiled for and the actual shared library used.
|
|
LIBXML_TEST_VERSION
|
|
|
|
xmlDocPtr doc = xmlReadMemory(str, len, NULL, NULL, 0);
|
|
if (doc == nullptr) {
|
|
*error = "KML document not parsed successfully.";
|
|
xmlFreeDoc(doc);
|
|
return false;
|
|
}
|
|
|
|
xmlNodePtr cur = xmlDocGetRootElement(doc);
|
|
if (cur == nullptr) {
|
|
*error = "Could not get root element of parsed KML file.";
|
|
xmlFreeDoc(doc);
|
|
xmlCleanupParser();
|
|
return false;
|
|
}
|
|
bool isWellFormed = traverseSubtree(cur, fixes, error);
|
|
|
|
xmlFreeDoc(doc);
|
|
xmlCleanupParser();
|
|
|
|
return isWellFormed;
|
|
}
|