]> git.street.me.uk Git - andy/viking.git/commitdiff
Enable importing a map from a KMZ file.
authorRob Norris <rw_norris@hotmail.com>
Sun, 8 Nov 2015 12:41:48 +0000 (12:41 +0000)
committerRob Norris <rw_norris@hotmail.com>
Mon, 14 Dec 2015 23:56:56 +0000 (23:56 +0000)
Uses libzip to decompress the KMZ and processes the XML description to
 generate a corresponding GeoRef layer.

configure.ac
src/kmz.c
src/kmz.h
src/menu.xml.h
src/vikwindow.c
viking.spec.in

index 87c2dbb4a418a77c862050c8d4533ece328d6c23..82a30fce6dd72fc32c0f4d3eb179c725ba2590c6 100644 (file)
@@ -349,7 +349,7 @@ case $ac_cv_enable_zip in
     # Thus supporting old versions via simple #if #else #endif directives is seemingly not possible
     # Resort to checking versioning only via pkgconfig
     PKG_CHECK_MODULES([LIBZIP], [libzip >= 0.11],
-      [ AC_CHECK_HEADERS([zip.h],[],[AC_MSG_ERROR([zip.h is needed but not found - you will need to install package 'libzip-dev' or similar. The feature can be disabled with --disable-zip])]) 
+      [ AC_CHECK_HEADERS([zip.h],[],[AC_MSG_ERROR([zip.h is needed but not found - you will need to install package 'libzip-dev' or similar. The feature can be disabled with --disable-zip])])
         AC_CHECK_LIB(zip, [main], [], [AC_MSG_ERROR([libzip is needed but not found.])]) ],
       [ AC_MSG_WARN([libzip version needs to be at least 0.11, use of libzip is disabled])
         ac_cv_enable_zip=no ]
index 40aacde4efd0aabd6ace91fabc91e7f734c97ac3..3742ed8b6cf5d8a33bd729ee2ff652e692c46ddc 100644 (file)
--- a/src/kmz.c
+++ b/src/kmz.c
 #ifdef HAVE_ZIP_H
 #include <zip.h>
 #endif
-#include "coords.h"
-#include "fileutils.h"
+#include "viking.h"
+#include <string.h>
 #include <stdio.h>
 #include <glib/gstdio.h>
+#ifdef HAVE_EXPAT_H
+#include <expat.h>
+#endif
+#include <math.h>
 
 #ifdef HAVE_ZIP_H
 /**
@@ -154,3 +158,308 @@ typedef struct zip_source zip_source_t;
 #endif
 }
 
+typedef enum {
+       tt_unknown = 0,
+       tt_kml,
+       tt_kml_go,
+       tt_kml_go_name,
+       tt_kml_go_image,
+       tt_kml_go_latlonbox,
+       tt_kml_go_latlonbox_n,
+       tt_kml_go_latlonbox_e,
+       tt_kml_go_latlonbox_s,
+       tt_kml_go_latlonbox_w,
+} xtag_type;
+
+typedef struct {
+       xtag_type tag_type;              /* enum from above for this tag */
+       const char *tag_name;           /* xpath-ish tag name */
+} xtag_mapping;
+
+typedef struct {
+       GString *xpath;
+       GString *c_cdata;
+       xtag_type current_tag;
+       gchar *name;
+       gchar *image; // AKA icon
+       gdouble north;
+       gdouble east;
+       gdouble south;
+       gdouble west;
+} xml_data;
+
+#ifdef HAVE_ZIP_H
+#ifdef HAVE_EXPAT_H
+// NB No support for orientation ATM
+static xtag_mapping xtag_path_map[] = {
+       { tt_kml,                "/kml" },
+       { tt_kml_go,             "/kml/GroundOverlay" },
+       { tt_kml_go_name,        "/kml/GroundOverlay/name" },
+       { tt_kml_go_image,       "/kml/GroundOverlay/Icon/href" },
+       { tt_kml_go_latlonbox,   "/kml/GroundOverlay/LatLonBox" },
+       { tt_kml_go_latlonbox_n, "/kml/GroundOverlay/LatLonBox/north" },
+       { tt_kml_go_latlonbox_e, "/kml/GroundOverlay/LatLonBox/east" },
+       { tt_kml_go_latlonbox_s, "/kml/GroundOverlay/LatLonBox/south" },
+       { tt_kml_go_latlonbox_w, "/kml/GroundOverlay/LatLonBox/west" },
+};
+
+// NB Don't be pedantic about matching case of strings for tags
+static xtag_type get_tag ( const char *t )
+{
+       xtag_mapping *tm;
+       for (tm = xtag_path_map; tm->tag_type != 0; tm++)
+               if (0 == g_ascii_strcasecmp(tm->tag_name, t))
+                       return tm->tag_type;
+       return tt_unknown;
+}
+
+static void kml_start ( xml_data *xd, const char *el, const char **attr )
+{
+       g_string_append_c ( xd->xpath, '/' );
+       g_string_append ( xd->xpath, el );
+
+       xd->current_tag = get_tag ( xd->xpath->str );
+       switch ( xd->current_tag ) {
+       case tt_kml_go_name:
+       case tt_kml_go_image:
+       case tt_kml_go_latlonbox_n:
+       case tt_kml_go_latlonbox_s:
+       case tt_kml_go_latlonbox_e:
+       case tt_kml_go_latlonbox_w:
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       default: break;  // ignore cdata from other things
+       }
+}
+
+static void kml_end ( xml_data *xd, const char *el )
+{
+       g_string_truncate ( xd->xpath, xd->xpath->len - strlen(el) - 1 );
+
+       switch ( xd->current_tag ) {
+       case tt_kml_go_name:
+               xd->name = g_strdup ( xd->c_cdata->str );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       case tt_kml_go_image:
+               xd->image = g_strdup ( xd->c_cdata->str );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       case tt_kml_go_latlonbox_n:
+               xd->north = g_ascii_strtod ( xd->c_cdata->str, NULL );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       case tt_kml_go_latlonbox_s:
+               xd->south = g_ascii_strtod ( xd->c_cdata->str, NULL );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       case tt_kml_go_latlonbox_e:
+               xd->east = g_ascii_strtod ( xd->c_cdata->str, NULL );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       case tt_kml_go_latlonbox_w:
+               xd->west = g_ascii_strtod ( xd->c_cdata->str, NULL );
+               g_string_erase ( xd->c_cdata, 0, -1 );
+               break;
+       default:
+               break;
+       }
+
+       xd->current_tag = get_tag ( xd->xpath->str );
+}
+
+static void kml_cdata ( xml_data *xd, const XML_Char *s, int len )
+{
+       switch ( xd->current_tag ) {
+       case tt_kml_go_name:
+       case tt_kml_go_image:
+       case tt_kml_go_latlonbox_n:
+       case tt_kml_go_latlonbox_s:
+       case tt_kml_go_latlonbox_e:
+       case tt_kml_go_latlonbox_w:
+               g_string_append_len ( xd->c_cdata, s, len );
+               break;
+       default: break;  // ignore cdata from other things
+       }
+}
+#endif
+
+/**
+ *
+ */
+static gboolean parse_kml ( const char* buffer, int len, gchar **name, gchar **image, gdouble *north, gdouble *south, gdouble *east, gdouble *west )
+{
+#ifdef HAVE_EXPAT_H
+       XML_Parser parser = XML_ParserCreate(NULL);
+       enum XML_Status status = XML_STATUS_ERROR;
+
+       xml_data *xd = g_malloc ( sizeof (xml_data) );
+       // Set default (invalid) values;
+       xd->xpath = g_string_new ( "" );
+       xd->c_cdata = g_string_new ( "" );
+       xd->current_tag = tt_unknown;
+       xd->north = NAN;
+       xd->south = NAN;
+       xd->east = NAN;
+       xd->west = NAN;
+       xd->name = NULL;
+       xd->image = NULL;
+
+       XML_SetElementHandler(parser, (XML_StartElementHandler) kml_start, (XML_EndElementHandler) kml_end);
+       XML_SetUserData(parser, xd);
+       XML_SetCharacterDataHandler(parser, (XML_CharacterDataHandler) kml_cdata);
+
+       status = XML_Parse(parser, buffer, len, TRUE);
+
+       XML_ParserFree (parser);
+
+       *north = xd->north;
+       *south = xd->south;
+       *east = xd->east;
+       *west = xd->west;
+       *name = xd->name; // NB don't free xd->name
+       *image = xd->image; // NB don't free xd->image
+
+       g_string_free ( xd->xpath, TRUE );
+       g_string_free ( xd->c_cdata, TRUE );
+       g_free ( xd );
+
+       return status != XML_STATUS_ERROR;
+#else
+       return FALSE;
+#endif
+}
+#endif
+
+/**
+ * kmz_open_file:
+ * @filename: The KMZ file to open
+ * @vvp:      The #VikViewport
+ * @vlp:      The #VikLayersPanel that the converted KMZ will be stored in
+ *
+ * Returns:
+ *  -1 if KMZ not supported (this shouldn't happen)
+ *  0 on success
+ *  >0 <128 ZIP error code
+ *  128 - No doc.kml file in KMZ
+ *  129 - Couldn't understand the doc.kml file
+ *  130 - Couldn't get bounds from KML (error not detected ATM)
+ *  131 - No image file in KML
+ *  132 - Couldn't get image from KML
+ *  133 - Image file problem
+ */
+int kmz_open_file ( const gchar* filename, VikViewport *vvp, VikLayersPanel *vlp )
+{
+       // Unzip
+#ifdef HAVE_ZIP_H
+// Older libzip compatibility:
+#ifndef zip_t
+typedef struct zip zip_t;
+typedef struct zip_file zip_file_t;
+#endif
+#ifndef ZIP_RDONLY
+#define ZIP_RDONLY 0
+#endif
+
+       int ans = ZIP_ER_OK;
+       zip_t *archive = zip_open ( filename, ZIP_RDONLY, &ans );
+       if ( !archive ) {
+               g_warning ( "Unable to open archive: '%s' Error code %d", filename, ans );
+               goto cleanup;
+       }
+
+       zip_int64_t zindex = zip_name_locate ( archive, "doc.kml", ZIP_FL_NOCASE | ZIP_FL_ENC_GUESS );
+       if ( zindex == -1 ) {
+               g_warning ( "Unable to find doc.kml" );
+               goto kmz_cleanup;
+       }
+
+       struct zip_stat zs;
+       if ( zip_stat_index( archive, zindex, 0, &zs ) == 0) {
+               zip_file_t *zf = zip_fopen_index ( archive, zindex, 0 );
+               char *buffer = g_malloc(zs.size);
+               int len = zip_fread ( zf, buffer, zs.size );
+               if ( len != zs.size ) {
+                       ans = 128;
+                       g_free ( buffer );
+                       g_warning ( "Unable to read doc.kml from zip file" );
+                       goto kmz_cleanup;
+               }
+
+               gdouble north, south, east, west;
+               gchar *name = NULL;
+               gchar *image = NULL;
+               gboolean parsed = parse_kml ( buffer, len, &name, &image, &north, &south, &east, &west );
+               g_free ( buffer );
+
+               GdkPixbuf *pixbuf = NULL;
+
+               if ( parsed ) {
+                       // Read zip for image...
+                       if ( image ) {
+                               if ( zip_stat ( archive, image, ZIP_FL_NOCASE | ZIP_FL_ENC_GUESS, &zs ) == 0) {
+                                       zip_file_t *zfi = zip_fopen_index ( archive, zs.index, 0 );
+                                       // Don't know a way to create a pixbuf using streams.
+                                       // Thus write out to file
+                                       // Could read in chunks rather than one big buffer, but don't expect images to be that big
+                                       char *ibuffer = g_malloc(zs.size);
+                                       int ilen = zip_fread ( zfi, ibuffer, zs.size );
+                                       if ( ilen != zs.size ) {
+                                               ans = 131;
+                                               g_warning ( "Unable to read %s from zip file", image );
+                                       }
+                                       else {
+                                               gchar *image_file = util_write_tmp_file_from_bytes ( ibuffer, ilen );
+                                               GError *error = NULL;
+                                               pixbuf = gdk_pixbuf_new_from_file ( image_file, &error );
+                                               if ( error ) {
+                                                       g_warning ("%s: %s", __FUNCTION__, error->message );
+                                                       g_error_free ( error );
+                                                       ans = 133;
+                                               }
+                                               else {
+                                                       util_remove ( image_file );
+                                               }
+                                               g_free ( image_file );
+                                       }
+                                       g_free ( ibuffer );
+                               }
+                               g_free ( image );
+                       }
+                       else {
+                               ans = 132;
+                       }
+               }
+               else {
+                       ans = 129;
+               }
+
+               if ( pixbuf ) {
+                       // Some simple detection of broken position values ??
+                       //if ( xd->north > 90.0 || xd->north < -90.0 ||
+                       //     xd->south > 90.0 || xd->south < -90.0 || )
+                       VikCoord vc_tl, vc_br;
+                       struct LatLon ll_tl, ll_br;
+                       ll_tl.lat = north;
+                       ll_tl.lon = west;
+                       ll_br.lat = south;
+                       ll_br.lon = east;
+                       vik_coord_load_from_latlon ( &vc_tl, vik_viewport_get_coord_mode(vvp), &ll_tl );
+                       vik_coord_load_from_latlon ( &vc_br, vik_viewport_get_coord_mode(vvp), &ll_br );
+
+                       VikGeorefLayer *vgl = vik_georef_layer_create ( vvp, vlp, name, pixbuf, &vc_tl, &vc_br );
+                       if ( vgl ) {
+                               VikAggregateLayer *top = vik_layers_panel_get_top_layer ( vlp );
+                               vik_aggregate_layer_add_layer ( top, VIK_LAYER(vgl), FALSE );
+                       }
+               }
+       }
+
+kmz_cleanup:
+       zip_discard ( archive ); // Close and ensure unchanged
+ cleanup:
+       return ans;
+#else
+       return -1;
+#endif
+}
index 40623598df43f0a6f64d4e9644277dd5d5ac9f3a..762041f3e9e30851cac21446b082dd17e1ba58f3 100644 (file)
--- a/src/kmz.h
+++ b/src/kmz.h
 
 #include <glib.h>
 #include <gdk-pixbuf/gdk-pixbuf.h>
+#include "vikviewport.h"
+#include "viklayerspanel.h"
 
 G_BEGIN_DECLS
 
 int kmz_save_file ( GdkPixbuf *pixbuf, const gchar* filename, gdouble north, gdouble east, gdouble south, gdouble west );
 
+int kmz_open_file ( const gchar* filename, VikViewport *vvp, VikLayersPanel *vlp );
+
 G_END_DECLS
 
 #endif
index b061789b47bf23eb26bbb86f94a371931ca68c5d..ed136025f1238cc4bd439b79d0b7615bd4629fff 100644 (file)
@@ -35,6 +35,9 @@ static const char *menu_xml =
        "        <menuitem action='AcquireWikipedia'/>"
 #endif
        "      </menu>"
+#ifdef HAVE_ZIP_H
+       "      <menuitem action='ImportKMZ'/>"
+#endif
        "      <separator/>"
 #ifdef HAVE_ZIP_H
        "      <menuitem action='GenKMZ'/>"
index 597ef31112840e133eaf48c9063f23144833e777..b89791fbcceab64d1f9d751402a66b65d4287115 100644 (file)
@@ -4279,6 +4279,45 @@ static void draw_to_image_dir_cb ( GtkAction *a, VikWindow *vw )
   draw_to_image_file ( vw, VW_GEN_DIRECTORY_OF_IMAGES );
 }
 
