source: git/src/kml.cc @ c2b9253

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

KML: Use https for icon URLs

  • Property mode set to 100644
File size: 9.6 KB
Line 
1/* kml.cc
2 * Export from Aven as KML.
3 */
4/* Copyright (C) 2012 Olaf Kähler
5 * Copyright (C) 2012-2024 Olly Betts
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
22#include <config.h>
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"
33#include <proj.h>
34
35#include "aven.h"
36#include "message.h"
37
38using namespace std;
39
40#define WGS84_DATUM_STRING "EPSG:4326"
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
63static void discarding_proj_logger(void *, int, const char *) { }
64
65KML::KML(const char * input_datum, bool clamp_to_ground_)
66    : clamp_to_ground(clamp_to_ground_)
67{
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);
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
84    if (!pj) {
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{
93    if (pj)
94        proj_destroy(pj);
95}
96
97const int *
98KML::passes() const
99{
100    static const int default_passes[] = {
101        PASG, XSECT, WALL1, WALL2, LEGS|SURF, LABELS|ENTS|FIXES|EXPORTS, 0
102    };
103    return default_passes;
104}
105
106/* Initialise KML routines. */
107void KML::header(const char * title, const char *, time_t,
108                 double, double, double, double, double, double)
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);
116    // Set up styles for the icons to reduce the file size.
117    fputs("<Style id=\"fix\"><IconStyle>"
118          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/red-blank.png</href></Icon>"
119          "</IconStyle></Style>\n", fh);
120    fputs("<Style id=\"exp\"><IconStyle>"
121          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/blu-blank.png</href></Icon>"
122          "</IconStyle></Style>\n", fh);
123    fputs("<Style id=\"ent\"><IconStyle>"
124          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/grn-blank.png</href></Icon>"
125          "</IconStyle></Style>\n", fh);
126    // FIXME: does KML allow bounds?
127    // NB Lat+long bounds are not necessarily the same as the bounds in survex
128    // coords translated to WGS84 lat+long...
129}
130
131void
132KML::start_pass(int)
133{
134    if (in_linestring) {
135        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
136        in_linestring = false;
137    }
138}
139
140void
141KML::line(const img_point *p1, const img_point *p, unsigned /*flags*/, bool fPendingMove)
142{
143    if (fPendingMove) {
144        if (!in_linestring) {
145            in_linestring = true;
146            fputs("<Placemark><MultiGeometry>\n", fh);
147        } else {
148            fputs("</coordinates></LineString>\n", fh);
149        }
150        if (clamp_to_ground) {
151            fputs("<LineString><coordinates>\n", fh);
152        } else {
153            fputs("<LineString><altitudeMode>absolute</altitudeMode><coordinates>\n", fh);
154        }
155
156        PJ_COORD coord{{p1->x, p1->y, p1->z, HUGE_VAL}};
157        coord = proj_trans(pj, PJ_FWD, coord);
158        if (coord.xyzt.x == HUGE_VAL ||
159            coord.xyzt.y == HUGE_VAL ||
160            coord.xyzt.z == HUGE_VAL) {
161            // FIXME report errors
162        }
163        // %.8f is at worst just over 1mm.
164        fprintf(fh, "%.8f,%.8f,%.2f\n",
165                coord.xyzt.x,
166                coord.xyzt.y,
167                coord.xyzt.z);
168    }
169
170    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
171    coord = proj_trans(pj, PJ_FWD, coord);
172    if (coord.xyzt.x == HUGE_VAL ||
173        coord.xyzt.y == HUGE_VAL ||
174        coord.xyzt.z == HUGE_VAL) {
175        // FIXME report errors
176    }
177    // %.8f is at worst just over 1mm.
178    fprintf(fh, "%.8f,%.8f,%.2f\n",
179            coord.xyzt.x,
180            coord.xyzt.y,
181            coord.xyzt.z);
182}
183
184void
185KML::xsect(const img_point *p, double angle, double d1, double d2)
186{
187    if (clamp_to_ground) {
188        fputs("<Placemark><name></name><LineString><coordinates>", fh);
189    } else {
190        fputs("<Placemark><name></name><LineString><altitudeMode>absolute</altitudeMode><coordinates>", fh);
191    }
192
193    double s = sin(rad(angle));
194    double c = cos(rad(angle));
195
196    {
197        PJ_COORD coord{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
198        coord = proj_trans(pj, PJ_FWD, coord);
199        if (coord.xyzt.x == HUGE_VAL ||
200            coord.xyzt.y == HUGE_VAL ||
201            coord.xyzt.z == HUGE_VAL) {
202            // FIXME report errors
203        }
204        // %.8f is at worst just over 1mm.
205        fprintf(fh, "%.8f,%.8f,%.2f ",
206                coord.xyzt.x,
207                coord.xyzt.y,
208                coord.xyzt.z);
209    }
210
211    {
212        PJ_COORD coord{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
213        coord = proj_trans(pj, PJ_FWD, coord);
214        if (coord.xyzt.x == HUGE_VAL ||
215            coord.xyzt.y == HUGE_VAL ||
216            coord.xyzt.z == HUGE_VAL) {
217            // FIXME report errors
218        }
219        // %.8f is at worst just over 1mm.
220        fprintf(fh, "%.8f,%.8f,%.2f\n",
221                coord.xyzt.x,
222                coord.xyzt.y,
223                coord.xyzt.z);
224    }
225
226    fputs("</coordinates></LineString></Placemark>\n", fh);
227}
228
229void
230KML::wall(const img_point *p, double angle, double d)
231{
232    if (!in_wall) {
233        if (clamp_to_ground) {
234            fputs("<Placemark><name></name><LineString><coordinates>", fh);
235        } else {
236            fputs("<Placemark><name></name><LineString><altitudeMode>absolute</altitudeMode><coordinates>", fh);
237        }
238        in_wall = true;
239    }
240
241    double s = sin(rad(angle));
242    double c = cos(rad(angle));
243
244    PJ_COORD coord{{p->x + s * d, p->y + c * d, p->z, HUGE_VAL}};
245    coord = proj_trans(pj, PJ_FWD, coord);
246    if (coord.xyzt.x == HUGE_VAL ||
247        coord.xyzt.y == HUGE_VAL ||
248        coord.xyzt.z == HUGE_VAL) {
249        // FIXME report errors
250    }
251    // %.8f is at worst just over 1mm.
252    fprintf(fh, "%.8f,%.8f,%.2f\n",
253            coord.xyzt.x,
254            coord.xyzt.y,
255            coord.xyzt.z);
256}
257
258void
259KML::passage(const img_point *p, double angle, double d1, double d2)
260{
261    double s = sin(rad(angle));
262    double c = cos(rad(angle));
263
264    PJ_COORD coord1{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
265    coord1 = proj_trans(pj, PJ_FWD, coord1);
266    if (coord1.xyzt.x == HUGE_VAL ||
267        coord1.xyzt.y == HUGE_VAL ||
268        coord1.xyzt.z == HUGE_VAL) {
269        // FIXME report errors
270    }
271    double x1 = coord1.xyzt.x;
272    double y1 = coord1.xyzt.y;
273    double z1 = coord1.xyzt.z;
274
275    PJ_COORD coord2{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
276    coord2 = proj_trans(pj, PJ_FWD, coord2);
277    if (coord2.xyzt.x == HUGE_VAL ||
278        coord2.xyzt.y == HUGE_VAL ||
279        coord2.xyzt.z == HUGE_VAL) {
280        // FIXME report errors
281    }
282    double x2 = coord2.xyzt.x;
283    double y2 = coord2.xyzt.y;
284    double z2 = coord2.xyzt.z;
285
286    // Define each passage as a multigeometry comprising of one quadrilateral
287    // per section.  This prevents invalid geometry (such as self-intersecting
288    // polygons) being created.
289
290    if (!in_passage){
291        in_passage = true;
292        fputs("<Placemark><name></name><MultiGeometry>\n", fh);
293    } else {
294        if (clamp_to_ground) {
295            fputs("<Polygon>"
296                  "<outerBoundaryIs><LinearRing><coordinates>\n", fh);
297        } else {
298            fputs("<Polygon><altitudeMode>absolute</altitudeMode>"
299                  "<outerBoundaryIs><LinearRing><coordinates>\n", fh);
300        }
301
302        // Draw anti-clockwise around the ring.
303        fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
304        fprintf(fh, "%.8f,%.8f,%.2f\n", v1.GetX(), v1.GetY(), v1.GetZ());
305
306        fprintf(fh, "%.8f,%.8f,%.2f\n", x1, y1, z1);
307        fprintf(fh, "%.8f,%.8f,%.2f\n", x2, y2, z2);
308
309        // Close the ring.
310        fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
311
312        fputs("</coordinates></LinearRing></outerBoundaryIs>"
313              "</Polygon>\n", fh);
314    }
315
316    v2 = Vector3(x2, y2, z2);
317    v1 = Vector3(x1, y1, z1);
318}
319
320void
321KML::tube_end()
322{
323    if (in_passage){
324        fputs("</MultiGeometry></Placemark>\n", fh);
325        in_passage = false;
326    }
327    if (in_wall) {
328        fputs("</coordinates></LineString></Placemark>\n", fh);
329        in_wall = false;
330    }
331}
332
333void
334KML::label(const img_point *p, const wxString& str, int /*sflags*/, int type)
335{
336    const char* s = str.utf8_str();
337    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
338    coord = proj_trans(pj, PJ_FWD, coord);
339    if (coord.xyzt.x == HUGE_VAL ||
340        coord.xyzt.y == HUGE_VAL ||
341        coord.xyzt.z == HUGE_VAL) {
342        // FIXME report errors
343    }
344    // %.8f is at worst just over 1mm.
345    fprintf(fh, "<Placemark><Point><coordinates>%.8f,%.8f,%.2f</coordinates></Point><name>",
346            coord.xyzt.x,
347            coord.xyzt.y,
348            coord.xyzt.z);
349    html_escape(fh, s);
350    fputs("</name>", fh);
351    // Add a "pin" symbol with colour matching what aven shows.
352    switch (type) {
353        case FIXES:
354            fputs("<styleUrl>#fix</styleUrl>", fh);
355            break;
356        case EXPORTS:
357            fputs("<styleUrl>#exp</styleUrl>", fh);
358            break;
359        case ENTS:
360            fputs("<styleUrl>#ent</styleUrl>", fh);
361            break;
362    }
363    fputs("</Placemark>\n", fh);
364}
365
366void
367KML::footer()
368{
369    if (in_linestring)
370        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
371    fputs("</Document></kml>\n", fh);
372}
Note: See TracBrowser for help on using the repository browser.