source: git/src/kml.cc @ b632a67

stereo-2025
Last change on this file since b632a67 was e6ef621, checked in by Olly Betts <olly@…>, 11 months ago

KML: Use styles for surface legs and splays

Addresses #60 for KML

  • Property mode set to 100644
File size: 10.7 KB
RevLine 
[1534ed9]1/* kml.cc
2 * Export from Aven as KML.
3 */
4/* Copyright (C) 2012 Olaf Kähler
[371385f]5 * Copyright (C) 2012-2024 Olly Betts
[1534ed9]6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
20 */
21
[4c83f84]22#include <config.h>
[1534ed9]23
24#include "kml.h"
25
26#include "export.h" // For LABELS, etc
27
28#include <stdio.h>
29#include <string>
30#include <math.h>
31
32#include "useful.h"
[c2ea0c5]33#include <proj.h>
[1534ed9]34
35#include "aven.h"
36#include "message.h"
37
38using namespace std;
39
[297d142]40#define WGS84_DATUM_STRING "EPSG:4326"
[1534ed9]41
42static void
43html_escape(FILE *fh, const char *s)
44{
45    while (*s) {
46        switch (*s) {
47            case '<':
48                fputs("&lt;", fh);
49                break;
50            case '>':
51                fputs("&gt;", fh);
52                break;
53            case '&':
54                fputs("&amp;", fh);
55                break;
56            default:
57                PUTC(*s, fh);
58        }
59        ++s;
60    }
61}
62
[c2ea0c5]63static void discarding_proj_logger(void *, int, const char *) { }
64
[32a040e]65KML::KML(const char * input_datum, bool clamp_to_ground_)
66    : clamp_to_ground(clamp_to_ground_)
[1534ed9]67{
[c2ea0c5]68    /* Prevent stderr spew from PROJ. */
69    proj_log_func(PJ_DEFAULT_CTX, nullptr, discarding_proj_logger);
70
71    pj = proj_create_crs_to_crs(PJ_DEFAULT_CTX,
72                                input_datum, WGS84_DATUM_STRING,
73                                NULL);
[297d142]74
75    if (pj) {
76        // Normalise the output order so x is longitude and y latitude - by
77        // default new PROJ has them switched for EPSG:4326 which just seems
78        // confusing.
79        PJ* pj_norm = proj_normalize_for_visualization(PJ_DEFAULT_CTX, pj);
80        proj_destroy(pj);
81        pj = pj_norm;
82    }
83
[c2ea0c5]84    if (!pj) {
[1534ed9]85        wxString m = wmsg(/*Failed to initialise input coordinate system “%s”*/287);
86        m = wxString::Format(m.c_str(), input_datum);
87        throw m;
88    }
89}
90
91KML::~KML()
92{
[c2ea0c5]93    if (pj)
94        proj_destroy(pj);
[1534ed9]95}
96
97const int *
98KML::passes() const
99{
[f2cb101]100    static const int default_passes[] = {
[e63994c]101        PASG, XSECT, WALL1, WALL2, LEGS|SURF, LABELS|ENTS|FIXES|EXPORTS, 0
[f2cb101]102    };
[1534ed9]103    return default_passes;
104}
105
106/* Initialise KML routines. */
[55a861a]107void KML::header(const char * title, const char *, time_t,
108                 double, double, double, double, double, double)
[1534ed9]109{
110    fputs(
111"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
112"<kml xmlns=\"http://www.opengis.net/kml/2.2\">\n", fh);
113    fputs("<Document><name>", fh);
114    html_escape(fh, title);
115    fputs("</name>\n", fh);
[7aed359]116    // Set up styles for the icons to reduce the file size.
[e6ef621]117    // Note "color" code order is aabbggrr
[7aed359]118    fputs("<Style id=\"fix\"><IconStyle>"
[c2b9253]119          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/red-blank.png</href></Icon>"
[e6ef621]120          "<color>ff0000ff</color>"
121          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
[7aed359]122          "</IconStyle></Style>\n", fh);
123    fputs("<Style id=\"exp\"><IconStyle>"
[c2b9253]124          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/blu-blank.png</href></Icon>"
[e6ef621]125          "<color>ffff0000</color>"
126          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
[7aed359]127          "</IconStyle></Style>\n", fh);
128    fputs("<Style id=\"ent\"><IconStyle>"
[c2b9253]129          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/grn-blank.png</href></Icon>"
[e6ef621]130          "<color>ff00ff00</color>"
131          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
[7aed359]132          "</IconStyle></Style>\n", fh);
[e6ef621]133    // Set up styles for surface legs and splays.
134    fputs("<Style id=\"surf\"><LineStyle>"
135          "<color>ff00ff00</color>" // Green.
136          "</LineStyle></Style>\n", fh);
137    fputs("<Style id=\"splay\"><LineStyle>"
138          "<color>40ff00ff</color>" // Partly transparent magenta.
139          "</LineStyle></Style>\n", fh);
[1534ed9]140    // FIXME: does KML allow bounds?
141    // NB Lat+long bounds are not necessarily the same as the bounds in survex
142    // coords translated to WGS84 lat+long...
143}
144
[f03d89e]145void
[0fe63d3]146KML::start_pass(int)
[f03d89e]147{
[e6ef621]148    if (linestring_flags) {
[f03d89e]149        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
[e6ef621]150        linestring_flags = 0;
[f03d89e]151    }
152}
153
[1534ed9]154void
[e6ef621]155KML::line(const img_point *p1, const img_point *p, unsigned flags, bool fPendingMove)
[1534ed9]156{
[e6ef621]157    if (linestring_flags && linestring_flags != (flags & (LEGS|SURF|SPLAYS))) {
158        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
159        linestring_flags = 0;
160        fPendingMove = true;
161    }
[1534ed9]162    if (fPendingMove) {
[e6ef621]163        if (linestring_flags == 0) {
164            linestring_flags = (flags & (LEGS|SURF|SPLAYS));
165            if (flags & SURF) {
166                fputs("<Placemark>"
167                      "<styleUrl>#surf</styleUrl>"
168                      "<MultiGeometry>", fh);
169            } else if (flags & SPLAYS) {
170                fputs("<Placemark>"
171                      "<styleUrl>#splay</styleUrl>"
172                      "<MultiGeometry>", fh);
173            } else {
174                fputs("<Placemark><MultiGeometry>", fh);
175            }
[3b23819]176        } else {
177            fputs("</coordinates></LineString>\n", fh);
178        }
[32a040e]179        if (clamp_to_ground) {
180            fputs("<LineString><coordinates>\n", fh);
181        } else {
182            fputs("<LineString><altitudeMode>absolute</altitudeMode><coordinates>\n", fh);
183        }
[c2ea0c5]184
[47b2b81]185        PJ_COORD coord{{p1->x, p1->y, p1->z, HUGE_VAL}};
[c2ea0c5]186        coord = proj_trans(pj, PJ_FWD, coord);
187        if (coord.xyzt.x == HUGE_VAL ||
188            coord.xyzt.y == HUGE_VAL ||
189            coord.xyzt.z == HUGE_VAL) {
190            // FIXME report errors
191        }
[1534ed9]192        // %.8f is at worst just over 1mm.
[c2ea0c5]193        fprintf(fh, "%.8f,%.8f,%.2f\n",
194                coord.xyzt.x,
195                coord.xyzt.y,
196                coord.xyzt.z);
197    }
198
[47b2b81]199    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
[c2ea0c5]200    coord = proj_trans(pj, PJ_FWD, coord);
201    if (coord.xyzt.x == HUGE_VAL ||
202        coord.xyzt.y == HUGE_VAL ||
203        coord.xyzt.z == HUGE_VAL) {
204        // FIXME report errors
[1534ed9]205    }
206    // %.8f is at worst just over 1mm.
[c2ea0c5]207    fprintf(fh, "%.8f,%.8f,%.2f\n",
208            coord.xyzt.x,
209            coord.xyzt.y,
210            coord.xyzt.z);
[1534ed9]211}
212
[e63994c]213void
214KML::xsect(const img_point *p, double angle, double d1, double d2)
215{
[32a040e]216    if (clamp_to_ground) {
217        fputs("<Placemark><name></name><LineString><coordinates>", fh);
218    } else {
219        fputs("<Placemark><name></name><LineString><altitudeMode>absolute</altitudeMode><coordinates>", fh);
220    }
[c2ea0c5]221
222    double s = sin(rad(angle));
223    double c = cos(rad(angle));
224
225    {
[47b2b81]226        PJ_COORD coord{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
[c2ea0c5]227        coord = proj_trans(pj, PJ_FWD, coord);
228        if (coord.xyzt.x == HUGE_VAL ||
229            coord.xyzt.y == HUGE_VAL ||
230            coord.xyzt.z == HUGE_VAL) {
231            // FIXME report errors
232        }
233        // %.8f is at worst just over 1mm.
234        fprintf(fh, "%.8f,%.8f,%.2f ",
235                coord.xyzt.x,
236                coord.xyzt.y,
237                coord.xyzt.z);
238    }
239
240    {
[47b2b81]241        PJ_COORD coord{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
[c2ea0c5]242        coord = proj_trans(pj, PJ_FWD, coord);
243        if (coord.xyzt.x == HUGE_VAL ||
244            coord.xyzt.y == HUGE_VAL ||
245            coord.xyzt.z == HUGE_VAL) {
246            // FIXME report errors
247        }
248        // %.8f is at worst just over 1mm.
249        fprintf(fh, "%.8f,%.8f,%.2f\n",
250                coord.xyzt.x,
251                coord.xyzt.y,
252                coord.xyzt.z);
253    }
254
[e63994c]255    fputs("</coordinates></LineString></Placemark>\n", fh);
256}
257
258void
259KML::wall(const img_point *p, double angle, double d)
260{
261    if (!in_wall) {
[6028fff]262        if (clamp_to_ground) {
263            fputs("<Placemark><name></name><LineString><coordinates>", fh);
264        } else {
265            fputs("<Placemark><name></name><LineString><altitudeMode>absolute</altitudeMode><coordinates>", fh);
266        }
267        in_wall = true;
[e63994c]268    }
[c2ea0c5]269
270    double s = sin(rad(angle));
271    double c = cos(rad(angle));
272
[47b2b81]273    PJ_COORD coord{{p->x + s * d, p->y + c * d, p->z, HUGE_VAL}};
[c2ea0c5]274    coord = proj_trans(pj, PJ_FWD, coord);
275    if (coord.xyzt.x == HUGE_VAL ||
276        coord.xyzt.y == HUGE_VAL ||
277        coord.xyzt.z == HUGE_VAL) {
278        // FIXME report errors
279    }
280    // %.8f is at worst just over 1mm.
281    fprintf(fh, "%.8f,%.8f,%.2f\n",
282            coord.xyzt.x,
283            coord.xyzt.y,
284            coord.xyzt.z);
[e63994c]285}
286
[f2cb101]287void
288KML::passage(const img_point *p, double angle, double d1, double d2)
289{
290    double s = sin(rad(angle));
291    double c = cos(rad(angle));
292
[47b2b81]293    PJ_COORD coord1{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
[c2ea0c5]294    coord1 = proj_trans(pj, PJ_FWD, coord1);
295    if (coord1.xyzt.x == HUGE_VAL ||
296        coord1.xyzt.y == HUGE_VAL ||
297        coord1.xyzt.z == HUGE_VAL) {
298        // FIXME report errors
299    }
300    double x1 = coord1.xyzt.x;
301    double y1 = coord1.xyzt.y;
302    double z1 = coord1.xyzt.z;
303
[47b2b81]304    PJ_COORD coord2{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
[c2ea0c5]305    coord2 = proj_trans(pj, PJ_FWD, coord2);
306    if (coord2.xyzt.x == HUGE_VAL ||
307        coord2.xyzt.y == HUGE_VAL ||
308        coord2.xyzt.z == HUGE_VAL) {
309        // FIXME report errors
310    }
311    double x2 = coord2.xyzt.x;
312    double y2 = coord2.xyzt.y;
313    double z2 = coord2.xyzt.z;
[f2cb101]314
[6028fff]315    // Define each passage as a multigeometry comprising of one quadrilateral
316    // per section.  This prevents invalid geometry (such as self-intersecting
317    // polygons) being created.
[0939dcd]318
319    if (!in_passage){
[6028fff]320        in_passage = true;
321        fputs("<Placemark><name></name><MultiGeometry>\n", fh);
322    } else {
323        if (clamp_to_ground) {
324            fputs("<Polygon>"
325                  "<outerBoundaryIs><LinearRing><coordinates>\n", fh);
326        } else {
327            fputs("<Polygon><altitudeMode>absolute</altitudeMode>"
328                  "<outerBoundaryIs><LinearRing><coordinates>\n", fh);
329        }
[0939dcd]330
[6028fff]331        // Draw anti-clockwise around the ring.
332        fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
333        fprintf(fh, "%.8f,%.8f,%.2f\n", v1.GetX(), v1.GetY(), v1.GetZ());
[0939dcd]334
[6028fff]335        fprintf(fh, "%.8f,%.8f,%.2f\n", x1, y1, z1);
336        fprintf(fh, "%.8f,%.8f,%.2f\n", x2, y2, z2);
[0939dcd]337
[6028fff]338        // Close the ring.
339        fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
[0939dcd]340
[6028fff]341        fputs("</coordinates></LinearRing></outerBoundaryIs>"
342              "</Polygon>\n", fh);
[0939dcd]343    }
344
[6028fff]345    v2 = Vector3(x2, y2, z2);
346    v1 = Vector3(x1, y1, z1);
[f2cb101]347}
348
349void
350KML::tube_end()
351{
[0939dcd]352    if (in_passage){
[6028fff]353        fputs("</MultiGeometry></Placemark>\n", fh);
354        in_passage = false;
[e63994c]355    }
356    if (in_wall) {
[6028fff]357        fputs("</coordinates></LineString></Placemark>\n", fh);
358        in_wall = false;
[f2cb101]359    }
360}
361
[1534ed9]362void
[371385f]363KML::label(const img_point *p, const wxString& str, int /*sflags*/, int type)
[1534ed9]364{
[8c4cefb]365    const char* s = str.utf8_str();
[47b2b81]366    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
[c2ea0c5]367    coord = proj_trans(pj, PJ_FWD, coord);
368    if (coord.xyzt.x == HUGE_VAL ||
369        coord.xyzt.y == HUGE_VAL ||
370        coord.xyzt.z == HUGE_VAL) {
371        // FIXME report errors
372    }
[1534ed9]373    // %.8f is at worst just over 1mm.
[c2ea0c5]374    fprintf(fh, "<Placemark><Point><coordinates>%.8f,%.8f,%.2f</coordinates></Point><name>",
375            coord.xyzt.x,
376            coord.xyzt.y,
377            coord.xyzt.z);
[1534ed9]378    html_escape(fh, s);
379    fputs("</name>", fh);
380    // Add a "pin" symbol with colour matching what aven shows.
381    switch (type) {
382        case FIXES:
[7aed359]383            fputs("<styleUrl>#fix</styleUrl>", fh);
[1534ed9]384            break;
385        case EXPORTS:
[7aed359]386            fputs("<styleUrl>#exp</styleUrl>", fh);
[1534ed9]387            break;
388        case ENTS:
[7aed359]389            fputs("<styleUrl>#ent</styleUrl>", fh);
[1534ed9]390            break;
391    }
392    fputs("</Placemark>\n", fh);
393}
394
395void
396KML::footer()
397{
[e6ef621]398    if (linestring_flags)
[3b23819]399        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
[1534ed9]400    fputs("</Document></kml>\n", fh);
401}
Note: See TracBrowser for help on using the repository browser.