/* kml.cc
* Export from Aven as KML.
*/
/* Copyright (C) 2012 Olaf Kähler
* Copyright (C) 2012-2024 Olly Betts
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU 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 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
#include "kml.h"
#include "export.h" // For LABELS, etc
#include
#include
#include
#include "useful.h"
#include
#include "aven.h"
#include "message.h"
using namespace std;
#define WGS84_DATUM_STRING "EPSG:4326"
static void
html_escape(FILE *fh, const char *s)
{
while (*s) {
switch (*s) {
case '<':
fputs("<", fh);
break;
case '>':
fputs(">", fh);
break;
case '&':
fputs("&", fh);
break;
default:
PUTC(*s, fh);
}
++s;
}
}
static void discarding_proj_logger(void *, int, const char *) { }
KML::KML(const char * input_datum, bool clamp_to_ground_)
: clamp_to_ground(clamp_to_ground_)
{
/* Prevent stderr spew from PROJ. */
proj_log_func(PJ_DEFAULT_CTX, nullptr, discarding_proj_logger);
pj = proj_create_crs_to_crs(PJ_DEFAULT_CTX,
input_datum, WGS84_DATUM_STRING,
NULL);
if (pj) {
// Normalise the output order so x is longitude and y latitude - by
// default new PROJ has them switched for EPSG:4326 which just seems
// confusing.
PJ* pj_norm = proj_normalize_for_visualization(PJ_DEFAULT_CTX, pj);
proj_destroy(pj);
pj = pj_norm;
}
if (!pj) {
wxString m = wmsg(/*Failed to initialise input coordinate system “%s”*/287);
m = wxString::Format(m.c_str(), input_datum);
throw m;
}
}
KML::~KML()
{
if (pj)
proj_destroy(pj);
}
const int *
KML::passes() const
{
static const int default_passes[] = {
PASG, XSECT, WALL1, WALL2, LEGS|SURF, LABELS|ENTS|FIXES|EXPORTS, 0
};
return default_passes;
}
/* Initialise KML routines. */
void KML::header(const char * title, time_t,
double, double, double, double, double, double)
{
fputs(
"\n"
"\n", fh);
fputs("", fh);
html_escape(fh, title);
fputs("\n", fh);
// Set up styles for the icons to reduce the file size.
// Note "color" code order is aabbggrr
fputs("\n", fh);
fputs("\n", fh);
fputs("\n", fh);
// Set up styles for surface legs and splays.
fputs("\n", fh);
fputs("\n", fh);
// FIXME: does KML allow bounds?
// NB Lat+long bounds are not necessarily the same as the bounds in survex
// coords translated to WGS84 lat+long...
}
void
KML::start_pass(int)
{
if (linestring_flags) {
fputs("\n", fh);
linestring_flags = 0;
}
}
void
KML::line(const img_point *p1, const img_point *p, unsigned flags, bool fPendingMove)
{
if (linestring_flags && linestring_flags != (flags & (LEGS|SURF|SPLAYS))) {
fputs("\n", fh);
linestring_flags = 0;
fPendingMove = true;
}
if (fPendingMove) {
if (linestring_flags == 0) {
linestring_flags = (flags & (LEGS|SURF|SPLAYS));
if (flags & SURF) {
fputs(""
"#surf"
"", fh);
} else if (flags & SPLAYS) {
fputs(""
"#splay"
"", fh);
} else {
fputs("", fh);
}
} else {
fputs("\n", fh);
}
if (clamp_to_ground) {
fputs("\n", fh);
} else {
fputs("absolute\n", fh);
}
PJ_COORD coord{{p1->x, p1->y, p1->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f\n",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
}
PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f\n",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
}
void
KML::xsect(const img_point *p, double angle, double d1, double d2)
{
if (clamp_to_ground) {
fputs("", fh);
} else {
fputs("absolute", fh);
}
double s = sin(rad(angle));
double c = cos(rad(angle));
{
PJ_COORD coord{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f ",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
}
{
PJ_COORD coord{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f\n",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
}
fputs("\n", fh);
}
void
KML::wall(const img_point *p, double angle, double d)
{
if (!in_wall) {
if (clamp_to_ground) {
fputs("", fh);
} else {
fputs("absolute", fh);
}
in_wall = true;
}
double s = sin(rad(angle));
double c = cos(rad(angle));
PJ_COORD coord{{p->x + s * d, p->y + c * d, p->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f\n",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
}
void
KML::passage(const img_point *p, double angle, double d1, double d2)
{
double s = sin(rad(angle));
double c = cos(rad(angle));
PJ_COORD coord1{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
coord1 = proj_trans(pj, PJ_FWD, coord1);
if (coord1.xyzt.x == HUGE_VAL ||
coord1.xyzt.y == HUGE_VAL ||
coord1.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
double x1 = coord1.xyzt.x;
double y1 = coord1.xyzt.y;
double z1 = coord1.xyzt.z;
PJ_COORD coord2{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
coord2 = proj_trans(pj, PJ_FWD, coord2);
if (coord2.xyzt.x == HUGE_VAL ||
coord2.xyzt.y == HUGE_VAL ||
coord2.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
double x2 = coord2.xyzt.x;
double y2 = coord2.xyzt.y;
double z2 = coord2.xyzt.z;
// Define each passage as a multigeometry comprising of one quadrilateral
// per section. This prevents invalid geometry (such as self-intersecting
// polygons) being created.
if (!in_passage){
in_passage = true;
fputs("\n", fh);
} else {
if (clamp_to_ground) {
fputs(""
"\n", fh);
} else {
fputs("absolute"
"\n", fh);
}
// Draw anti-clockwise around the ring.
fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
fprintf(fh, "%.8f,%.8f,%.2f\n", v1.GetX(), v1.GetY(), v1.GetZ());
fprintf(fh, "%.8f,%.8f,%.2f\n", x1, y1, z1);
fprintf(fh, "%.8f,%.8f,%.2f\n", x2, y2, z2);
// Close the ring.
fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
fputs(""
"\n", fh);
}
v2 = Vector3(x2, y2, z2);
v1 = Vector3(x1, y1, z1);
}
void
KML::tube_end()
{
if (in_passage){
fputs("\n", fh);
in_passage = false;
}
if (in_wall) {
fputs("\n", fh);
in_wall = false;
}
}
void
KML::label(const img_point *p, const wxString& str, int /*sflags*/, int type)
{
const char* s = str.utf8_str();
PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
coord = proj_trans(pj, PJ_FWD, coord);
if (coord.xyzt.x == HUGE_VAL ||
coord.xyzt.y == HUGE_VAL ||
coord.xyzt.z == HUGE_VAL) {
// FIXME report errors
}
// %.8f is at worst just over 1mm.
fprintf(fh, "%.8f,%.8f,%.2f",
coord.xyzt.x,
coord.xyzt.y,
coord.xyzt.z);
html_escape(fh, s);
fputs("", fh);
// Add a "pin" symbol with colour matching what aven shows.
switch (type) {
case FIXES:
fputs("#fix", fh);
break;
case EXPORTS:
fputs("#exp", fh);
break;
case ENTS:
fputs("#ent", fh);
break;
}
fputs("\n", fh);
}
void
KML::footer()
{
if (linestring_flags)
fputs("\n", fh);
fputs("\n", fh);
}