From ab92244238df63f100bcace212951275115830de Mon Sep 17 00:00:00 2001 From: Rob Norris Date: Sun, 8 Nov 2015 12:41:48 +0000 Subject: [PATCH] Enable importing a map from a KMZ file. Uses libzip to decompress the KMZ and processes the XML description to generate a corresponding GeoRef layer. --- configure.ac | 2 +- src/kmz.c | 313 +++++++++++++++++++++++++++++++++++++++++++++++- src/kmz.h | 4 + src/menu.xml.h | 3 + src/vikwindow.c | 40 +++++++ viking.spec.in | 1 + 6 files changed, 360 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 87c2dbb4..82a30fce 100644 --- a/configure.ac +++ b/configure.ac @@ -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 ] diff --git a/src/kmz.c b/src/kmz.c index 40aacde4..3742ed8b 100644 --- a/src/kmz.c +++ b/src/kmz.c @@ -27,10 +27,14 @@ #ifdef HAVE_ZIP_H #include #endif -#include "coords.h" -#include "fileutils.h" +#include "viking.h" +#include #include #include +#ifdef HAVE_EXPAT_H +#include +#endif +#include #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 +} diff --git a/src/kmz.h b/src/kmz.h index 40623598..762041f3 100644 --- a/src/kmz.h +++ b/src/kmz.h @@ -23,11 +23,15 @@ #include #include +#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 diff --git a/src/menu.xml.h b/src/menu.xml.h index b061789b..ed136025 100644 --- a/src/menu.xml.h +++ b/src/menu.xml.h @@ -35,6 +35,9 @@ static const char *menu_xml = " " #endif " " +#ifdef HAVE_ZIP_H + " " +#endif " " #ifdef HAVE_ZIP_H " " diff --git a/src/vikwindow.c b/src/vikwindow.c index 597ef311..b89791fb 100644 --- a/src/vikwindow.c +++ b/src/vikwindow.c @@ -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 }, diff --git a/viking.spec.in b/viking.spec.in index cb0100df..4b9be5ae 100644 --- a/viking.spec.in +++ b/viking.spec.in @@ -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. -- 2.39.5