source: git/src/kml.cc @ 8a7804fb

Last change on this file since 8a7804fb was 0b99107, checked in by Olly Betts <olly@…>, 2 weeks ago

Eliminate old FSF addresses

Update GPL/LGPL licence files and boilerplate to direct people who
didn't receive the licence text to the FSF website, as the current
versions of the FSF licence texts now do, rather than giving a postal
address.

  • Property mode set to 100644
File size: 10.7 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, see
19 * <https://www.gnu.org/licenses/>.
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, 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    // Note "color" code order is aabbggrr
118    fputs("<Style id=\"fix\"><IconStyle>"
119          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/red-blank.png</href></Icon>"
120          "<color>ff0000ff</color>"
121          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
122          "</IconStyle></Style>\n", fh);
123    fputs("<Style id=\"exp\"><IconStyle>"
124          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/blu-blank.png</href></Icon>"
125          "<color>ffff0000</color>"
126          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
127          "</IconStyle></Style>\n", fh);
128    fputs("<Style id=\"ent\"><IconStyle>"
129          "<Icon><href>https://maps.google.com/mapfiles/kml/paddle/grn-blank.png</href></Icon>"
130          "<color>ff00ff00</color>"
131          "<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
132          "</IconStyle></Style>\n", fh);
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);
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
145void
146KML::start_pass(int)
147{
148    if (linestring_flags) {
149        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
150        linestring_flags = 0;
151    }
152}
153
154void
155KML::line(const img_point *p1, const img_point *p, unsigned flags, bool fPendingMove)
156{
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    }
162    if (fPendingMove) {
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            }
176        } else {
177            fputs("</coordinates></LineString>\n", fh);
178        }
179        if (clamp_to_ground) {
180            fputs("<LineString><coordinates>\n", fh);
181        } else {
182            fputs("<LineString><altitudeMode>absolute</altitudeMode><coordinates>\n", fh);
183        }
184
185        PJ_COORD coord{{p1->x, p1->y, p1->z, HUGE_VAL}};
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        }
192        // %.8f is at worst just over 1mm.
193        fprintf(fh, "%.8f,%.8f,%.2f\n",
194                coord.xyzt.x,
195                coord.xyzt.y,
196                coord.xyzt.z);
197    }
198
199    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
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
205    }
206    // %.8f is at worst just over 1mm.
207    fprintf(fh, "%.8f,%.8f,%.2f\n",
208            coord.xyzt.x,
209            coord.xyzt.y,
210            coord.xyzt.z);
211}
212
213void
214KML::xsect(const img_point *p, double angle, double d1, double d2)
215{
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    }
221
222    double s = sin(rad(angle));
223    double c = cos(rad(angle));
224
225    {
226        PJ_COORD coord{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
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    {
241        PJ_COORD coord{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
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
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) {
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;
268    }
269
270    double s = sin(rad(angle));
271    double c = cos(rad(angle));
272
273    PJ_COORD coord{{p->x + s * d, p->y + c * d, p->z, HUGE_VAL}};
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);
285}
286
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
293    PJ_COORD coord1{{p->x + s * d1, p->y + c * d1, p->z, HUGE_VAL}};
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
304    PJ_COORD coord2{{p->x - s * d2, p->y - c * d2, p->z, HUGE_VAL}};
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;
314
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.
318
319    if (!in_passage){
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        }
330
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());
334
335        fprintf(fh, "%.8f,%.8f,%.2f\n", x1, y1, z1);
336        fprintf(fh, "%.8f,%.8f,%.2f\n", x2, y2, z2);
337
338        // Close the ring.
339        fprintf(fh, "%.8f,%.8f,%.2f\n", v2.GetX(), v2.GetY(), v2.GetZ());
340
341        fputs("</coordinates></LinearRing></outerBoundaryIs>"
342              "</Polygon>\n", fh);
343    }
344
345    v2 = Vector3(x2, y2, z2);
346    v1 = Vector3(x1, y1, z1);
347}
348
349void
350KML::tube_end()
351{
352    if (in_passage){
353        fputs("</MultiGeometry></Placemark>\n", fh);
354        in_passage = false;
355    }
356    if (in_wall) {
357        fputs("</coordinates></LineString></Placemark>\n", fh);
358        in_wall = false;
359    }
360}
361
362void
363KML::label(const img_point *p, const wxString& str, int /*sflags*/, int type)
364{
365    const char* s = str.utf8_str();
366    PJ_COORD coord{{p->x, p->y, p->z, HUGE_VAL}};
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    }
373    // %.8f is at worst just over 1mm.
374    fprintf(fh, "<Placemark><Point><coordinates>%.8f,%.8f,%.2f</coordinates></Point><name>",
375            coord.xyzt.x,
376            coord.xyzt.y,
377            coord.xyzt.z);
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:
383            fputs("<styleUrl>#fix</styleUrl>", fh);
384            break;
385        case EXPORTS:
386            fputs("<styleUrl>#exp</styleUrl>", fh);
387            break;
388        case ENTS:
389            fputs("<styleUrl>#ent</styleUrl>", fh);
390            break;
391    }
392    fputs("</Placemark>\n", fh);
393}
394
395void
396KML::footer()
397{
398    if (linestring_flags)
399        fputs("</coordinates></LineString></MultiGeometry></Placemark>\n", fh);
400    fputs("</Document></kml>\n", fh);
401}
Note: See TracBrowser for help on using the repository browser.