[gnome-maps/wip/osm-edit: 4/4] osmEdit: WIP, implement editing objects in OSM.



commit 1f707c9ac0196cdc9ef27d7d33180593a3c18d9c
Author: Marcus Lundblad <ml update uu se>
Date:   Mon Jan 19 22:24:34 2015 +0100

        osmEdit: WIP, implement editing objects in OSM.

 configure.ac                           |    1 +
 data/org.gnome.Maps.data.gresource.xml |    1 +
 data/ui/map-bubble.ui                  |   11 +
 data/ui/osm-edit-dialog.ui             |  236 +++++++++++++++
 lib/Makefile.am                        |   16 +-
 lib/maps-osm-changeset.c               |   61 ++++
 lib/maps-osm-changeset.h               |   51 ++++
 lib/maps-osm-node.c                    |  203 +++++++++++++
 lib/maps-osm-node.h                    |   53 ++++
 lib/maps-osm-object.c                  |  378 +++++++++++++++++++++++++
 lib/maps-osm-object.h                  |   63 ++++
 lib/maps-osm-relation.c                |  157 ++++++++++
 lib/maps-osm-relation.h                |   58 ++++
 lib/maps-osm-way.c                     |  116 ++++++++
 lib/maps-osm-way.h                     |   49 ++++
 lib/maps-osm.c                         |  487 ++++++++++++++++++++++++++++++++
 lib/maps-osm.h                         |   36 +++
 src/application.js                     |    3 +
 src/mapBubble.js                       |    7 +-
 src/org.gnome.Maps.src.gresource.xml   |    4 +
 src/osmConnection.js                   |  301 ++++++++++++++++++++
 src/osmEdit.js                         |  144 ++++++++++
 src/osmEditDialog.js                   |  382 +++++++++++++++++++++++++
 src/osmUtils.js                        |   57 ++++
 src/placeBubble.js                     |   45 +++-
 25 files changed, 2915 insertions(+), 5 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 4df8a69..0091ccc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -50,6 +50,7 @@ PKG_CHECK_MODULES(GNOME_MAPS_LIB, [
     folks                        >= $FOLKS_MIN_VERSION
     geocode-glib-1.0             >= $GEOCODE_MIN_VERSION
     champlain-0.12               >= $CHAMPLAIN_MIN_VERSION
+    libxml-2.0
 ])
 AC_SUBST(GNOME_MAPS_LIB_CFLAGS)
 AC_SUBST(GNOME_MAPS_LIB_LIBS)
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 6ba6040..eb68093 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -13,6 +13,7 @@
     <file preprocess="xml-stripblanks">ui/main-window.ui</file>
     <file preprocess="xml-stripblanks">ui/map-bubble.ui</file>
     <file preprocess="xml-stripblanks">ui/notification.ui</file>
+    <file preprocess="xml-stripblanks">ui/osm-edit-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/place-bubble.ui</file>
     <file preprocess="xml-stripblanks">ui/place-entry.ui</file>
     <file preprocess="xml-stripblanks">ui/place-list-row.ui</file>
diff --git a/data/ui/map-bubble.ui b/data/ui/map-bubble.ui
index e488412..17b8706 100644
--- a/data/ui/map-bubble.ui
+++ b/data/ui/map-bubble.ui
@@ -108,6 +108,17 @@
                 <property name="tooltip-text" translatable="yes">Check in here</property>
               </object>
             </child>
+            <child>
+              <object class="GtkButton" id="bubble-edit-button">
+                <property name="name">bubble-edit-button"</property>
+                <property name="label" translatable="yes">Edit</property>
+                <!-- TODO: this button should be invisible by default
+                     when we handle OSM accounts -->
+                <property name="visible">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+             </object>
+            </child>
           </object>
           <packing>
             <property name="left_attach">0</property>
diff --git a/data/ui/osm-edit-dialog.ui b/data/ui/osm-edit-dialog.ui
new file mode 100644
index 0000000..4e60716
--- /dev/null
+++ b/data/ui/osm-edit-dialog.ui
@@ -0,0 +1,236 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OSMEditDialog" parent="GtkDialog">
+    <property name="can_focus">False</property>
+    <property name="type">popup</property>
+    <property name="type_hint">dialog</property>
+    <property name="width_request">500</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="contentArea">
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="homogeneous">True</property>
+            <property name="transition_type">crossfade</property>
+            <child>
+              <object class="GtkGrid" id="loadingGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkSpinner" id="loadingSpinner">
+                    <property name="height_request">32</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="vexpand">True</property>
+                    <property name="active">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">loading</property>
+              </packing>
+            </child>
+            <child>
+             <object class="GtkBox">
+               <property name="visible">True</property>
+                <property name="can_focus">False</property>
+               <property name="orientation">vertical</property>
+               <property name="margin">20</property>
+
+               <child>
+                 <object class="GtkGrid" id="editorGrid">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                   <property name="row-spacing">12</property>
+                   <property name="column-spacing">6</property>
+                   <property name="margin-bottom">12</property>
+                 </object>
+               </child>
+               <child>
+                 <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                   <property name="orientation">horizontal</property>
+
+                   <child>
+                     <object class="GtkMenuButton" id="addFieldButton">
+                       <property name="visible">True</property>
+                       <property name="can_focus">True</property>
+                       <property name="receives_default">False</property>
+                       <property name="popover">addFieldPopover</property>
+                       <property name="direction">GTK_ARROW_UP</property>
+                       
+                       <child>
+                         <object class="GtkBox">
+                           <property name="visible">True</property>
+                           <property name="can_focus">False</property>
+                           <property name="orientation">horizontal</property>
+                           <property name="spacing">5</property>
+                           <child>
+                             <object class="GtkLabel">
+                               <property name="visible">True</property>
+                               <property name="can_focus">False</property>
+                               <property name="label">Add Field</property>
+                             </object>
+                           </child>
+                           <child>
+                             <object class="GtkImage">
+                               <property name="visible">True</property>
+                               <property name="can_focus">False</property>
+                               <property name="icon-name">go-up-symbolic</property>
+                             </object>
+                           </child>
+                         </object>
+                       </child>
+                     </object>
+                     <packing>
+                       <property name="pack-type">GTK_PACK_START</property>
+                     </packing>
+                   </child>
+                 </object>
+                 <packing>
+                   <property name="pack-type">GTK_PACK_END</property>
+                 </packing>
+               </child>
+             </object>
+             <packing>
+                <property name="name">editor</property>
+             </packing>
+            </child>
+            <child>
+              <object class="GtkGrid" id="uploadGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_start">15</property>
+                <property name="margin_end">15</property>
+                <property name="margin_top">15</property>
+                <property name="margin_bottom">15</property>
+               <property name="row-spacing">5</property>
+                <child>
+                  <object class="GtkLabel" id="commentLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                   <property name="label" translatable="true">Comment</property>
+                   <property name="halign">GTK_ALIGN_START</property>
+                   <style>
+                     <class name="dim-label"/>
+                   </style>
+                 </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+               <child>
+                 <object class="GtkFrame">
+                   <property name="visible">True</property>
+                   <child>
+                      <object class="GtkTextView" id="commentTextView">
+                       <property name="visible">True</property>
+                       <property name="can_focus">True</property>
+                       <property name="hexpand">True</property>
+                       <property name="vexpand">True</property>
+                     </object>
+                    </child>
+                 </object>
+                 <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+               </child>
+                <child>
+                  <object class="GtkLabel" id="uploadInfoLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                   <property name="label" translatable="true">Map changes will be visible on all maps that 
use
+OpenStreetMap data.</property>
+                   <property name="halign">GTK_ALIGN_START</property>
+                   <style>
+                     <class name="dim-label"/>
+                   </style>
+                 </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">3</property>
+                  </packing>
+                </child>
+
+              </object>
+              <packing>
+                <property name="name">upload</property>
+              </packing>
+            </child>
+         </object>
+       </child>
+      </object>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="headerBar">
+        <property name="visible">True</property>
+        <property name="can-focus">False</property>
+        <property name="show-close-button">False</property>
+       <property name="title" translatable="yes">Edit Point of Interest</property>
+        <child>
+          <object class="GtkButton" id="cancelButton">
+            <property name="label" translatable="yes">Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="backButton">
+            <property name="visible">False</property>
+            <property name="can_focus">True</property>
+            <child>
+              <object class="GtkImage">
+               <property name="visible">True</property>
+               <property name="can_focus">False</property>
+               <property name="icon-name">go-previous-symbolic</property>
+               <property name="pixel_size">16</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="nextButton">
+            <property name="label" translatable="yes">Next</property>
+            <property name="visible">True</property>
+           <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+           <property name="receives_default">True</property>
+            <style>
+              <class name="default"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+
+  </template>
+
+  <object class="GtkPopover" id="addFieldPopover">
+    <property name="visible">False</property>x
+    <child>
+      <object class="GtkBox" id="addFieldPopoverBox">
+       <property name="visible">True</property>
+       <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+      </object>
+    </child>
+  </object>  
+</interface>
diff --git a/lib/Makefile.am b/lib/Makefile.am
index cc4252c..1d7e9bf 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -9,12 +9,24 @@ libgnome_maps_headers_private =                                       \
        maps-contact.h                                                  \
        mapsintl.h                                                      \
        maps.h                                                          \
-       maps-file-tile-source.h
+       maps-file-tile-source.h                                 \
+       maps-osm.h                                              \
+       maps-osm-changeset.h                                    \
+       maps-osm-node.h                                         \
+       maps-osm-object.h                                       \
+       maps-osm-relation.h                                     \
+       maps-osm-way.h
 
 libgnome_maps_sources =                                                \
        maps-contact-store.c                                            \
        maps-contact.c                                                  \
-       maps-file-tile-source.c
+       maps-file-tile-source.c                                 \
+       maps-osm.c                                              \
+       maps-osm-changeset.c                                    \
+       maps-osm-node.c                                         \
+       maps-osm-object.c                                       \
+       maps-osm-way.c                                          \
+       maps-osm-relation.c
 
 libgnome_maps_la_SOURCES =                                             \
        $(libgnome_maps_sources)                                        \
