blob: 9f21e2c6f17056f0551a05866633607b023a8e61 [file] [log] [blame] [edit]
/*
* 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;
}