+/**
+ *
+ */
+static void import_kmz_file_cb ( GtkAction *a, VikWindow *vw )
+{
+  GtkWidget *dialog = gtk_file_chooser_dialog_new (_("Open File"),
+                                                   GTK_WINDOW(vw),
+                                                   GTK_FILE_CHOOSER_ACTION_OPEN,
+                                                   GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+                                                   GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+                                                   NULL);
+
+  GtkFileFilter *filter;
+  filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name ( filter, _("KMZ") );
+  gtk_file_filter_add_mime_type ( filter, "vnd.google-earth.kmz");
+  gtk_file_filter_add_pattern ( filter, "*.kmz" );
+  gtk_file_chooser_add_filter ( GTK_FILE_CHOOSER(dialog), filter );
+  gtk_file_chooser_set_filter ( GTK_FILE_CHOOSER(dialog), filter );
+
+  filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name( filter, _("All") );
+  gtk_file_filter_add_pattern ( filter, "*" );
+  gtk_file_chooser_add_filter ( GTK_FILE_CHOOSER(dialog), filter );
+  // Default to any file - same as before open filters were added
+  gtk_file_chooser_set_filter ( GTK_FILE_CHOOSER(dialog), filter );
+
+  if ( gtk_dialog_run ( GTK_DIALOG(dialog) ) == GTK_RESPONSE_ACCEPT )  {
+    gchar *fn = gtk_file_chooser_get_filename ( GTK_FILE_CHOOSER(dialog) );
+    // TODO convert ans value into readable explaination of failure...
+    int ans = kmz_open_file ( fn, vw->viking_vvp, vw->viking_vlp );
+    if ( ans )
+      a_dialog_error_msg_extra ( GTK_WINDOW(vw), _("Unable to import %s."), fn );
+
+    draw_update ( vw );
+  }
+  gtk_widget_destroy ( dialog );
+}
+
 static void print_cb ( GtkAction *a, VikWindow *vw )
 {
   a_print(vw, vw->viking_vvp);
@@ -4437,6 +4476,7 @@ static GtkActionEntry entries[] = {
   { "SaveAs",    GTK_STOCK_SAVE_AS,      N_("Save _As..."),                      NULL,  N_("Save the file under different name"),           (GCallback)save_file_as          },
   { "FileProperties", NULL,              N_("Properties..."),                    NULL,  N_("File Properties"),                              (GCallback)file_properties_cb },
 #ifdef HAVE_ZIP_H
+  { "ImportKMZ", GTK_STOCK_CONVERT,      N_("Import KMZ _Map File..."),        NULL,  N_("Import a KMZ file"), (GCallback)import_kmz_file_cb },
   { "GenKMZ",    GTK_STOCK_DND,          N_("Generate _KMZ Map File..."),        NULL,  N_("Generate a KMZ file with an overlay of the current view"), (GCallback)draw_to_kmz_file_cb },
 #endif
   { "GenImg",    GTK_STOCK_FILE,         N_("_Generate Image File..."),          NULL,  N_("Save a snapshot of the workspace into a file"), (GCallback)draw_to_image_file_cb },
index cb0100df087ac868411176110c1253542f8d8012..4b9be5aeb561e714e28e7ef774c3e218bd85310c 100644 (file)
@@ -27,6 +27,7 @@ BuildRequires:  libbz2-devel
 BuildRequires:  libmagic-devel
 BuildRequires:  libsqlite3-devel
 BuildRequires:  libmapnik-devel
+BuildRequires:  libzip-devel
 
 %description
 Viking is a free/open source program to manage GPS data.