diff --git a/lib/maps-osm-changeset.c b/lib/maps-osm-changeset.c
new file mode 100644
index 0000000..719bcdc
--- /dev/null
+++ b/lib/maps-osm-changeset.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm-changeset.h"
+
+G_DEFINE_TYPE (MapsOSMChangeset, maps_osm_changeset,
+              MAPS_TYPE_OSMOBJECT)
+
+static const char *
+maps_osm_changeset_get_xml_tag_name (void)
+{
+  return "changeset";
+}
+
+static void
+maps_osm_changeset_class_init (MapsOSMChangesetClass *klass)
+{
+  MapsOSMObjectClass *object_class = MAPS_OSMOBJECT_CLASS (klass);
+
+  object_class->get_xml_tag_name = maps_osm_changeset_get_xml_tag_name;
+}
+
+static void
+maps_osm_changeset_init (MapsOSMChangeset *changeset)
+{
+}
+
+MapsOSMChangeset *
+maps_osm_changeset_new (const char *comment, const char *source,
+                       const char *created_by)
+{
+  MapsOSMChangeset *changeset = g_object_new (MAPS_TYPE_OSMCHANGESET, NULL);
+
+  if (comment)
+    maps_osm_object_set_tag (MAPS_OSMOBJECT (changeset), "comment", comment);
+
+  if (source)
+    maps_osm_object_set_tag (MAPS_OSMOBJECT (changeset), "source", source);
+
+  if (created_by)
+    maps_osm_object_set_tag (MAPS_OSMOBJECT (changeset), "created_by",
+                            created_by);
+
+  return changeset;
+}
diff --git a/lib/maps-osm-changeset.h b/lib/maps-osm-changeset.h
new file mode 100644
index 0000000..51253ca
--- /dev/null
+++ b/lib/maps-osm-changeset.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_CHANGESET_H__
+#define __MAPS_OSM_CHANGESET_H__
+
+#include "maps-osm-object.h"
+
+#include <glib-object.h>
+
+#define MAPS_TYPE_OSMCHANGESET maps_osm_changeset_get_type ()
+G_DECLARE_FINAL_TYPE(MapsOSMChangeset, maps_osm_changeset, MAPS, OSMCHANGESET,
+                    MapsOSMObject)
+
+struct _MapsOSMChangeset
+{
+  MapsOSMObject parent_instance;
+};
+
+struct _MapsOSMChangesetClass
+{
+  MapsOSMObjectClass parent_class;
+};
+
+/**
+ * maps_osm_changeset_new:
+ * @comment: (nullable): A comment about the OSM change, optional
+ * @source: (nullable): The source of the OSM change, optional
+ */
+MapsOSMChangeset *maps_osm_changeset_new (const char *comment,
+                                         const char *source,
+                                         const char *created_by);
+
+#endif /* __MAPS_OSM_CHANGESET_H__ */
+
diff --git a/lib/maps-osm-node.c b/lib/maps-osm-node.c
new file mode 100644
index 0000000..564acd3
--- /dev/null
+++ b/lib/maps-osm-node.c
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm-node.h"
+
+struct _MapsOSMNodePrivate
+{
+  double lon;
+  double lat;
+};
+
+enum {
+  PROP_0,
+
+  PROP_LON,
+  PROP_LAT
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (MapsOSMNode, maps_osm_node,
+                           MAPS_TYPE_OSMOBJECT)
+
+
+static void
+maps_osm_node_set_property (GObject      *object,
+                           guint         property_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  MapsOSMNode *node = MAPS_OSMNODE (object);
+
+  switch (property_id)
+    {
+    case PROP_LON:
+      node->priv->lon = g_value_get_double (value);
+      break;
+
+    case PROP_LAT:
+      node->priv->lat = g_value_get_double (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+maps_osm_node_get_property (GObject    *object,
+                           guint       property_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  MapsOSMNode *node = MAPS_OSMNODE (object);
+
+  switch (property_id)
+    {
+    case PROP_LON:
+      g_value_set_double (value,
+                         node->priv->lon);
+      break;
+
+    case PROP_LAT:
+      g_value_set_double (value,
+                         node->priv->lat);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static const char *
+maps_osm_node_get_xml_tag_name (void)
+{
+  return "node";
+}
+
+static GHashTable *
+maps_osm_node_get_xml_attributes (const MapsOSMObject *object)
+{
+  const MapsOSMNode *node = MAPS_OSMNODE (object);
+  GHashTable *attributes;
+  char buf[G_ASCII_DTOSTR_BUF_SIZE];
+  
+  attributes = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                     NULL, g_free);
+
+  g_ascii_dtostr (buf, sizeof (buf), node->priv->lon);
+  g_hash_table_insert (attributes, "lon", g_strdup (buf));
+  g_ascii_dtostr (buf, sizeof (buf), node->priv->lat);  
+  g_hash_table_insert (attributes, "lat", g_strdup (buf)); 
+
+  return attributes;
+}
+
+static void
+maps_osm_node_class_init (MapsOSMNodeClass *klass)
+{
+  GObjectClass *node_class = G_OBJECT_CLASS (klass);
+  MapsOSMObjectClass *object_class = MAPS_OSMOBJECT_CLASS (klass);
+  GParamSpec *pspec;
+
+  node_class->get_property = maps_osm_node_get_property;
+  node_class->set_property = maps_osm_node_set_property;
+  object_class->get_xml_tag_name = maps_osm_node_get_xml_tag_name;
+  object_class->get_xml_attributes = maps_osm_node_get_xml_attributes;
+
+  /**
+   * MapsOSMNode:lon:
+   *
+   * The longitude of the node.
+   */
+  pspec = g_param_spec_double ("lon",
+                              "Longitude",
+                              "Longitude",
+                              -180.0,
+                              180.0,
+                              0.0,
+                              G_PARAM_READWRITE);
+  g_object_class_install_property (node_class, PROP_LON, pspec);
+
+  /**
+   * MapsOSMNode:lat:
+   *
+   * The latitude of the node.
+   */
+  pspec = g_param_spec_double ("lat",
+                              "Latitude",
+                              "Latitude",
+                              -90.0,
+                              90.0,
+                              0.0,
+                              G_PARAM_READWRITE);
+  g_object_class_install_property (node_class, PROP_LAT, pspec);
+}
+
+static void
+maps_osm_node_init (MapsOSMNode *node)
+{
+  node->priv = maps_osm_node_get_instance_private (node);
+
+  node->priv->lon = 0.0;
+  node->priv->lat = 0.0;
+}
+
+MapsOSMNode *
+maps_osm_node_new (guint64 id, guint version, guint64 changeset,
+                  double lon, double lat)
+{
+  return g_object_new (MAPS_TYPE_OSMNODE,
+                      "id", id,
+                      "version", version,
+                      "changeset", changeset,
+                      "lon", lon,
+                      "lat", lat, NULL);
+}
+
+double
+maps_osm_node_get_lon (MapsOSMNode *node)
+{
+  double lon;
+
+  g_object_get (node, "lon", &lon);
+  return lon;
+}
+
+void
+maps_osm_node_set_lon (MapsOSMNode *node, double lon)
+{
+  g_object_set (node, "lon", lon);
+}
+
+double
+maps_osm_node_get_lat (MapsOSMNode *node)
+{
+  double lat;
+
+  g_object_get (node, "lat", &lat);
+  return lat;
+}
+
+void
+maps_osm_node_set_lat (MapsOSMNode *node, double lat)
+{
+  g_object_set (node, "lat", lat);
+}
diff --git a/lib/maps-osm-node.h b/lib/maps-osm-node.h
new file mode 100644
index 0000000..a2a74d7
--- /dev/null
+++ b/lib/maps-osm-node.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_NODE_H__
+#define __MAPS_OSM_NODE_H__
+
+#include "maps-osm-object.h"
+
+#include <glib-object.h>
+
+#define MAPS_TYPE_OSMNODE maps_osm_node_get_type ()
+G_DECLARE_FINAL_TYPE(MapsOSMNode, maps_osm_node, MAPS, OSMNODE,
+                    MapsOSMObject)
+
+typedef struct _MapsOSMNodePrivate MapsOSMNodePrivate;
+
+struct _MapsOSMNode
+{
+  MapsOSMObject parent_instance;
+  MapsOSMNodePrivate *priv;
+};
+
+struct _MapsOSMNodeClass
+{
+  MapsOSMObjectClass parent_class;
+};
+
+MapsOSMNode *maps_osm_node_new (guint64 id, guint version, guint64 changeset,
+                               double lon, double lat);
+
+double maps_osm_node_get_lon (MapsOSMNode *node);
+void maps_osm_node_set_lon (MapsOSMNode *node, double lon);
+double maps_osm_node_get_lat (MapsOSMNode *node);
+void maps_osm_node_set_lat (MapsOSMNode *node, double lat);
+
+
+#endif //__MAPS_OSM_NODE_H__
diff --git a/lib/maps-osm-object.c b/lib/maps-osm-object.c
new file mode 100644
index 0000000..0d60e95
--- /dev/null
+++ b/lib/maps-osm-object.c
@@ -0,0 +1,378 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm-object.h"
+
+struct _MapsOSMObjectPrivate
+{
+  guint64 id;
+  guint version;
+  guint64 changeset;
+
+  GHashTable *tags;
+};
+
+enum {
+  PROP_0,
+
+  PROP_ID,
+  PROP_VERSION,
+  PROP_CHANGESET
+};
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (MapsOSMObject, maps_osm_object,
+                                    G_TYPE_OBJECT)
+
+static void
+maps_osm_object_set_property (GObject      *object,
+                             guint         property_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  MapsOSMObject *osm_object = MAPS_OSMOBJECT (object);
+  MapsOSMObjectPrivate *priv =
+    maps_osm_object_get_instance_private (osm_object);
+  
+  switch (property_id)
+    {
+    case PROP_ID:
+      priv->id = g_value_get_uint64 (value);
+      break;
+
+    case PROP_VERSION:
+      g_debug ("setting version: %d\n", g_value_get_uint (value));
+      priv->version = g_value_get_uint (value);
+      break;
+
+    case PROP_CHANGESET:
+      priv->changeset = g_value_get_uint64 (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+maps_osm_object_get_property (GObject    *object,
+                             guint       property_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  MapsOSMObject *osm_object = MAPS_OSMOBJECT (object);
+  MapsOSMObjectPrivate *priv =
+    maps_osm_object_get_instance_private (osm_object);
+
+  switch (property_id)
+    {
+    case PROP_ID:
+      g_value_set_uint64 (value,
+                         priv->id);
+      break;
+
+    case PROP_VERSION:
+      g_value_set_uint (value,
+                       priv->version);
+      g_debug ("getting version: %d\n", g_value_get_uint (value));
+      break;
+
+    case PROP_CHANGESET:
+      g_value_set_uint64 (value,
+                         priv->changeset);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+maps_osm_object_dispose (GObject *object)
+{
+  MapsOSMObject *osm_object = MAPS_OSMOBJECT (object);
+  MapsOSMObjectPrivate *priv =
+    maps_osm_object_get_instance_private (osm_object);
+  
+  g_hash_table_destroy (priv->tags);
+  priv->tags = NULL;
+
+  G_OBJECT_CLASS (maps_osm_object_parent_class)->dispose (object);
+}
+
+/* base implementation returning no object-specific XML attributes */
+static GHashTable *
+maps_osm_object_get_xml_attributes (const MapsOSMObject *object)
+{
+  return g_hash_table_new (g_str_hash, g_str_equal);
+}
+
+/* base implementation return no object-specific child XML nodes */
+static xmlNodePtr
+maps_osm_object_get_xml_child_nodes (const MapsOSMObject *object)
+{
+  return NULL;
+}
+
+static void
+maps_osm_object_class_init (MapsOSMObjectClass *klass)
+{
+  GObjectClass *maps_class = G_OBJECT_CLASS (klass);
+  MapsOSMObjectClass *object_class = MAPS_OSMOBJECT_CLASS (klass);
+  GParamSpec *pspec;
+
+  maps_class->dispose = maps_osm_object_dispose;
+  maps_class->get_property = maps_osm_object_get_property;
+  maps_class->set_property = maps_osm_object_set_property;
+  object_class->get_xml_attributes = maps_osm_object_get_xml_attributes;
+  object_class->get_xml_child_nodes = maps_osm_object_get_xml_child_nodes;
+  
+  /**
+   * MapsOSMObject:id:
+   *
+   * The OSM id of the object.
+   */
+  pspec = g_param_spec_uint64 ("id",
+                              "ID",
+                              "ID",
+                              0,
+                              G_MAXUINT64,
+                              0,
+                              G_PARAM_READWRITE);
+  g_object_class_install_property (maps_class, PROP_ID, pspec);
+
+  /**
+   * MapsOSMObject:version:
+   *
+   * The latest OSM version of the object.
+   */
+  pspec = g_param_spec_uint("version",
+                           "Version",
+                           "Version",
+                           0,
+                           G_MAXUINT,
+                           0,
+                           G_PARAM_READWRITE);
+  g_object_class_install_property (maps_class, PROP_VERSION, pspec);
+
+  /**
+   * MapsOSMObject:changeset:
+   *
+   * The OSM changeset for the current upload of the object.
+   */
+  pspec = g_param_spec_uint64 ("changeset",
+                              "Changeset",
+                              "Changeset",
+                              0,
+                              G_MAXUINT64,
+                              0,
+                              G_PARAM_READWRITE);
+  g_object_class_install_property (maps_class, PROP_CHANGESET, pspec);
+}
+  
+static void
+maps_osm_object_init (MapsOSMObject *object)
+{
+  MapsOSMObjectPrivate *priv = maps_osm_object_get_instance_private (object);
+
+  priv->tags = g_hash_table_new_full (g_str_hash,
+                                     g_str_equal,
+                                     g_free,
+                                     g_free);
+}
+
+guint64
+maps_osm_object_get_id (MapsOSMObject *object)
+{
+  guint64 id;
+
+  g_object_get (G_OBJECT (object), "id", &id);
+  return id;
+}
+
+void
+maps_osm_object_set_id (MapsOSMObject *object, guint64 id)
+{
+  g_object_set (G_OBJECT (object), "id", id);
+}
+
+guint
+maps_osm_object_get_version (MapsOSMObject *object)
+{
+  guint version;
+
+  g_object_get (G_OBJECT (object), "version", &version);
+  return version;
+}
+
+void
+maps_osm_object_set_version (MapsOSMObject *object, guint version)
+{
+  g_object_set (G_OBJECT (object), "version", version);
+}
+
+guint64
+maps_osm_object_get_changeset (MapsOSMObject *object)
+{
+  guint64 changeset;
+
+  g_object_get (G_OBJECT (object), "changeset", &changeset);
+  return changeset;
+}
+
+void
+maps_osm_object_set_changset (MapsOSMObject *object, guint64 changeset)
+{
+  g_object_set (G_OBJECT (object), "changeset", changeset);
+}
+
+const char *
+maps_osm_object_get_tag (const MapsOSMObject *object, const char *key)
+{
+  MapsOSMObjectPrivate *priv = maps_osm_object_get_instance_private (object);
+
+  return g_hash_table_lookup (priv->tags, key);
+}
+
+void
+maps_osm_object_set_tag (MapsOSMObject *object,
+                        const char *key, const char *value)
+{
+  MapsOSMObjectPrivate *priv = maps_osm_object_get_instance_private (object);
+
+  g_hash_table_insert (priv->tags, g_strdup (key), g_strdup (value));
+}
+
+void
+maps_osm_object_delete_tag (MapsOSMObject *object, const char *key)
+{
+  MapsOSMObjectPrivate *priv = maps_osm_object_get_instance_private (object);
+
+  g_hash_table_remove (priv->tags, key);
+}
+
+void
+maps_osm_object_foreach_tag (gpointer key, gpointer value, gpointer user_data)
+{
+  const char *name = (const char *) key;
+  const char *val = (const char *) value;
+  xmlNodePtr object_node = (xmlNodePtr) user_data;
+
+  /* skip tag if it has an empty placeholder value */
+  if (val) {
+    xmlNodePtr tag_node;
+    
+    tag_node = xmlNewNode (NULL, "tag");
+    xmlNewProp (tag_node, "k", key);
+    xmlNewProp (tag_node, "v", val);
+    xmlAddChild (object_node, tag_node);
+  }
+}
+
+void
+maps_osm_object_foreach_type_attr (gpointer key, gpointer value,
+                                  gpointer user_data)
+{
+  const char *name = (const char *) key;
+  const char *val = (const char *) value;
+  xmlNodePtr object_node = (xmlNodePtr) user_data;
+
+  xmlNewProp (object_node, name, val);
+}
+
+xmlDocPtr
+maps_osm_object_to_xml (const MapsOSMObject *object)
+{
+  MapsOSMObjectPrivate *priv;
+  xmlDocPtr doc;
+  xmlNodePtr osm_node;
+  xmlNodePtr object_node;
+  const char *type;
+  guint64 id;
+  guint version;
+  guint64 changeset;
+  GHashTable *type_attrs;
+  xmlNodePtr type_sub_nodes;
+  
+  doc = xmlNewDoc ("1.0");
+  osm_node = xmlNewNode (NULL, "osm");
+  priv = (MapsOSMObjectPrivate *) maps_osm_object_get_instance_private (object);
+  type = MAPS_OSMOBJECT_GET_CLASS (object)->get_xml_tag_name ();
+  object_node = xmlNewNode (NULL, type);
+
+  /* add common OSM attributes */
+  id = priv->id;
+  version = priv->version;
+  changeset = priv->changeset;
+
+  if (id != 0) {
+    char buf[32];
+    g_snprintf (buf, 32, "%" G_GUINT64_FORMAT, id);
+    xmlNewProp (object_node, "id", buf);
+  }
+    
+  if (version != 0) {
+    char buf[16];
+    g_snprintf (buf, 16, "%d", version);
+    xmlNewProp (object_node, "version", buf);
+  }
+    
+  if (changeset != 0) {
+    char buf[32];
+    g_snprintf (buf, 32, "%" G_GUINT64_FORMAT, changeset);
+    xmlNewProp (object_node, "changeset", buf);
+  }
+
+  /* add OSM tags */
+  g_hash_table_foreach (priv->tags, maps_osm_object_foreach_tag, object_node);
+  
+  /* add type-specific attributes */
+  type_attrs = MAPS_OSMOBJECT_GET_CLASS (object)->get_xml_attributes (object);
+  g_hash_table_foreach (type_attrs, maps_osm_object_foreach_type_attr,
+                        object_node);
+  g_hash_table_destroy (type_attrs);
+
+  /* add type-specific sub-nodes */
+  type_sub_nodes =
+    MAPS_OSMOBJECT_GET_CLASS (object)->get_xml_child_nodes (object);
+  if (type_sub_nodes)
+    xmlAddChildList (object_node, type_sub_nodes);
+
+  /* add type node to top node */
+  xmlAddChild (osm_node, object_node);
+  xmlDocSetRootElement (doc, osm_node);
+  
+  return doc;
+}
+
+
+char *
+maps_osm_object_serialize (const MapsOSMObject *object)
+{
+  xmlDocPtr doc;
+  xmlChar *result;
+  int size;
+  
+  doc = maps_osm_object_to_xml (object);
+  xmlDocDumpMemory (doc, &result, &size);
+  xmlFreeDoc (doc);
+  
+  return result;
+}
diff --git a/lib/maps-osm-object.h b/lib/maps-osm-object.h
new file mode 100644
index 0000000..4ff3223
--- /dev/null
+++ b/lib/maps-osm-object.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_OBJECT_H__
+#define __MAPS_OSM_OBJECT_H__
+
+#include <glib-object.h>
+#include <libxml/xpath.h>
+
+#define MAPS_TYPE_OSMOBJECT maps_osm_object_get_type ()
+G_DECLARE_DERIVABLE_TYPE(MapsOSMObject, maps_osm_object, MAPS, OSMOBJECT,
+                        GObject)
+
+typedef struct _MapsOSMObjectPrivate MapsOSMObjectPrivate;
+
+struct _MapsOSMObjectClass
+{
+  GObjectClass parent_class;
+
+  /* return the name of the distinguishing OSM XML tag (beneath <osm/>) */
+  const char * (* get_xml_tag_name) (void);
+
+  /* return hash table with XML attributes (key/values) specific for
+     the object (on the XML tag beneath <osm/>) */
+  GHashTable * (* get_xml_attributes) (const MapsOSMObject *object);
+
+  /* return a list of custom object-specific XML tags to attach,
+     can return NULL if there's no object-specific nodes */
+  xmlNodePtr (* get_xml_child_nodes) (const MapsOSMObject *object);
+};
+
+guint64 maps_osm_object_get_id (MapsOSMObject *object);
+void maps_osm_object_set_id (MapsOSMObject *object, guint64 id);
+guint maps_osm_object_get_version (MapsOSMObject *object);
+void maps_osm_object_set_version (MapsOSMObject *object, guint version);
+guint64 maps_osm_object_get_changeset (MapsOSMObject *object);
+void maps_osm_object_set_changeset (MapsOSMObject *object, guint64 changeset);
+
+const char *maps_osm_object_get_tag (const MapsOSMObject *object,
+                                    const char *key);
+void maps_osm_object_set_tag (MapsOSMObject *object, const char *key,
+                             const char *value);
+void maps_osm_object_delete_tag (MapsOSMObject *object, const char *key);
+
+char *maps_osm_object_serialize (const MapsOSMObject *object);
+
+#endif //__MAPS_OSM_OBJECT_H__
diff --git a/lib/maps-osm-relation.c b/lib/maps-osm-relation.c
new file mode 100644
index 0000000..bc866e6
--- /dev/null
+++ b/lib/maps-osm-relation.c
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm-relation.h"
+
+struct _MapsOSMRelationPrivate
+{
+  GList *members;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (MapsOSMRelation, maps_osm_relation,
+                           MAPS_TYPE_OSMOBJECT);
+
+typedef struct
+{
+  char *role;
+  guint type;
+  guint64 ref;
+} MapsOSMRelationMember;
+
+static void
+maps_osm_relation_member_free (gpointer data)
+{
+  MapsOSMRelationMember *member = (MapsOSMRelationMember *) data;
+
+  g_free (member->role);
+}
+
+static void
+maps_osm_relation_dispose (GObject *object)
+{
+  MapsOSMRelation *relation = MAPS_OSMRELATION (object);
+
+  g_list_free_full (relation->priv->members, maps_osm_relation_member_free);
+  relation->priv->members = NULL;
+
+  G_OBJECT_CLASS (maps_osm_relation_parent_class)->dispose (object);
+}
+
+static const char *
+maps_osm_relation_get_xml_tag_name (void)
+{
+  return "relation";
+}
+
+static const char *
+maps_osm_relation_member_type_to_string (guint type)
+{
+  switch (type) {
+  case MEMBER_TYPE_NODE:
+    return "node";
+  case MEMBER_TYPE_WAY:
+    return "way";
+  case MEMBER_TYPE_RELATION:
+    return "relation";
+  default:
+    g_warning ("Unknown relation member type: %d\n", type);
+    return NULL;
+  }
+}
+
+static xmlNodePtr
+maps_osm_relation_get_member_node (const MapsOSMRelationMember *member)
+{
+  xmlNodePtr node = xmlNewNode (NULL, "member");
+  char buf[16];
+  
+  if (member->role)
+    xmlNewProp (node, "role", g_strdup (member->role));
+
+  xmlNewProp (node, "type",
+             maps_osm_relation_member_type_to_string (member->type));
+  g_snprintf (buf, 16, "%" G_GUINT64_FORMAT, member->ref);
+  xmlNewProp (node, "ref", buf);
+
+  return node;
+}
+
+static xmlNodePtr
+maps_osm_relation_get_xml_child_nodes (const MapsOSMObject *object)
+{
+  MapsOSMRelation *relation = MAPS_OSMRELATION (object);
+  xmlNodePtr nodes = NULL;
+  const GList *members = relation->priv->members;
+  
+  if (members) {
+    const GList *iter;
+    nodes = maps_osm_relation_get_member_node ((MapsOSMRelationMember *)
+                                              members->data);
+
+    for (iter = members->next; iter; iter = iter->next) {
+      xmlAddSibling (nodes,
+                    maps_osm_relation_get_member_node (
+                      (MapsOSMRelationMember *) iter->data));
+    }
+  }
+
+  return nodes;
+}
+
+static void
+maps_osm_relation_class_init (MapsOSMRelationClass *klass)
+{
+  GObjectClass *relation_class = G_OBJECT_CLASS (klass);
+  MapsOSMObjectClass *object_class = MAPS_OSMOBJECT_CLASS (klass);
+
+  relation_class->dispose = maps_osm_relation_dispose;
+  object_class->get_xml_tag_name = maps_osm_relation_get_xml_tag_name;
+  object_class->get_xml_child_nodes = maps_osm_relation_get_xml_child_nodes;
+}
+
+static void
+maps_osm_relation_init (MapsOSMRelation *relation)
+{
+  relation->priv = maps_osm_relation_get_instance_private (relation);
+}
+
+MapsOSMRelation *
+maps_osm_relation_new (guint64 id, guint version, guint64 changeset)
+{
+  return g_object_new (MAPS_TYPE_OSMRELATION,
+                      "id", id,
+                      "version", version,
+                      "changeset", changeset, NULL);
+}
+
+void
+maps_osm_relation_add_member (MapsOSMRelation *relation,
+                             const gchar *role, guint type, guint64 ref)
+{
+  MapsOSMRelationMember *member = g_new (MapsOSMRelationMember, 1);
+
+  member->role = g_strdup (role);
+  member->type = type;
+  member->ref = ref;
+
+  relation->priv->members = g_list_append (relation->priv->members,
+                                          member);
+}
+
+                             
diff --git a/lib/maps-osm-relation.h b/lib/maps-osm-relation.h
new file mode 100644
index 0000000..9e0119d
--- /dev/null
+++ b/lib/maps-osm-relation.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_RELATION_H__
+#define __MAPS_OSM_RELATION_H__
+
+#include "maps-osm-object.h"
+
+#include <glib-object.h>
+
+#define MAPS_TYPE_OSMRELATION maps_osm_relation_get_type ()
+G_DECLARE_FINAL_TYPE(MapsOSMRelation, maps_osm_relation, MAPS, OSMRELATION,
+                    MapsOSMObject)
+
+typedef struct _MapsOSMRelationPrivate MapsOSMRelationPrivate;
+
+struct _MapsOSMRelation
+{
+  MapsOSMObject parent_instance;
+  MapsOSMRelationPrivate *priv;
+};
+
+struct _MapsOSMRelationClass
+{
+  MapsOSMObjectClass parent_class;
+};
+
+enum {
+  MEMBER_TYPE_NODE,
+  MEMBER_TYPE_WAY,
+  MEMBER_TYPE_RELATION
+};
+
+MapsOSMRelation *maps_osm_relation_new (guint64 id, guint version,
+                                       guint64 changeset);
+
+void maps_osm_relation_add_member (MapsOSMRelation *relation,
+                                  const char *role, guint type,
+                                  guint64 ref);
+
+#endif /* __MAPS_OSM_RELATION_H__ */
+
diff --git a/lib/maps-osm-way.c b/lib/maps-osm-way.c
new file mode 100644
index 0000000..841bad6
--- /dev/null
+++ b/lib/maps-osm-way.c
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm-way.h"
+
+struct _MapsOSMWayPrivate
+{
+  GArray *node_ids;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (MapsOSMWay, maps_osm_way,
+                           MAPS_TYPE_OSMOBJECT);
+
+static void
+maps_osm_way_dispose (GObject *object)
+{
+  MapsOSMWay *way = MAPS_OSMWAY (object);
+
+  g_array_free (way->priv->node_ids, TRUE);
+  way->priv->node_ids = NULL;
+
+  G_OBJECT_CLASS (maps_osm_way_parent_class)->dispose (object);
+}
+
+static const char *
+maps_osm_way_get_xml_tag_name (void)
+{
+  return "way";
+}
+
+static xmlNodePtr
+maps_osm_way_create_node_xml_node (guint64 ref)
+{
+  xmlNodePtr nd;
+  char buf[16];
+
+  g_snprintf (buf, 16, "%" G_GUINT64_FORMAT, ref);
+  nd = xmlNewNode (NULL, "nd");
+  xmlNewProp (nd, "ref", buf);
+
+  return nd;
+}
+
+static xmlNodePtr
+maps_osm_way_get_xml_child_nodes(const MapsOSMObject *object)
+{
+  const MapsOSMWay *way = MAPS_OSMWAY (object);
+  int i;
+  xmlNodePtr result;
+  xmlNodePtr next;
+
+  g_return_if_fail (way->priv->node_ids->len > 0);
+
+  result = maps_osm_way_create_node_xml_node (g_array_index (way->priv->node_ids,
+                                                            guint64, 0));
+  next = result;
+  
+  for (i = 1; i < way->priv->node_ids->len; i++) {
+    xmlNodePtr new_node;
+    new_node =
+      maps_osm_way_create_node_xml_node (g_array_index (way->priv->node_ids,
+                                                       guint64, i));
+    next = xmlAddNextSibling (next, new_node);
+  }
+
+  return result;
+}
+
+static void
+maps_osm_way_class_init (MapsOSMWayClass *klass)
+{
+  GObjectClass *way_class = G_OBJECT_CLASS (klass);
+  MapsOSMObjectClass *object_class = MAPS_OSMOBJECT_CLASS (klass);
+  
+  way_class->dispose = maps_osm_way_dispose;
+  object_class->get_xml_tag_name = maps_osm_way_get_xml_tag_name;
+  object_class->get_xml_child_nodes = maps_osm_way_get_xml_child_nodes;
+}
+
+static void
+maps_osm_way_init (MapsOSMWay *way)
+{
+  way->priv = maps_osm_way_get_instance_private (way);
+  way->priv->node_ids = g_array_new (FALSE, FALSE, sizeof (guint64));
+}
+
+MapsOSMWay *
+maps_osm_way_new (guint64 id, guint version, guint64 changeset)
+{
+  return g_object_new (MAPS_TYPE_OSMWAY,
+                      "id", id,
+                      "version", version,
+                      "changeset", changeset, NULL);
+}
+
+void
+maps_osm_way_add_node_id (MapsOSMWay *way, guint64 id)
+{
+  g_array_append_val (way->priv->node_ids, id);
+}
diff --git a/lib/maps-osm-way.h b/lib/maps-osm-way.h
new file mode 100644
index 0000000..2817d3e
--- /dev/null
+++ b/lib/maps-osm-way.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_WAY_H__
+#define __MAPS_OSM_WAY_H__
+
+#include "maps-osm-object.h"
+
+#include <glib-object.h>
+
+#define MAPS_TYPE_OSMWAY maps_osm_way_get_type ()
+G_DECLARE_FINAL_TYPE(MapsOSMWay, maps_osm_way, MAPS, OSMWAY,
+                    MapsOSMObject)
+
+typedef struct _MapsOSMWayPrivate MapsOSMWayPrivate;
+
+struct _MapsOSMWay
+{
+  MapsOSMObject parent_instance;
+  MapsOSMWayPrivate *priv;
+};
+
+struct _MapsOSMWayClass
+{
+  MapsOSMObjectClass parent_class;
+};
+
+MapsOSMWay *maps_osm_way_new (guint64 id, guint version, guint64 changeset);
+
+void maps_osm_way_add_node_id (MapsOSMWay *way, guint64 id);
+
+#endif /* __MAPS_OSM_WAY_H__ */
+
diff --git a/lib/maps-osm.c b/lib/maps-osm.c
new file mode 100644
index 0000000..86dcca6
--- /dev/null
+++ b/lib/maps-osm.c
@@ -0,0 +1,487 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#include "maps-osm.h"
+
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+
+void
+maps_osm_init (void)
+{
+  LIBXML_TEST_VERSION;
+}
+
+void
+maps_osm_finalize (void)
+{
+  xmlCleanupParser ();
+}
+
+static xmlDocPtr
+read_xml_doc (const char *content, guint length)
+{
+  xmlDoc *doc;
+
+  doc = xmlReadMemory (content, length, "noname.xml", NULL, 0);
+
+  if (!doc) {
+    g_error ("Failed to parse to XML document");
+    return NULL;
+  }
+
+  return doc;
+}
+
+static void
+parse_tag (const xmlAttr *attrs, GHashTable *tags)
+{
+  const xmlAttr *cur_attr;
+  char *key;
+  char *value;
+  char *result;
+
+  key = NULL;
+  value = NULL;
+  
+  for (cur_attr = attrs; cur_attr; cur_attr = cur_attr->next) {
+    if (g_str_equal (cur_attr->name, "k")) {
+      key = cur_attr->children->content, "";
+    } else if (g_str_equal (cur_attr->name, "v")) {
+      value = cur_attr->children->content, "";
+    } else {
+      g_warning ("Unexpected tag property: %s\n", cur_attr->name);
+    }
+  }
+
+  g_hash_table_insert (tags, key, value);
+}
+
+static GHashTable *
+parse_attributes (const xmlNode *node)
+{
+  GHashTable *attributes;
+  const xmlAttr *cur_attr;
+  
+  attributes = g_hash_table_new (g_str_hash, g_str_equal);
+
+  for (cur_attr = node->properties; cur_attr; cur_attr = cur_attr->next) {
+    g_hash_table_insert (attributes,
+                        (gpointer) cur_attr->name,
+                        (gpointer) cur_attr->children->content);
+  }
+
+  return attributes;
+}
+
+static GHashTable *
+parse_tags (const xmlNode *tag_child)
+{
+  GHashTable *tags;
+  const xmlNode *cur_node;
+  
+  tags = g_hash_table_new (g_str_hash, g_str_equal);
+
+  for (cur_node = tag_child; cur_node; cur_node = cur_node->next) {
+    /* skip non-element nodes */
+    if (cur_node->type != XML_ELEMENT_NODE) {
+      continue;
+    }
+
+    if (g_str_equal (cur_node->name, "tag")) {
+      char *tag;
+
+      parse_tag (cur_node->properties, tags);
+    }
+  }
+  
+  return tags;
+}
+
+static GArray *
+parse_node_refs (const xmlNode *node_ref_child)
+{
+  GArray *node_refs;
+  const xmlNode *cur_node;
+
+  node_refs = g_array_new (FALSE, FALSE, sizeof (guint64));
+
+  for (cur_node = node_ref_child; cur_node; cur_node = cur_node->next) {
+    /* skip non-element nodes */
+    if (cur_node->type != XML_ELEMENT_NODE) {
+      continue;
+    }
+
+    if (g_str_equal (cur_node->name, "nd")) {
+      char *ref;
+      GHashTable *attributes;
+
+      attributes = parse_attributes (cur_node);
+      ref = g_hash_table_lookup (attributes, "ref");
+
+      if (ref) {
+       guint64 id = g_ascii_strtoull (ref, NULL, 10);
+
+       if (id == 0)
+         g_warning ("Invalid node ref: %s", ref);
+       else
+         g_array_append_val (node_refs, id);
+      }
+
+      g_hash_table_destroy (attributes);
+    }
+  }
+
+  return node_refs;
+}
+
+static xmlNode *
+get_sub_node (xmlDoc *doc, const char *name)
+{
+  xmlNode *node;
+  xmlXPathContext *xpath_ctx;
+  xmlXPathObject * xpath_obj;
+  char *xpath;
+
+  xpath = g_strdup_printf ("/osm/%s", name);
+  xpath_ctx = xmlXPathNewContext (doc);
+  xpath_obj = xmlXPathEvalExpression (xpath, xpath_ctx);
+
+  if (xpath_obj && xpath_obj->nodesetval && xpath_obj->nodesetval->nodeNr > 0) {
+    node = xmlCopyNode (xpath_obj->nodesetval->nodeTab[0], 1);
+  } else {
+    g_warning ("Couldn't find element %s\n", name);
+    node = NULL;
+  }
+
+  xmlXPathFreeObject (xpath_obj);
+  xmlXPathFreeContext (xpath_ctx);
+  g_free (xpath);
+
+  return node;
+}
+
+static void
+for_each_tag (gpointer key, gpointer value, gpointer user_data)
+{
+  const char *k = (const char *) key;
+  const char *v = (const char *) value;
+  MapsOSMObject *object = MAPS_OSMOBJECT (user_data);
+
+  maps_osm_object_set_tag (object, k, v);
+}
+
+static void
+fill_tags (MapsOSMObject *object, GHashTable *tags)
+{
+  g_hash_table_foreach (tags, for_each_tag, object);
+}
+
+static void
+fill_node_ref_list (MapsOSMWay *way, const GArray *node_refs)
+{
+  int i;
+  
+  for (i = 0; i < node_refs->len; i++) {
+    maps_osm_way_add_node_id (way, g_array_index (node_refs, guint64, i));
+  }
+}
+
+/**
+ * maps_osm_parse_node:
+ * @content: XML data
+ * @length: Length of data
+ * Returns: (transfer full): An OSMNode
+ */
+MapsOSMNode *
+maps_osm_parse_node (const char *content, guint length)
+{
+  xmlDoc *doc;
+  xmlNode *node;
+
+  const char *id_string;
+  guint64 id;
+  const char *changeset_string;
+  guint64 changeset;
+  const char *version_string;
+  guint version;
+  const char *lat_string;
+  double lat;
+  const char *lon_string;
+  double lon;
+  
+  const xmlAttr *cur_attr;
+
+  GHashTable *tags;
+  GHashTable *attributes;
+
+  MapsOSMNode *result;
+  
+  doc = read_xml_doc (content, length);
+
+  if (!doc) {
+    return NULL;
+  }
+
+  node = get_sub_node (doc, "node");
+
+  if (!node) {
+    g_error ("Missing <node/> element");
+    xmlFreeDoc (doc);
+    return NULL;
+  }
+
+  attributes = parse_attributes (node);
+  
+  id_string = g_hash_table_lookup (attributes, "id");
+  changeset_string = g_hash_table_lookup (attributes, "changeset");
+  version_string = g_hash_table_lookup (attributes, "version");
+  lat_string = g_hash_table_lookup (attributes, "lat");
+  lon_string = g_hash_table_lookup (attributes, "lon");
+    
+  if (!id_string || !changeset_string || !version_string
+      || !lat_string || !lon_string) {
+    g_warning ("Missing required attributes\n");
+    xmlFreeDoc (doc);
+    xmlFreeNode (node);
+    g_hash_table_destroy (attributes);
+    return NULL;
+  }
+
+  id = g_ascii_strtoull (id_string, NULL, 10);
+  changeset = g_ascii_strtoull (changeset_string, NULL, 10);
+  version = g_ascii_strtoull (version_string, NULL, 10);
+  lon = g_ascii_strtod (lon_string, NULL);
+  lat = g_ascii_strtod (lat_string, NULL);
+
+  g_hash_table_destroy (attributes);
+
+  result = maps_osm_node_new (id, version, changeset, lon, lat);
+
+  tags = parse_tags (node->children);
+  fill_tags (MAPS_OSMOBJECT (result), tags);
+
+  g_hash_table_destroy (tags);
+
+  xmlFreeDoc (doc);
+  xmlFreeNode (node);
+
+  return result;
+}
+
+/**
+ * maps_osm_parse_way:
+ * @content: XML data
+ * @length: Length of data
+ * Returns: (transfer full): An OSMWay
+ */
+MapsOSMWay *
+maps_osm_parse_way (const char *content, guint length)
+{
+  xmlDoc *doc;
+  xmlNode *way;
+  GHashTable *attributes;
+  GHashTable *tags;
+  GArray *node_refs;
+  MapsOSMWay *result;
+  
+  const char *id_string;
+  guint64 id;
+  const char *changeset_string;
+  guint64 changeset;
+  const char *version_string;
+  guint version;
+  
+  doc = read_xml_doc (content, length);
+
+  if (!doc) {
+    return NULL;
+  }
+
+  way = get_sub_node (doc, "way");
+
+  if (!way) {
+    xmlFreeDoc (doc);
+    return NULL;
+  }
+
+  attributes = parse_attributes (way);
+  
+  id_string = g_hash_table_lookup (attributes, "id");
+  changeset_string = g_hash_table_lookup (attributes, "changeset");
+  version_string = g_hash_table_lookup (attributes, "version");
+
+  if (!id_string || !changeset_string || !version_string) {
+    g_warning ("Missing required attributes\n");
+    xmlFreeDoc (doc);
+    xmlFreeNode (way);
+    g_hash_table_destroy (attributes);
+    return NULL;
+  }
+
+  g_hash_table_destroy (attributes);
+
+  id = g_ascii_strtoull (id_string, NULL, 10);
+  changeset = g_ascii_strtoull (changeset_string, NULL, 10);
+  version = g_ascii_strtoull (version_string, NULL, 10);
+
+  result = maps_osm_way_new (id, version, changeset);
+
+  tags = parse_tags (way->children);
+  fill_tags (MAPS_OSMOBJECT (result), tags);
+  g_hash_table_destroy (tags);
+
+  node_refs = parse_node_refs (way->children);
+  fill_node_ref_list (result, node_refs);
+  g_array_free (node_refs, TRUE);
+
+  xmlFreeDoc (doc);
+  xmlFreeNode (way);
+
+  return result;
+}
+
+
+static GList *
+parse_members (const xmlNode *member_child)
+{
+  const xmlNode *cur_node;
+  GList *members;
+
+  members = NULL;
+
+  for (cur_node = member_child; cur_node; cur_node = cur_node->next) {
+    /* skip non-element nodes */
+    if (cur_node->type != XML_ELEMENT_NODE) {
+      continue;
+    }
+
+    if (g_str_equal (cur_node->name, "member")) {
+      GHashTable *attributes;
+
+      attributes = parse_attributes (cur_node);
+      members = g_list_append (members, attributes);
+    }
+  }
+  
+  return members;
+}
+
+static void
+fill_members (MapsOSMRelation *relation, const GList *members)
+{
+  const GList *cur;
+
+  for (cur = members; cur; cur = g_list_next (cur)) {
+    GHashTable *attributes = (GHashTable *) cur->data;
+    const char *type_string = g_hash_table_lookup (attributes, "type");
+    guint type;
+    const char *role = g_hash_table_lookup (attributes, "role");
+    const char *ref_string = g_hash_table_lookup (attributes, "ref");
+    guint64 ref;
+    
+    if (ref_string)
+      ref = g_ascii_strtoull (ref_string, NULL, 10);
+
+    if (g_strcmp0 (type_string, "node") == 0) {
+      type = MEMBER_TYPE_NODE;
+    } else if (g_strcmp0 (type_string, "way") == 0) {
+      type = MEMBER_TYPE_WAY;
+    } else if (g_strcmp0 (type_string, "relation") == 0) {
+      type = MEMBER_TYPE_RELATION;
+    } else {
+      g_warning ("Unknown relation type: %s\n", type_string);
+      continue;
+    }
+    
+    maps_osm_relation_add_member (relation, role, type, ref);
+  }
+}
+
+/**
+ * maps_osm_parse_relation:
+ * @content: XML data
+ * @length: Length of data
+ * Returns: (transfer full): An OSMRelation
+ */
+MapsOSMRelation *
+maps_osm_parse_relation (const char *content, guint length)
+{
+  xmlDoc *doc;
+  xmlNode *relation;
+  GHashTable *attributes;
+  GHashTable *tags;
+  GList *member_list;
+  
+  const char *id_string;
+  guint64 id;
+  const char *changeset_string;
+  guint64 changeset;
+  const char *version_string;
+  guint version;
+  
+  MapsOSMRelation *result;
+  
+  doc = read_xml_doc (content, length);
+
+  if (!doc) {
+    return NULL;
+  }
+
+  relation = get_sub_node (doc, "relation");
+
+  if (!relation) {
+    xmlFreeDoc (doc);
+    return NULL;
+  }
+
+  attributes = parse_attributes (relation);
+  id_string = g_hash_table_lookup (attributes, "id");
+  changeset_string = g_hash_table_lookup (attributes, "changeset");
+  version_string = g_hash_table_lookup (attributes, "version");  
+  
+  if (!id_string || !changeset_string || !version_string) {
+    g_error ("Missing required attributes\n");
+    xmlFreeDoc (doc);
+    xmlFreeNode (relation);
+    g_hash_table_destroy (attributes);
+    return NULL;
+  }
+
+  g_hash_table_destroy (attributes);
+
+  id = g_ascii_strtoull (id_string, NULL, 10);
+  changeset = g_ascii_strtoull (changeset_string, NULL, 10);
+  version = g_ascii_strtoull (version_string, NULL, 10);
+
+  result = maps_osm_relation_new (id, version, changeset);
+
+  tags = parse_tags (relation->children);
+  fill_tags (MAPS_OSMOBJECT (result), tags);
+  g_hash_table_destroy (tags);
+
+  member_list = parse_members (relation->children);
+  fill_members (result, member_list);
+  g_list_free_full (member_list, (GDestroyNotify) g_hash_table_destroy);
+
+  xmlFreeDoc (doc);
+  xmlFreeNode (relation);
+
+  return result;
+}
diff --git a/lib/maps-osm.h b/lib/maps-osm.h
new file mode 100644
index 0000000..d9b84bb
--- /dev/null
+++ b/lib/maps-osm.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, see <http://www.gnu.org/licenses/>
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+#ifndef __MAPS_OSM_H__
+#define __MAPS_OSM_H__
+
+#include <glib.h>
+
+#include "maps-osm-node.h"
+#include "maps-osm-way.h"
+#include "maps-osm-relation.h"
+
+void maps_osm_init (void);
+void maps_osm_finalize (void);
+
+MapsOSMNode *maps_osm_parse_node (const char *content, guint length);
+MapsOSMWay *maps_osm_parse_way (const char *content, guint length);
+MapsOSMRelation *maps_osm_parse_relation (const char *content, guint length);
+
+#endif
diff --git a/src/application.js b/src/application.js
index 2a6ca2c..9148537 100644
--- a/src/application.js
+++ b/src/application.js
@@ -35,6 +35,7 @@ const GeocodeService = imports.geocodeService;
 const MainWindow = imports.mainWindow;
 const Maps = imports.gi.GnomeMaps;
 const NotificationManager = imports.notificationManager;
+const OSMEdit = imports.osmEdit;
 const PlaceStore = imports.placeStore;
 const RouteService = imports.routeService;
 const Settings = imports.settings;
@@ -51,6 +52,7 @@ let geocodeService = null;
 let networkMonitor = null;
 let checkInManager = null;
 let contactStore = null;
+let osmEditManager = null;
 
 const Application = new Lang.Class({
     Name: 'Application',
@@ -224,6 +226,7 @@ const Application = new Lang.Class({
         checkInManager = new CheckIn.CheckInManager();
         contactStore = new Maps.ContactStore();
         contactStore.load();
+       osmEditManager = new OSMEdit.OSMEditManager();
     },
 
     _createWindow: function() {
diff --git a/src/mapBubble.js b/src/mapBubble.js
index 0d67414..dd15b24 100644
--- a/src/mapBubble.js
+++ b/src/mapBubble.js
@@ -37,7 +37,8 @@ const Button = {
     ROUTE: 2,
     SEND_TO: 4,
     FAVORITE: 8,
-    CHECK_IN: 16
+    CHECK_IN: 16,
+    EDIT: 32
 };
 
 const MapBubble = new Lang.Class({
@@ -75,9 +76,11 @@ const MapBubble = new Lang.Class({
                                                    'bubble-route-button',
                                                    'bubble-send-to-button',
                                                    'bubble-favorite-button',
-                                                   'bubble-check-in-button']);
+                                                   'bubble-check-in-button',
+                                                   'bubble-edit-button']);
         this._image = ui.bubbleImage;
         this._content = ui.bubbleContentArea;
+        this._editButton = ui.bubbleEditButton;
 
         if (!buttonFlags)
             ui.bubbleButtonArea.visible = false;
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index a15ac7e..dc02a83 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -27,6 +27,10 @@
     <file>mapWalker.js</file>
     <file>notification.js</file>
     <file>notificationManager.js</file>
+    <file>osmConnection.js</file>
+    <file>osmEdit.js</file>
+    <file>osmEditDialog.js</file>
+    <file>osmUtils.js</file>
     <file>overpass.js</file>
     <file>place.js</file>
     <file>placeBubble.js</file>
diff --git a/src/osmConnection.js b/src/osmConnection.js
new file mode 100644
index 0000000..7e3f112
--- /dev/null
+++ b/src/osmConnection.js
@@ -0,0 +1,301 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const _ = imports.gettext.gettext;
+
+const Utils = imports.utils;
+
+const Lang = imports.lang;
+const GLib = imports.gi.GLib;
+const Maps = imports.gi.GnomeMaps;
+const Soup = imports.gi.Soup;
+
+const BASE_URL = 'https://api.openstreetmap.org/api';
+const TEST_BASE_URL = 'http://api06.dev.openstreetmap.org/api';
+const API_VERSION = '0.6';
+
+const OSMConnection = new Lang.Class({
+    Name: 'OSMConnection',
+
+    _init: function(params) {
+       this._session = new Soup.Session();
+
+       // TODO: stopgap to supply username/password
+       // to use with HTTP basic auth, should be
+       // replaced with OAUTH and real settings or GOA account
+       this._username = GLib.getenv('OSM_USERNAME');
+       this._password = GLib.getenv('OSM_PASSWORD');
+
+       let useLiveApi = GLib.getenv('OSM_USE_LIVE_API');
+
+       // for now use the test API unless explicitly
+       // set to use the live one
+       if (useLiveApi == 'yes')
+           this._useTestApi = false;
+       else
+           this._useTestApi = true;
+
+       this._session.connect('authenticate', this._authenticate.bind(this));
+       
+       Maps.osm_init();
+    },
+
+    get useTestAPI() {
+       return this._useTestApi;
+    },
+
+    getOSMObject: function(type, id, callback, cancellable) {
+       let url = this._getQueryUrl(type, id);
+       let uri = new Soup.URI(url);
+       let request = new Soup.Message({ method: 'GET',
+                                        uri: uri });
+
+       cancellable.connect((function() {
+           this._session.cancel_message(request, Soup.STATUS_CANCELLED);
+       }).bind(this));
+       
+       this._session.queue_message(request, (function(obj, message) {
+           if (message.status_code !== Soup.Status.OK) {
+                callback(false, message.status_code, null);
+                return;
+            }
+
+            Utils.debug ('data received: ' + message.response_body.data);
+
+           // override object type to use the mock object if using the test API
+           if (this._useTestApi)
+               type = GLib.getenv('OSM_MOCK_TYPE');
+           
+           let object = this._parseXML(type, message.response_body);
+
+           if (object == null)
+               callback(false, message.status_code, null, type);
+           else
+               callback(true,
+                        message.status_code,
+                        object, type);
+        }).bind(this));
+    },
+    
+    _getQueryUrl: function(type, id) {
+       if (this._useTestApi) {
+           // override object type and ID from a mock object
+           // since the object we get from Nominatim and Overpass
+           // doesn't exist in the test OSM environment
+           type = GLib.getenv('OSM_MOCK_TYPE');
+           id = GLib.getenv('OSM_MOCK_ID');
+       }
+       
+       return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+    },
+
+    _getBaseUrl: function() {
+       return this._useTestApi ? TEST_BASE_URL : BASE_URL;
+    },
+
+    _parseXML: function(type, body) {
+       let object;
+       
+       switch (type) {
+       case 'node':
+           object = Maps.osm_parse_node(body.data, body.length);
+           break;
+       case 'way':
+           object = Maps.osm_parse_way(body.data, body.length);
+            break;
+        case 'relation':
+            object = Maps.osm_parse_relation(body.data, body.length);
+           break;
+        default:
+            GLib.error('unknown OSM type: ' + type);
+       }
+
+       return object;
+    },
+
+    openChangeset: function(comment, source, callback) {
+       let changeset =
+           Maps.OSMChangeset.new(comment, source, 'gnome-maps ' + pkg.version);
+       let xml = changeset.serialize();
+
+       Utils.debug('about open changeset:\n' + xml + '\n');
+
+       let url = this._getOpenChangesetUrl();
+       let uri = new Soup.URI(url);
+       let msg = new Soup.Message({ method: 'PUT',
+                                        uri: uri });
+       msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+       
+       this._session.queue_message(msg, (function(obj, message) {
+           if (message.status_code !== Soup.Status.OK) {
+                callback(false, message.status_code, null);
+                return;
+            }
+
+           let changesetId = GLib.ascii_strtoull (message.response_body.data,
+                                                  '', 10);
+           callback(true, message.status_code, changesetId);
+           
+        }));
+    },
+
+    uploadObject: function(object, type, changeset, callback) {
+       object.changeset = changeset;
+
+       let xml = object.serialize();
+
+       Utils.debug('about to upload object:\n' + xml + '\n');
+
+       let url = this._getCreateOrUpdateUrl(object, type);
+       let uri = new Soup.URI(url);
+       let msg = new Soup.Message({ method: 'PUT',
+                                    uri: uri });
+       msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+
+       this._session.queue_message(msg, (function(obj, message) {
+           if (message.status_code !== Soup.Status.OK) {
+                callback(false, message.status_code, null);
+                return;
+            }
+
+           callback(true, message.status_code, message.response_body.data);
+        }));
+    },
+
+    deleteObject: function(object, type, changeset, callback) {
+       object.changeset = changeset;
+       let xml = object.serialize();
+
+       Utils.debug('about to delete object:\n' + xml + '\n');
+       
+       let url = this._getDeleteUrl(object, type);
+       let uri = new Soup.URI(url);
+       let msg = new Soup.Message({ method: 'DELETE',
+                                    uri: uri });
+
+       Utils.debug('calling delete URL: ' + url);
+       
+       msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+
+       this._session.queue_message(msg, (function(obj, message) {
+           if (message.status_code !== Soup.Status.OK) {
+                callback(false, message.status_code, null);
+                return;
+            }
+
+           callback(true, message.status_code, message.response_body.data);
+        }));
+    },
+
+    closeChangeset: function(changesetId, callback) {
+       let url = this._getCloseChangesetUrl(changesetId);
+       let uri = new Soup.URI(url);
+       let msg = new Soup.Message({ method: 'PUT',
+                                    uri: uri });
+
+       this._session.queue_message(msg, (function(obj, message) {
+           if (message.status_code !== Soup.Status.OK) {
+                callback(false, message.status_code);
+                return;
+            }
+
+           callback(true, message.status_code);
+        }));
+    },
+
+    _getOpenChangesetUrl: function() {
+       return this._getBaseUrl() + '/' + API_VERSION + '/changeset/create';
+    },
+
+    _getCloseChangesetUrl: function(changesetId) {
+       return this._getBaseUrl() + '/' + API_VERSION + '/changeset/' +
+           changesetId + '/close';
+    },
+ 
+    _getCreateOrUpdateUrl: function(object, type) {
+       if (object.id) {
+           let id = object.id;
+           if (this._useTestApi) {
+               // override object type and ID from a mock object
+               // since the object we get from Nominatim and Overpass
+               // doesn't exist in the test OSM environment
+               type = GLib.getenv('OSM_MOCK_TYPE');
+               id = GLib.getenv('OSM_MOCK_ID');
+           }
+
+           return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+       } else {
+           return this._getBaseUrl() + '/' + API_VERSION + '/' + type +
+               '/create';
+       }
+    },
+
+    _getDeleteUrl: function(object, type) {
+       let id = object.id;
+
+       if (this._useTestApi) {
+           // override object type and ID from a mock object
+           // since the object we get from Nominatim and Overpass
+           // doesn't exist in the test OSM environment
+           type = GLib.getenv('OSM_MOCK_TYPE');
+           id = GLib.getenv('OSM_MOCK_ID');
+       }
+
+       return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+    },
+
+    _authenticate: function(session, msg, auth, retrying, user_data) {
+       if (retrying)
+           session.cancel_message(msg, Soup.Status.UNAUTHORIZED);
+
+       auth.authenticate(this._username, this._password);
+    }
+});
+
+/*
+ * Gets a status message (usually for an error case)
+ * to show for a given OSM server response.
+ */
+function getStatusMessage(statusCode) {
+    switch (statusCode) {
+    case Soup.Status.IO_ERROR:
+    case Soup.Status.UNAUTHORIZED:
+       // setting the status in session.cancel_message still seems
+       // to always give status IO_ERROR
+       return _("Incorrect user name or password");
+    case Soup.Status.OK:
+       return _("Success");
+    case Soup.Status.BAD_REQUEST:
+       return _("Bad request");
+    case Soup.Status.NOT_FOUND:
+       return _("Object not found");
+    case Soup.Status.CONFLICT:
+       return _("Conflict, someone else has just modified the object");
+    case Soup.Status.GONE:
+       return _("Object has been deleted");
+    case Soup.Status.PRECONDITION_FAILED:
+       return _("Way or relation refers to non-existing children");
+    default:
+       return null;
+    }
+}
+
diff --git a/src/osmEdit.js b/src/osmEdit.js
new file mode 100644
index 0000000..6c24704
--- /dev/null
+++ b/src/osmEdit.js
@@ -0,0 +1,144 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+
+const OSMEditDialog = imports.osmEditDialog;
+const OSMConnection = imports.osmConnection;
+const Utils = imports.utils;
+
+const OSMEditManager = new Lang.Class({
+    Name: 'OSMEditManager',
+    Extends: GObject.Object,
+
+    _init: function() {
+       this._osmConnection = new OSMConnection.OSMConnection();
+       this._osmObject = null; // currently edited object
+    },
+
+    get useTestApi() {
+       return this._osmConnection.useTestApi;
+    },
+
+    get osmObject() {
+       return this._osmObject;
+    },
+
+    showEditDialog: function(parentWindow, place) {
+       let dialog = new OSMEditDialog.OSMEditDialog( { transient_for: parentWindow,
+                                                       place: place });
+       let response = dialog.run();
+       dialog.destroy();
+       return response;
+    },
+
+    fetchObject: function(place, callback, cancellable) {
+       let osmType = this._getOSMTypeName(place.osmType);
+
+       // reset currenly edited object
+       this._osmObject = null;
+       this._osmConnection.getOSMObject(osmType, place.osm_id,
+                                        (function(success, status, osmObject,
+                                                  osmType) {
+                                            callback(success, status,
+                                                     osmObject, osmType);
+                                        }), cancellable);
+
+    },
+
+    _getOSMTypeName: function(placeType) {
+       let osmType;
+
+       switch (placeType) {
+       case 1:
+           osmType = 'node';
+           break;
+       case 2:
+           osmType = 'relation';
+           break;
+       case 3:
+           osmType = 'way';
+           break;
+       default:
+           Utils.debug ('Unknown OSM type: ' + placeType);
+           break;
+       }
+
+       return osmType;
+    },
+
+    uploadObject: function(object, type, comment, source, callback) {
+       this._openChangeset(object, type, comment, source, this._uploadObject.bind(this),
+                           callback);
+    },
+
+    _openChangeset: function(object, type, comment, source, action, callback) {
+       this._osmConnection.openChangeset(comment, source,
+                                         function(success, status,
+                                                  changesetId) {
+                                             if (success) {
+                                                 let osmType = this._getOSMTypeName(type);
+                                                 action(object, osmType, changesetId,
+                                                        callback);
+                                             } else {
+                                                 callback(false, status);
+                                             }
+                                         }.bind(this));
+
+    },
+    
+    _uploadObject: function(object, type, changesetId, callback) {
+       this._osmObject = object;
+       this._osmConnection.uploadObject(object, type, changesetId,
+                                        function(success, status,
+                                                 response) {
+                                            if (success)
+                                                this._closeChangeset(changesetId,
+                                                                     callback);
+                                            else
+                                                callback(false, status);
+                                        }.bind(this));
+    },
+
+    deleteObject: function(object, type, comment, source, callback) {
+       this._openChangeset(object, type, comment, source, this._deleteObject.bind(this),
+                           callback);
+    },
+
+    _deleteObject: function(object, type, changesetId, callback) {
+       this._osmObject = object;
+       this._osmConnection.deleteObject(object, type, changesetId,
+                                        function(success, status,
+                                                 response) {
+                                            if (success)
+                                                this._closeChangeset(changesetId,
+                                                                     callback);
+                                            else
+                                                callback(false, status);
+                                        }.bind(this));
+    },
+
+    _closeChangeset: function(changesetId, callback) {
+       this._osmConnection.closeChangeset(changesetId, callback);
+    }
+});
diff --git a/src/osmEditDialog.js b/src/osmEditDialog.js
new file mode 100644
index 0000000..071c762
--- /dev/null
+++ b/src/osmEditDialog.js
@@ -0,0 +1,382 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const _ = imports.gettext.gettext;
+
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const Application = imports.application;
+const OSMConnection = imports.osmConnection;
+const OSMUtils = imports.osmUtils;
+
+const Response = {
+    UPLOADED: 0,
+    DELETED: 1,
+    CANCELLED: 2,
+    ERROR: 3
+};
+
+// enumeration representing
+// the different OSM editing
+// field types
+const EditFieldType = {
+    // plain text
+    TEXT: 0,
+    // integer value (e.g. population)
+    INTEGER: 1,
+    // selection of yes|no|limited|designated
+    YES_NO_LIMITED_DESIGNATED: 2
+}
+
+// specification of OSM edit fields
+// name: the label for the edit field (translatable)
+// tag: the OSM tag key value
+// type: the field type (determines editing field type)
+// rewriteFunc: a rewrite function taking a string argument (only used for TEXT fields)
+const _osmFields = [{name: _("Name"), tag: 'name', type: EditFieldType.TEXT},
+                   {name: _("Wikipedia"), tag: 'wikipedia', type: EditFieldType.TEXT,
+                    rewriteFunc: this._osmWikipediaRewriteFunc},
+                   {name: _("Population"), tag: 'population',
+                    type: EditFieldType.INTEGER},
+                   {name: _("Wheelchair access"), tag: 'wheelchair',
+                    type: EditFieldType.YES_NO_LIMITED_DESIGNATED}]
+
+function _osmWikipediaRewriteFunc(text) {
+    let wikipediaArticleFormatted =
+       OSMUtils.getWikipediaOSMArticleFormatFromUrl(text);
+    
+    // if the entered text is a Wikipedia link,
+    // substitute it with the OSM-formatted Wikipedia article tag
+    if (wikipediaArticleFormatted)
+       return wikipediaArticleFormatted;
+    else
+       return text;
+}
+
+const OSMEditDialog = new Lang.Class({
+    Name: 'OSMEditDialog',
+    Extends: Gtk.Dialog,
+    Template: 'resource:///org/gnome/Maps/ui/osm-edit-dialog.ui',
+    InternalChildren: [ 'cancelButton',
+                       'backButton',
+                       'nextButton',
+                       'stack',
+                       'editorGrid',
+                       'commentTextView',
+                       'addFieldPopoverBox',
+                       'addFieldButton'],
+    
+    _init: function(params) {
+       this._place = params.place;
+       delete params.place;
+
+       // This is a construct-only property and cannot be set by GtkBuilder
+        params.use_header_bar = true;
+
+       this.parent(params);
+
+       this._cancellable = new Gio.Cancellable();
+       this._cancellable.connect((function() {
+            this.response(Response.CANCELLED);
+        }).bind(this));
+
+        this.connect('delete-event', (function() {
+            this._cancellable.cancel();
+        }).bind(this));
+
+       this._isEditing = false;
+       this._nextButton.connect('clicked', this._onNextClicked.bind(this));
+       this._cancelButton.connect('clicked', this._onCancelClicked.bind(this));
+       this._backButton.connect('clicked', this._onBackClicked.bind(this));
+       
+       Application.osmEditManager.fetchObject(this._place,
+                                              this._fetchOSMObjectCB.bind(this),
+                                              this._cancellable);
+    },
+
+    _onNextClicked: function() {
+       if (this._isEditing) {
+           // switch to the upload view
+           this._switchToUpload();
+       } else {
+           // upload data to OSM
+           let comment = this._commentTextView.buffer.text;
+           Application.osmEditManager.uploadObject(this._osmObject,
+                                                   this._place.osmType,
+                                                   comment,
+                                                   null,
+                                                   this._uploadOSMObjectCB.bind(this));
+       }
+    },
+
+    _switchToUpload: function() {
+       this._stack.set_visible_child_name('upload');
+       this._nextButton.label = _("Done");
+       this._cancelButton.visible = false;
+       this._backButton.visible = true;
+       this._cancelButton
+       this._isEditing = false;
+    },
+
+    _onCancelClicked: function() {
+       this.response(Response.CANCELLED);
+    },
+
+    _onBackClicked: function() {
+       this._backButton.visible = false;
+       this._cancelButton.visible = true;
+       this._stack.set_visible_child_name('editor');
+       this._isEditing = true;
+       this._commentTextView.buffer.text = '';
+    },
+    
+    _fetchOSMObjectCB: function(success, status, osmObject, osmType) {
+       if (success) {
+           this._isEditing = true;
+           this._loadOSMData(osmObject);
+       } else {
+           this._showError(status);
+       }
+    },
+
+    _uploadOSMObjectCB: function(success, status) {
+       if (success) {
+           this.response(Response.UPLOADED);
+       } else {
+           this._showError(status);
+           this.response(Response.ERROR);
+       }   
+    },
+
+    _showError: function(status) {
+       let statusMessage = OSMConnection.getStatusMessage(status);
+       let messageDialog =
+           new Gtk.MessageDialog({ transient_for: this.get_toplevel(),
+                                   destroy_with_parent: true,
+                                    message_type: Gtk.MessageType.ERROR,
+                                    buttons: Gtk.ButtonsType.OK,
+                                    modal: true,
+                                    text: _("An error has occurred"),
+                                    secondary_text: statusMessage });
+
+       messageDialog.run();
+        messageDialog.destroy();
+       this.response(Response.ERROR);
+    },
+
+    // GtkContainer.child_get_property doesn't seem to be usable from GJS
+    _getRowOfDeleteButton: function(button) {
+       for (let row = 0;; row++) {
+           let label = this._editorGrid.get_child_at(0, row);
+           let deleteButton = this._editorGrid.get_child_at(2, row);
+
+           if (deleteButton === button)
+               return row;
+
+           // if we reached the end of the table
+           if (label == null)
+               return -1;
+       }
+    },
+    
+    _addOSMEditDeleteButton: function(tag) {
+       let deleteButton = Gtk.Button.new_from_icon_name('user-trash-symbolic',
+                                                        Gtk.IconSize.BUTTON);
+       let styleContext = deleteButton.get_style_context();
+
+       styleContext.add_class('flat');
+       this._editorGrid.attach(deleteButton, 2, this._nextRow, 1, 1);
+
+       deleteButton.connect('clicked', (function() {
+           this._osmObject.delete_tag(tag);
+
+           let row = this._getRowOfDeleteButton(deleteButton);
+           this._editorGrid.remove_row(row);
+           this._nextButton.sensitive = true;
+           this._nextRow--;
+           this._updateAddFieldMenu();
+       }).bind(this, tag));
+
+       deleteButton.show();
+    },
+
+    _addOSMEditLabel: function(text) {
+       let label = new Gtk.Label({label: text});
+       label.halign = Gtk.Align.END;
+       label.get_style_context().add_class('dim-label');
+       this._editorGrid.attach(label, 0, this._nextRow, 1, 1);
+       label.show();
+    },
+    
+    _addOSMEditTextEntry: function(text, tag, value, rewriteFunc) {
+       this._addOSMEditLabel(text);
+       
+       let entry = new Gtk.Entry();
+       entry.text = value;
+       entry.hexpand = true;
+       
+       entry.connect('changed', (function() {
+           if (rewriteFunc)
+               entry.text = rewriteFunc(entry.text);
+           this._osmObject.set_tag(tag, entry.text);
+           this._nextButton.sensitive = true;
+       }).bind(this, tag, entry));
+
+       this._editorGrid.attach(entry, 1, this._nextRow, 1, 1);
+       entry.show();
+
+       // TODO: should we allow deleting the name field?
+       this._addOSMEditDeleteButton(tag);
+       
+       this._nextRow++;
+    },
+
+    _addOSMEditIntegerEntry: function(text, tag, value) {
+       this._addOSMEditLabel(text);
+       
+       let spinbutton = Gtk.SpinButton.new_with_range(0, 1e9, 1);
+       spinbutton.value = value;
+       spinbutton.numeric = true;
+       spinbutton.hexpand = true;
+       spinbutton.connect('changed', (function() {
+           this._osmObject.set_tag(tag, spinbutton.text);
+           this._nextButton.sensitive = true;
+       }).bind(this, tag, spinbutton));
+       
+       this._editorGrid.attach(spinbutton, 1, this._nextRow, 1, 1);
+       spinbutton.show();
+       
+       this._addOSMEditDeleteButton(tag);
+
+       this._nextRow++;
+    },
+
+    _addOSMEditYesNoLimitedDesignated: function(text, tag, value) {
+       this._addOSMEditLabel(text);
+
+       let combobox = new Gtk.ComboBoxText();
+
+       combobox.append('yes', _("Yes"));
+       combobox.append('no', _("No"));
+       combobox.append('limited', _("Limited"));
+       combobox.append('designated', _("Designated"));
+       
+       combobox.active_id = value;
+       combobox.hexpand = true;
+       combobox.connect('changed', (function() {
+           this._osmObject.set_tag(tag, combobox.active_id);
+           this._nextButton.sensitive = true;
+       }).bind(this, tag, combobox));
+
+       this._editorGrid.attach(combobox, 1, this._nextRow, 1, 1);
+       combobox.show();
+       
+       this._addOSMEditDeleteButton(tag);
+
+       this._nextRow++;        
+    },
+
+    // update visible items in the "Add Field" popover
+    _updateAddFieldMenu: function() {
+       // clear old items
+       let children = this._addFieldPopoverBox.get_children();
+       let hasAllFields = true;
+       
+       for (let i = 0; i < children.length; i++) {
+           let button = children[i];
+           button.destroy();
+       }
+
+       for (let i = 0; i < _osmFields.length; i++) {
+           let fieldSpec = _osmFields[i];
+           let label = fieldSpec.name;
+           let tag = fieldSpec.tag;
+           let value = this._osmObject.get_tag(tag);
+           let type = fieldSpec.type;
+           let rewriteFunc = fieldSpec.rewriteFunc;
+
+           if (value == null) {
+               let button = new Gtk.Button({visible: true, sensitive: true,
+                                            label: label});
+               button.get_style_context().add_class('menuitem');
+               button.get_style_context().add_class('button');
+               button.get_style_context().add_class('flat');
+
+               button.connect('clicked', (function() {
+                   this._addFieldButton.active = false;
+                   this._addOSMField(label, tag, '', type, rewriteFunc);
+                   // add a "placeholder" empty OSM tag to keep the add detail menu
+                   // updated, these tags will be filtered out if nothing is entered
+                   this._osmObject.set_tag(tag, '');
+                   this._updateAddFieldMenu();
+               }).bind(this, label, tag, type, rewriteFunc));
+               
+               hasAllFields = false;
+               this._addFieldPopoverBox.add(button);
+           }
+       }
+
+       // update sensitiveness of the add details button, set it as
+       // insensitive if all tags when support editing is already present
+       this._addFieldButton.sensitive = !hasAllFields;
+    },
+
+    _addOSMField: function(label, tag, value, type, rewriteFunc) {
+       switch (type) {
+       case EditFieldType.TEXT:
+           this._addOSMEditTextEntry(label, tag, value, rewriteFunc);
+           break;
+       case EditFieldType.INTEGER:
+           this._addOSMEditIntegerEntry(label, tag, value);
+           break;
+       case EditFieldType.YES_NO_LIMITED_DESIGNATED:
+           this._addOSMEditYesNoLimitedDesignated(label, tag, value);
+           break;
+       }
+
+    },
+    
+    _loadOSMData: function(osmObject, osmType) {
+       this._osmObject = osmObject;
+       this._osmType = osmType;
+
+       // create edit widgets
+       this._nextRow = 0;
+
+       for (let i = 0; i < _osmFields.length; i++) {
+           let fieldSpec = _osmFields[i];
+           let name = fieldSpec.name;
+           let tag = fieldSpec.tag;
+           let type = fieldSpec.type;
+           let rewriteFunc = fieldSpec.rewriteFunc;
+           let value = osmObject.get_tag(tag);
+
+           if (value != null)
+               this._addOSMField(name, tag, value, type, rewriteFunc);
+       }
+
+       this._updateAddFieldMenu();
+       this._stack.visible_child_name = 'editor';
+    }
+});
diff --git a/src/osmUtils.js b/src/osmUtils.js
new file mode 100644
index 0000000..c9d36e4
--- /dev/null
+++ b/src/osmUtils.js
@@ -0,0 +1,57 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Marcus Lundblad <ml update uu se>
+ */
+
+const Soup = imports.gi.Soup;
+
+const Application = imports.application;
+
+/*
+ * Gets a Wikipedia article in OSM tag format (i.e. lang:Article title)
+ * given a URL or null if input doesn't match a Wikipedia URL
+ */
+function getWikipediaOSMArticleFormatFromUrl(url) {
+    let regex = /https?:\/\/(..)\.wikipedia\.org\/wiki\/(.+)/;
+    let match = url.match(regex);
+
+    if (match && match.length == 3) {
+       let lang = match[1];
+       let article = match[2];
+
+       return lang + ':' + Soup.uri_decode(article).replace(/_/g, ' ');
+    } else {
+       return null;
+    }
+}
+
+/**
+ * Updates a Place object according to an OSMObject.
+ * Will also update place in the place store.
+ */
+function updatePlaceFromOSMObject(place, osmObject) {
+    place.name = osmObject.get_tag('name');
+    place.population = osmObject.get_tag('population');
+    place.wiki = osmObject.get_tag('wikipedia');
+    place.openingHours = osmObject.get_tag('opening_hours');
+    place.wheelchair = osmObject.get_tag('wheelchair');
+
+    Application.placeStore.updatePlace(place);
+}
diff --git a/src/placeBubble.js b/src/placeBubble.js
index 98bcce4..badbc15 100644
--- a/src/placeBubble.js
+++ b/src/placeBubble.js
@@ -26,6 +26,8 @@ const Lang = imports.lang;
 const Application = imports.application;
 const ContactPlace = imports.contactPlace;
 const MapBubble = imports.mapBubble;
+const OSMEditDialog = imports.osmEditDialog;
+const OSMUtils = imports.osmUtils;
 const Overpass = imports.overpass;
 const Place = imports.place;
 const PlaceFormatter = imports.placeFormatter;
@@ -41,7 +43,8 @@ const PlaceBubble = new Lang.Class({
                                                      'box-content',
                                                      'label-title']);
         params.buttons = (MapBubble.Button.ROUTE |
-                          MapBubble.Button.SEND_TO);
+                          MapBubble.Button.SEND_TO |
+                         MapBubble.Button.EDIT);
 
         // We do not serialize contacts to file, so adding them
         // as favourites does not makes sense right now.
@@ -79,6 +82,8 @@ const PlaceBubble = new Lang.Class({
             }).bind(this));
         }
         this.content.add(this._stack);
+
+       this._initEditButton(this._editButton);
     },
 
     _formatWikiLink: function(wiki) {
@@ -134,5 +139,43 @@ const PlaceBubble = new Lang.Class({
         }).bind(this));
 
         this._stack.visible_child = this._boxContent;
+    },
+
+    // clear the view widgets to be able to re-polute an updated place
+    _clearView: function() {
+       let widgets = this._boxContent.get_children();
+
+       // remove the dynamically added content, the title label
+       // has position 0 in the box
+       for (let i = 1; i < widgets.length; i++) {
+           this._boxContent.remove(widgets[i]);
+       }
+    },
+
+    _initEditButton: function(button) {
+       // TODO: hook this up with OSM account setting
+       button.visible = true;
+       button.connect('clicked', this._onEditClicked.bind(this));
+    },
+
+    _onEditClicked: function() {
+       let response =
+           Application.osmEditManager.showEditDialog(this.get_toplevel(),
+                                                     this._place);
+
+       switch (response) {
+       case OSMEditDialog.Response.UPLOADED:
+           if (!Application.osmEditManager.useTestApi) {
+               // update place
+               let osmObject = Application.osmEditManager.osmObject;
+               OSMUtils.updatePlaceFromOSMObject(this._place, osmObject);
+               // refresh place view
+               this._clearView();
+               this._populate(this._place);
+           }
+           break;
+       default:
+           break;
+       }
     }
 });



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]