]> git.street.me.uk Git - andy/viking.git/blob - src/kmz.c
vikcoord does not depend on GTK
[andy/viking.git] / src / kmz.c
1 /*
2  * viking -- GPS Data and Topo Analyzer, Explorer, and Manager
3  *
4  * Copyright (C) 2015, Rob Norris <rw_norris@hotmail.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  *
20  */
21
22 #include "kmz.h"
23
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif
27 #ifdef HAVE_ZIP_H
28 #include <zip.h>
29 #endif
30 #include "viking.h"
31 #include <string.h>
32 #include <stdio.h>
33 #include <glib/gstdio.h>
34 #ifdef HAVE_EXPAT_H
35 #include <expat.h>
36 #endif
37 #include <math.h>
38
39 #ifdef HAVE_ZIP_H
40 /**
41  * Simple KML 'file' with a Ground Overlay
42  *
43  * See https://developers.google.com/kml/documentation/kmlreference
44  *
45  * AFAIK the projection is always in Web Mercator
46  * Probably for normal use case of not too large an area coverage (on a Garmin device) the projection is near enough...
47  */
48 // Hopefully image_filename will not break the XML file tag structure
49 static gchar* doc_kml_str ( const gchar *name, const gchar *image_filename, gdouble north, gdouble south, gdouble east, gdouble west )
50 {
51         gchar *tmp_n = a_coords_dtostr ( north );
52         gchar *tmp_s = a_coords_dtostr ( south );
53         gchar *tmp_e = a_coords_dtostr ( east );
54         gchar *tmp_w = a_coords_dtostr ( west );
55
56         gchar *doc_kml = g_strdup_printf (
57                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
58                 "<kml xmlns=\"http://www.opengis.net/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\" xmlns:kml=\"http://www.opengis.net/kml/2.2\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"
59                 "<GroundOverlay>\n"
60                 "  <name>%s</name>\n"
61                 "  <Icon>\n"
62                 "    <href>%s</href>\n"
63                 "  </Icon>\n"
64                 "  <LatLonBox>\n"
65                 "    <north>%s</north>\n"
66                 "    <south>%s</south>\n"
67                 "    <east>%s</east>\n"
68                 "    <west>%s</west>\n"
69                 "    <rotation>0</rotation>\n" // Rotation always zero
70                 "  </LatLonBox>\n"
71                 "</GroundOverlay>\n"
72                 "</kml>\n",
73                 name, image_filename, tmp_n, tmp_s, tmp_e, tmp_w );
74
75         g_free ( tmp_n );
76         g_free ( tmp_s );
77         g_free ( tmp_e );
78         g_free ( tmp_w );
79
80         return doc_kml;
81 }
82 #endif
83
84 /**
85  * kmz_save_file:
86  *
87  * @pixbuf:   The image to save
88  * @filename: Save the KMZ as this filename
89  * @north:    Top latitude in degrees
90  * @east:     Right most longitude in degrees
91  * @south:    Bottom latitude in degrees
92  * @west:     Left most longitude in degrees
93  *
94  * Returns:
95  *  -1 if KMZ not supported (this shouldn't happen)
96  *   0 on success
97  *   >0 some kind of error
98  *
99  * Mostly intended for use with as a Custom Map on a Garmin
100  *
101  * See http://garminbasecamp.wikispaces.com/Custom+Maps
102  *
103  * The KMZ is a zipped file containing a KML file with the associated image
104  */
105 int kmz_save_file ( GdkPixbuf *pixbuf, const gchar* filename, gdouble north, gdouble east, gdouble south, gdouble west )
106 {
107 #ifdef HAVE_ZIP_H
108 // Older libzip compatibility:
109 #ifndef zip_source_t
110 typedef struct zip_source zip_source_t;
111 #endif
112
113         int ans = ZIP_ER_OK;
114         gchar *image_filename = "image.jpg";
115
116         // Generate KMZ file (a zip file)
117         struct zip* archive = zip_open ( filename, ZIP_CREATE | ZIP_TRUNCATE, &ans );
118         if ( !archive ) {
119                 g_warning ( "Unable to create archive: '%s' Error code %d", filename, ans );
120                 goto finish;
121         }
122
123         // Generate KML file
124         gchar *dk = doc_kml_str ( a_file_basename(filename), image_filename, north, south, east, west );
125         int dkl = strlen ( dk );
126
127         // KML must be named doc.kml in the kmz file
128         zip_source_t *src_kml = zip_source_buffer ( archive, dk, dkl, 0 );
129         zip_file_add ( archive, "doc.kml", src_kml, ZIP_FL_OVERWRITE );
130
131         GError *error = NULL;
132         gchar *buffer;
133         gsize blen;
134         gdk_pixbuf_save_to_buffer ( pixbuf, &buffer, &blen, "jpeg", &error, "x-dpi", "72", "y-dpi", "72", NULL );
135         if ( error ) {
136                 g_warning ( "Save to buffer error: %s", error->message );
137                 g_error_free (error);
138                 zip_discard ( archive );
139                 ans = 130;
140                 goto kml_cleanup;
141         }
142
143         zip_source_t *src_img = zip_source_buffer ( archive, buffer, (int)blen, 0 );
144         zip_file_add ( archive, image_filename, src_img, ZIP_FL_OVERWRITE );
145         // NB Only store as limited use trying to (further) compress a JPG
146         zip_set_file_compression ( archive, 1, ZIP_CM_STORE, 0 );
147
148         ans = zip_close ( archive );
149
150         g_free ( buffer );
151
152  kml_cleanup:
153         g_free ( dk );
154  finish:
155         return ans;
156 #else
157         return -1;
158 #endif
159 }
160
161 typedef enum {
162         tt_unknown = 0,
163         tt_kml,
164         tt_kml_go,
165         tt_kml_go_name,
166         tt_kml_go_image,
167         tt_kml_go_latlonbox,
168         tt_kml_go_latlonbox_n,
169         tt_kml_go_latlonbox_e,
170         tt_kml_go_latlonbox_s,
171         tt_kml_go_latlonbox_w,
172 } xtag_type;
173
174 typedef struct {
175         xtag_type tag_type;              /* enum from above for this tag */
176         const char *tag_name;           /* xpath-ish tag name */
177 } xtag_mapping;
178
179 typedef struct {
180         GString *xpath;
181         GString *c_cdata;
182         xtag_type current_tag;
183         gchar *name;
184         gchar *image; // AKA icon
185         gdouble north;
186         gdouble east;
187         gdouble south;
188         gdouble west;
189 } xml_data;
190
191 #ifdef HAVE_ZIP_H
192 #ifdef HAVE_EXPAT_H
193 // NB No support for orientation ATM
194 static xtag_mapping xtag_path_map[] = {
195         { tt_kml,                "/kml" },
196         { tt_kml_go,             "/kml/GroundOverlay" },
197         { tt_kml_go_name,        "/kml/GroundOverlay/name" },
198         { tt_kml_go_image,       "/kml/GroundOverlay/Icon/href" },
199         { tt_kml_go_latlonbox,   "/kml/GroundOverlay/LatLonBox" },
200         { tt_kml_go_latlonbox_n, "/kml/GroundOverlay/LatLonBox/north" },
201         { tt_kml_go_latlonbox_e, "/kml/GroundOverlay/LatLonBox/east" },
202         { tt_kml_go_latlonbox_s, "/kml/GroundOverlay/LatLonBox/south" },
203         { tt_kml_go_latlonbox_w, "/kml/GroundOverlay/LatLonBox/west" },
204 };
205
206 // NB Don't be pedantic about matching case of strings for tags
207 static xtag_type get_tag ( const char *t )
208 {
209         xtag_mapping *tm;
210         for (tm = xtag_path_map; tm->tag_type != 0; tm++)
211                 if (0 == g_ascii_strcasecmp(tm->tag_name, t))
212                         return tm->tag_type;
213         return tt_unknown;
214 }
215
216 static void kml_start ( xml_data *xd, const char *el, const char **attr )
217 {
218         g_string_append_c ( xd->xpath, '/' );
219         g_string_append ( xd->xpath, el );
220
221         xd->current_tag = get_tag ( xd->xpath->str );
222         switch ( xd->current_tag ) {
223         case tt_kml_go_name:
224         case tt_kml_go_image:
225         case tt_kml_go_latlonbox_n:
226         case tt_kml_go_latlonbox_s:
227         case tt_kml_go_latlonbox_e:
228         case tt_kml_go_latlonbox_w:
229                 g_string_erase ( xd->c_cdata, 0, -1 );
230                 break;
231         default: break;  // ignore cdata from other things
232         }
233 }
234
235 static void kml_end ( xml_data *xd, const char *el )
236 {
237         g_string_truncate ( xd->xpath, xd->xpath->len - strlen(el) - 1 );
238
239         switch ( xd->current_tag ) {
240         case tt_kml_go_name:
241                 xd->name = g_strdup ( xd->c_cdata->str );
242                 g_string_erase ( xd->c_cdata, 0, -1 );
243                 break;
244         case tt_kml_go_image:
245                 xd->image = g_strdup ( xd->c_cdata->str );
246                 g_string_erase ( xd->c_cdata, 0, -1 );
247                 break;
248         case tt_kml_go_latlonbox_n:
249                 xd->north = g_ascii_strtod ( xd->c_cdata->str, NULL );
250                 g_string_erase ( xd->c_cdata, 0, -1 );
251                 break;
252         case tt_kml_go_latlonbox_s:
253                 xd->south = g_ascii_strtod ( xd->c_cdata->str, NULL );
254                 g_string_erase ( xd->c_cdata, 0, -1 );
255                 break;
256         case tt_kml_go_latlonbox_e:
257                 xd->east = g_ascii_strtod ( xd->c_cdata->str, NULL );
258                 g_string_erase ( xd->c_cdata, 0, -1 );
259                 break;
260         case tt_kml_go_latlonbox_w:
261                 xd->west = g_ascii_strtod ( xd->c_cdata->str, NULL );
262                 g_string_erase ( xd->c_cdata, 0, -1 );
263                 break;
264         default:
265                 break;
266         }
267
268         xd->current_tag = get_tag ( xd->xpath->str );
269 }
270
271 static void kml_cdata ( xml_data *xd, const XML_Char *s, int len )
272 {
273         switch ( xd->current_tag ) {
274         case tt_kml_go_name:
275         case tt_kml_go_image:
276         case tt_kml_go_latlonbox_n:
277         case tt_kml_go_latlonbox_s:
278         case tt_kml_go_latlonbox_e:
279         case tt_kml_go_latlonbox_w:
280                 g_string_append_len ( xd->c_cdata, s, len );
281                 break;
282         default: break;  // ignore cdata from other things
283         }
284 }
285 #endif
286
287 /**
288  *
289  */
290 static gboolean parse_kml ( const char* buffer, int len, gchar **name, gchar **image, gdouble *north, gdouble *south, gdouble *east, gdouble *west )
291 {
292 #ifdef HAVE_EXPAT_H
293         XML_Parser parser = XML_ParserCreate(NULL);
294         enum XML_Status status = XML_STATUS_ERROR;
295
296         xml_data *xd = g_malloc ( sizeof (xml_data) );
297         // Set default (invalid) values;
298         xd->xpath = g_string_new ( "" );
299         xd->c_cdata = g_string_new ( "" );
300         xd->current_tag = tt_unknown;
301         xd->north = NAN;
302         xd->south = NAN;
303         xd->east = NAN;
304         xd->west = NAN;
305         xd->name = NULL;
306         xd->image = NULL;
307
308         XML_SetElementHandler(parser, (XML_StartElementHandler) kml_start, (XML_EndElementHandler) kml_end);
309         XML_SetUserData(parser, xd);
310         XML_SetCharacterDataHandler(parser, (XML_CharacterDataHandler) kml_cdata);
311
312         status = XML_Parse(parser, buffer, len, TRUE);
313
314         XML_ParserFree (parser);
315
316         *north = xd->north;
317         *south = xd->south;
318         *east = xd->east;
319         *west = xd->west;
320         *name = xd->name; // NB don't free xd->name
321         *image = xd->image; // NB don't free xd->image
322
323         g_string_free ( xd->xpath, TRUE );
324         g_string_free ( xd->c_cdata, TRUE );
325         g_free ( xd );
326
327         return status != XML_STATUS_ERROR;
328 #else
329         return FALSE;
330 #endif
331 }
332 #endif
333
334 /**
335  * kmz_open_file:
336  * @filename: The KMZ file to open
337  * @vvp:      The #VikViewport
338  * @vlp:      The #VikLayersPanel that the converted KMZ will be stored in
339  *
340  * Returns:
341  *  -1 if KMZ not supported (this shouldn't happen)
342  *  0 on success
343  *  >0 <128 ZIP error code
344  *  128 - No doc.kml file in KMZ
345  *  129 - Couldn't understand the doc.kml file
346  *  130 - Couldn't get bounds from KML (error not detected ATM)
347  *  131 - No image file in KML
348  *  132 - Couldn't get image from KML
349  *  133 - Image file problem
350  */
351 int kmz_open_file ( const gchar* filename, VikViewport *vvp, VikLayersPanel *vlp )
352 {
353         // Unzip
354 #ifdef HAVE_ZIP_H
355 // Older libzip compatibility:
356 #ifndef zip_t
357 typedef struct zip zip_t;
358 typedef struct zip_file zip_file_t;
359 #endif
360 #ifndef ZIP_RDONLY
361 #define ZIP_RDONLY 0
362 #endif
363
364         int ans = ZIP_ER_OK;
365         zip_t *archive = zip_open ( filename, ZIP_RDONLY, &ans );
366         if ( !archive ) {
367                 g_warning ( "Unable to open archive: '%s' Error code %d", filename, ans );
368                 goto cleanup;
369         }
370
371         zip_int64_t zindex = zip_name_locate ( archive, "doc.kml", ZIP_FL_NOCASE | ZIP_FL_ENC_GUESS );
372         if ( zindex == -1 ) {
373                 g_warning ( "Unable to find doc.kml" );
374                 goto kmz_cleanup;
375         }
376
377         struct zip_stat zs;
378         if ( zip_stat_index( archive, zindex, 0, &zs ) == 0) {
379                 zip_file_t *zf = zip_fopen_index ( archive, zindex, 0 );
380                 char *buffer = g_malloc(zs.size);
381                 int len = zip_fread ( zf, buffer, zs.size );
382                 if ( len != zs.size ) {
383                         ans = 128;
384                         g_free ( buffer );
385                         g_warning ( "Unable to read doc.kml from zip file" );
386                         goto kmz_cleanup;
387                 }
388
389                 gdouble north, south, east, west;
390                 gchar *name = NULL;
391                 gchar *image = NULL;
392                 gboolean parsed = parse_kml ( buffer, len, &name, &image, &north, &south, &east, &west );
393                 g_free ( buffer );
394
395                 GdkPixbuf *pixbuf = NULL;
396
397                 if ( parsed ) {
398                         // Read zip for image...
399                         if ( image ) {
400                                 if ( zip_stat ( archive, image, ZIP_FL_NOCASE | ZIP_FL_ENC_GUESS, &zs ) == 0) {
401                                         zip_file_t *zfi = zip_fopen_index ( archive, zs.index, 0 );
402                                         // Don't know a way to create a pixbuf using streams.
403                                         // Thus write out to file
404                                         // Could read in chunks rather than one big buffer, but don't expect images to be that big
405                                         char *ibuffer = g_malloc(zs.size);
406                                         int ilen = zip_fread ( zfi, ibuffer, zs.size );
407                                         if ( ilen != zs.size ) {
408                                                 ans = 131;
409                                                 g_warning ( "Unable to read %s from zip file", image );
410                                         }
411                                         else {
412                                                 gchar *image_file = util_write_tmp_file_from_bytes ( ibuffer, ilen );
413                                                 GError *error = NULL;
414                                                 pixbuf = gdk_pixbuf_new_from_file ( image_file, &error );
415                                                 if ( error ) {
416                                                         g_warning ("%s: %s", __FUNCTION__, error->message );
417                                                         g_error_free ( error );
418                                                         ans = 133;
419                                                 }
420                                                 else {
421                                                         util_remove ( image_file );
422                                                 }
423                                                 g_free ( image_file );
424                                         }
425                                         g_free ( ibuffer );
426                                 }
427                                 g_free ( image );
428                         }
429                         else {
430                                 ans = 132;
431                         }
432                 }
433                 else {
434                         ans = 129;
435                 }
436
437                 if ( pixbuf ) {
438                         // Some simple detection of broken position values ??
439                         //if ( xd->north > 90.0 || xd->north < -90.0 ||
440                         //     xd->south > 90.0 || xd->south < -90.0 || )
441                         VikCoord vc_tl, vc_br;
442                         struct LatLon ll_tl, ll_br;
443                         ll_tl.lat = north;
444                         ll_tl.lon = west;
445                         ll_br.lat = south;
446                         ll_br.lon = east;
447                         vik_coord_load_from_latlon ( &vc_tl, vik_viewport_get_coord_mode(vvp), &ll_tl );
448                         vik_coord_load_from_latlon ( &vc_br, vik_viewport_get_coord_mode(vvp), &ll_br );
449
450                         VikGeorefLayer *vgl = vik_georef_layer_create ( vvp, vlp, name, pixbuf, &vc_tl, &vc_br );
451                         if ( vgl ) {
452                                 VikAggregateLayer *top = vik_layers_panel_get_top_layer ( vlp );
453                                 vik_aggregate_layer_add_layer ( top, VIK_LAYER(vgl), FALSE );
454                         }
455                 }
456         }
457
458 kmz_cleanup:
459         zip_discard ( archive ); // Close and ensure unchanged
460  cleanup:
461         return ans;
462 #else
463         return -1;
464 #endif
465 }