[gnome-maps] osmEdit: Add OSM edit and account support



commit ce39c3c1d1dee20df246672d32e04a1b1639ccb4
Author: Marcus Lundblad <ml update uu se>
Date:   Mon Oct 19 21:43:32 2015 +0200

    osmEdit: Add OSM edit and account support
    
    High-level JS implementation for communication with the OSM server.
    Implementing the OAuth 1.0a protocol to enroll user credentials and store
    credentials in the system keyring using libsecret.
    Dialog for setting up OSM account.
    Dialog for editing OSM data and utility functions used for editing.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=726628

 data/org.gnome.Maps.data.gresource.xml |    2 +
 data/org.gnome.Maps.gschema.xml        |    5 +
 data/ui/app-menu.ui                    |    6 +
 data/ui/osm-account-dialog.ui          |  326 +++++++++++++++++++++++
 data/ui/osm-edit-dialog.ui             |  226 ++++++++++++++++
 data/ui/place-bubble.ui                |   56 +++-
 src/application.js                     |   10 +
 src/org.gnome.Maps.src.gresource.xml   |    5 +
 src/osmAccountDialog.js                |  208 +++++++++++++++
 src/osmConnection.js                   |  456 ++++++++++++++++++++++++++++++++
 src/osmEdit.js                         |  184 +++++++++++++
 src/osmEditDialog.js                   |  396 +++++++++++++++++++++++++++
 src/osmUtils.js                        |   57 ++++
 src/placeBubble.js                     |   54 ++++-
 14 files changed, 1977 insertions(+), 14 deletions(-)
---
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 7e25b57..225c61a 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -14,6 +14,8 @@
     <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-account-dialog.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/org.gnome.Maps.gschema.xml b/data/org.gnome.Maps.gschema.xml
index a9dcfd9..aa083af 100644
--- a/data/org.gnome.Maps.gschema.xml
+++ b/data/org.gnome.Maps.gschema.xml
@@ -55,5 +55,10 @@
       <summary>Foursquare check-in Twitter broadcasting</summary>
       <description>Indicates if Foursquare should broadcast the check-in as a tweet in the Twitter account 
associated with the Foursquare account.</description>
     </key>
+    <key name="osm-username" type="s">
+      <default>""</default>
+      <summary>OpenStreetMap username or e-mail address</summary>
+      <description>Indicates if the user has signed in to edit OpenStreetMap data.</description>
+    </key>
   </schema>
 </schemalist>
diff --git a/data/ui/app-menu.ui b/data/ui/app-menu.ui
index a40eb08..1dd897e 100644
--- a/data/ui/app-menu.ui
+++ b/data/ui/app-menu.ui
@@ -3,6 +3,12 @@
   <menu id="app-menu">
     <section>
       <item>
+        <attribute name="action">app.osm-account-setup</attribute>
+        <attribute name="label" translatable="yes">Setup OpenStreetMap Account</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
         <attribute name="action">win.about</attribute>
         <attribute name="label" translatable="yes">About</attribute>
       </item>
diff --git a/data/ui/osm-account-dialog.ui b/data/ui/osm-account-dialog.ui
new file mode 100644
index 0000000..48a7f51
--- /dev/null
+++ b/data/ui/osm-account-dialog.ui
@@ -0,0 +1,326 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="Gjs_OSMAccountDialog" 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>
+    <property name="title" translatable="yes">OpenStreetMap Account</property>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="contentArea">
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="visible">True</property>
+            <child>
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="row-spacing">10</property>
+                <property name="margin">20</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">&lt;span weight="bold" size="x-large"&gt;Sign 
in to edit maps&lt;/span&gt;</property>
+                    <property name="use_markup">True</property>
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">GTK_ALIGN_CENTER</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Help to improve the map, using an
+OpenStreetMap account.</property>
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">GTK_ALIGN_CENTER</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkGrid">
+                    <property name="visible">True</property>
+                    <property name="column-spacing">10</property>
+                    <property name="row-spacing">10</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">Email</property>
+                        <property name="halign">GTK_ALIGN_END</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="emailEntry">
+                        <property name="visible">True</property>
+                        <property name="hexpand">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">0</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">Password</property>
+                        <property name="halign">GTK_ALIGN_END</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="GtkEntry" id="passwordEntry">
+                        <property name="visible">True</property>
+                        <property name="hexpand">True</property>
+                        <property name="input-purpose">GTK_INPUT_PURPOSE_PASSWORD</property>
+                        <property name="visibility">False</property>
+                        <property name="caps-lock-warning">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                        <property name="width">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="signInSpinner">
+                        <property name="visible">False</property>
+                        <property name="height_request">16</property>
+                        <property name="width_request">16</property>
+                        <property name="can_focus">False</property>
+                        <property name="active">True</property>
+                        <property name="halign">GTK_ALIGN_END</property>
+                        <property name="hexpand">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="signInButton">
+                        <property name="visible">True</property>
+                        <property name="halign">GTK_ALIGN_END</property>
+                        <property name="label" translatable="yes">Sign In</property>
+                        <property name="sensitive">False</property>
+                        <style>
+                          <class name="suggested-action"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">2</property>
+                        <property name="top_attach">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLinkButton" id="signUpLinkButton">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">Don't have an account?</property>
+                    <property name="uri">https://www.openstreetmap.org/user/new?referer=gnome-maps</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="resetPasswordLabel">
+                    <property name="visible">False</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes"
+                      comments="The label should contain the link to the OSM reset password page with a 
translated title">Sorry, that didn't work. Please try again, or visit
+&lt;a href="https://www.openstreetmap.org/user/forgot-password"&gt;OpenStreetMap&lt;/a&gt; to reset your 
password.</property>
+                    <property name="use-markup">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="verificationFailedLabel">
+                    <property name="visible">False</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">The verification code didn't match, please try 
again.</property>
+                    <property name="use-markup">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">5</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">sign-in</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkGrid" id="verifyGrid">
+                <property name="visible">True</property>
+                <property name="row-spacing">10</property>
+                <property name="margin">20</property>
+                <child>
+                  <object class="WebKitWebView" id="verifyView">
+                    <property name="visible">True</property>
+                    <property name="halign">GTK_ALIGN_FILL</property>
+                    <property name="self-scrolling">True</property>
+                    <property name="height-request">250</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkGrid">
+                    <property name="visible">True</property>
+                    <property name="column-spacing">10</property>
+                    <property name="row-spacing">10</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="label" translatable="yes">Enter verification code shown 
above</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="verificationEntry">
+                        <property name="visible">True</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="verifyButton">
+                        <property name="visible">True</property>
+                        <property name="sensitive">False</property>
+                        <property name="label" translatable="yes">Verify</property>
+                        <property name="hexpand">False</property>
+                        <property name="halign">GTK_ALIGN_END</property>
+                        <style>
+                          <class name="suggested-action"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">verify</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="row-spacing">10</property>
+                <property name="margin">20</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">&lt;span weight="bold" 
size="x-large"&gt;Signed In&lt;/span&gt;</property>
+                    <property name="use_markup">True</property>
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">GTK_ALIGN_CENTER</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Your OpenStreetMap account is 
active.</property>
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">GTK_ALIGN_CENTER</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon-name">avatar-default-symbolic</property>
+                    <property name="pixel-size">64</property>
+                    <property name="opacity">0.33</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="signedInUserLabel">
+                    <property name="visible">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="signOutButton">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Sign Out</property>
+                    <property name="halign">GTK_ALIGN_CENTER</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">4</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">logged-in</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/osm-edit-dialog.ui b/data/ui/osm-edit-dialog.ui
new file mode 100644
index 0000000..9cb9139
--- /dev/null
+++ b/data/ui/osm-edit-dialog.ui
@@ -0,0 +1,226 @@
+<?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>
+    <property name="height_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="GtkGrid">
+                <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="GtkGrid">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="vexpand">True</property>
+                    <property name="valign">GTK_ALIGN_END</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="GtkGrid">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="row-spacing">5</property>
+                            <property name="column-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>
+                    </child>
+                  </object>
+                </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 Location</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>
+    <child>
+      <object class="GtkGrid" id="addFieldPopoverGrid">
+        <property name="visible">True</property>
+        <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/place-bubble.ui b/data/ui/place-bubble.ui
index c6f2b6a..5c2f97d 100644
--- a/data/ui/place-bubble.ui
+++ b/data/ui/place-bubble.ui
@@ -11,29 +11,61 @@
       </object>
     </child>
     <child>
-      <object class="GtkBox" id="box-content">
+      <object class="GtkGrid" id="grid-content">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="orientation">vertical</property>
-        <style>
-          <class name="bubble-content"/>
-        </style>
+        <property name="column-spacing">5</property>
         <child>
-          <object class="GtkLabel" id="label-title">
+          <object class="GtkBox" id="box-content">
             <property name="visible">True</property>
             <property name="can_focus">False</property>
-            <property name="halign">start</property>
-            <property name="vexpand">True</property>
+            <property name="orientation">vertical</property>
             <style>
-              <class name="bubble-title"/>
+              <class name="bubble-content"/>
             </style>
+            <child>
+              <object class="GtkLabel" id="label-title">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">start</property>
+                <property name="vexpand">True</property>
+                <style>
+                  <class name="bubble-title"/>
+                </style>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
           </object>
           <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">0</property>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
           </packing>
         </child>
+        <child>
+          <object class="GtkButton" id="edit-button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="valign">GTK_ALIGN_START</property>
+            <property name="halign">GTK_ALIGN_CENTER</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon-name">edit-symbolic</property>
+                <property name="pixel_size">16</property>
+              </object>
+            </child>
+            <style>
+              <class name="image-button"/>
+              <class name="circular"/>
+              <class name="flat"/>
+            </style>
+          </object>
+        </child>
       </object>
     </child>
   </object>
diff --git a/src/application.js b/src/application.js
index 92b2448..99bb426 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 osmEdit = null;
 
 const Application = new Lang.Class({
     Name: 'Application',
@@ -147,6 +149,10 @@ const Application = new Lang.Class({
         this._mainWindow.destroy();
     },
 
+    _onOsmAccountSetupActivate: function() {
+        osmEdit.showAccountDialog(this._mainWindow, false);
+    },
+
     _addContacts: function() {
         contactStore.get_contacts().forEach(function(contact) {
             contact.geocode(function() {
@@ -205,6 +211,9 @@ const Application = new Lang.Class({
             'show-contact': {
                 paramType: 's',
                 onActivate: this._onShowContactActivate.bind(this)
+            },
+            'osm-account-setup': {
+                onActivate: this._onOsmAccountSetupActivate.bind(this)
             }
         });
 
@@ -225,6 +234,7 @@ const Application = new Lang.Class({
         checkInManager = new CheckIn.CheckInManager();
         contactStore = new Maps.ContactStore();
         contactStore.load();
+        osmEdit = new OSMEdit.OSMEdit();
     },
 
     _createWindow: function() {
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index 50791dd..36f4c07 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -30,6 +30,11 @@
     <file>mapWalker.js</file>
     <file>notification.js</file>
     <file>notificationManager.js</file>
+    <file>osmAccountDialog.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/osmAccountDialog.js b/src/osmAccountDialog.js
new file mode 100644
index 0000000..324f84b
--- /dev/null
+++ b/src/osmAccountDialog.js
@@ -0,0 +1,208 @@
+/* -*- 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 Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const WebKit = imports.gi.WebKit;
+
+const Application = imports.application;
+
+const Response = {
+    SIGNED_IN: 0
+};
+
+const OSMAccountDialog = new Lang.Class({
+    Name: 'OSMAccountDialog',
+    Extends: Gtk.Dialog,
+    Template: 'resource:///org/gnome/Maps/ui/osm-account-dialog.ui',
+    InternalChildren: ['stack',
+                       'emailEntry',
+                       'passwordEntry',
+                       'signInButton',
+                       'signInSpinner',
+                       'signUpLinkButton',
+                       'resetPasswordLabel',
+                       'verifyView',
+                       'verificationEntry',
+                       'verifyButton',
+                       'verificationFailedLabel',
+                       'signedInUserLabel',
+                       'signOutButton'],
+
+    _init: function(params) {
+        /* This is a construct-only property and cannot be set by GtkBuilder */
+        params.use_header_bar = true;
+        GObject.type_ensure(WebKit.WebView);
+
+        this._closeOnSignIn = params.closeOnSignIn;
+        delete params.closeOnSignIn;
+
+        this.parent(params);
+
+        this._emailEntry.connect('changed',
+                                 this._onCredentialsChanged.bind(this));
+        this._passwordEntry.connect('changed',
+                                    this._onCredentialsChanged.bind(this));
+        this._passwordEntry.connect('activate',
+                                    this._onPasswordActivated.bind(this));
+        this._signInButton.connect('clicked',
+                                   this._onSignInButtonClicked.bind(this));
+        this._verifyButton.connect('clicked',
+                                   this._onVerifyButtonClicked.bind(this));
+        this._verificationEntry.connect('changed',
+                                        this._onVerificationEntryChanged.bind(this));
+        this._verificationEntry.connect('activate',
+                                        this._onVerificationEntryActivated.bind(this));
+        this._signOutButton.connect('clicked',
+                                    this._onSignOutButtonClicked.bind(this));
+
+        /* if the user is logged in, show the logged-in view */
+        if (Application.osmEdit.isSignedIn) {
+            this._signedInUserLabel.label = Application.osmEdit.username;
+            this._stack.visible_child_name = 'logged-in';
+        }
+    },
+
+    _onCredentialsChanged: function() {
+        let email = this._emailEntry.text;
+        let password = this._passwordEntry.text;
+
+        /* make sign in button sensitive if credential have been entered
+            TODO: should we try to validate email addresses? */
+        this._signInButton.sensitive =
+            email && email.length > 0 && password && password.length > 0;
+    },
+
+    _onSignInButtonClicked: function() {
+        this._performSignIn();
+    },
+
+    _onPasswordActivated: function() {
+        /* if username and password was entered, proceed with sign-in */
+        let email = this._emailEntry.text;
+        let password = this._passwordEntry.text;
+
+        if (email && email.length > 0 && password && password.length > 0)
+            this._performSignIn();
+    },
+
+    _performSignIn: function() {
+        /* turn on signing in spinner and desensisize credential entries */
+        this._signInSpinner.visible = true;
+        this._signInButton.sensitive = false;
+        this._emailEntry.sensitive = false;
+        this._passwordEntry.sensitive = false;
+        this._signUpLinkButton.visible = false;
+
+        Application.osmEdit.performOAuthSignIn(this._emailEntry.text,
+                                               this._passwordEntry.text,
+                                               this._onOAuthSignInPerformed.bind(this));
+    },
+
+    _onOAuthSignInPerformed: function(success, verificationPage) {
+        if (success) {
+            /* switch to the verification view and show the verification
+               page */
+            this._verifyView.load_html_string(verificationPage,
+                                              'https://www.openstreetmap.org/');
+            this._stack.visible_child_name = 'verify';
+        } else {
+            /* clear password entry */
+            this._passwordEntry.text = '';
+            /* show the password reset link */
+            this._resetPasswordLabel.visible = true;
+        }
+
+        this._signInSpinner.visible = false;
+        /* re-sensisize credential entries */
+        this._emailEntry.sensitive = true;
+        this._passwordEntry.sensitive = true;
+    },
+
+    _onVerifyButtonClicked: function() {
+        this._performVerification();
+    },
+
+    _performVerification: function() {
+        /* allow copying the leading space between the "The verification is"
+           label and the code */
+        let verificationCode = this._verificationEntry.text.trim();
+
+        /* Since the text shown on OSM's OAuth authorization verification form
+           is a bit unclear with a trailing period after the verification code,
+           let's strip that off if the user copied that over. */
+        if (verificationCode.charAt(verificationCode.length - 1) === '.') {
+            verificationCode = verificationCode.slice(0, -1);
+        }
+
+        Application.osmEdit.requestOAuthAccessToken(verificationCode,
+                                                    this._onOAuthAccessTokenRequested.bind(this));
+    },
+
+    _onVerificationEntryChanged: function() {
+        this._verifyButton.sensitive =
+            this._verificationEntry.text &&
+            this._verificationEntry.text.length > 0;
+    },
+
+    _onVerificationEntryActivated: function() {
+        /* proceed with verfication if a code has been entered */
+        let verificationCode = this._verificationEntry.text;
+
+        if (verificationCode && verificationCode.length > 0)
+            this._performVerification();
+    },
+
+    _onOAuthAccessTokenRequested: function(success) {
+        if (success) {
+            /* update the username label */
+            this._signedInUserLabel.label = Application.osmEdit.username;
+
+            if (this._closeOnSignIn) {
+                this.response(Response.SIGNED_IN);
+            } else {
+                /* switch to the logged in view and reset the state in case
+                   the user signs out and start over again */
+                this._resetPasswordLabel.visible = false;
+                this._verificationFailedLabel = false;
+                this._signUpLinkButton.visible = true;
+                this._stack.visible_child_name = 'logged-in';
+            }
+        } else {
+            /* switch back to the sign-in view, and show a label indicating
+               that verification failed */
+            this._resetPasswordLabel.visible = false;
+            this._signUpLinkButton.visible = false;
+            this._verificationFailedLabel.visible = true;
+            this._signInButton.sensitive = true;
+            this._stack.visible_child_name = 'sign-in';
+        }
+        /* reset verification code entry */
+        this._verificationEntry.text = '';
+    },
+
+    _onSignOutButtonClicked: function() {
+        Application.osmEdit.signOut();
+        this._stack.visible_child_name = 'sign-in';
+    }
+});
diff --git a/src/osmConnection.js b/src/osmConnection.js
new file mode 100644
index 0000000..fa36a30
--- /dev/null
+++ b/src/osmConnection.js
@@ -0,0 +1,456 @@
+/* -*- 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 Maps = imports.gi.GnomeMaps;
+const Rest = imports.gi.Rest;
+const Secret = imports.gi.Secret;
+const Soup = imports.gi.Soup;
+
+const BASE_URL = 'https://api.openstreetmap.org/api';
+const API_VERSION = '0.6';
+
+/* OAuth constants */
+const CONSUMER_KEY = '2lbpDoED0ZspGssTBAJ8zOCtrtmUoX4KnmZUIWIK';
+const CONSUMER_SECRET = 'AO9BhDl9sJ33DjaZgQmYcNIuM3ZSml4xtugai6gE';
+const OAUTH_ENDPOINT_URL = 'https://www.openstreetmap.org/oauth';
+const LOGIN_URL = 'https://www.openstreetmap.org/login';
+
+const SECRET_SCHEMA = new Secret.Schema("org.gnome.Maps",
+    Secret.SchemaFlags.NONE,
+    {
+    }
+);
+
+const OSMConnection = new Lang.Class({
+    Name: 'OSMConnection',
+
+    _init: function(params) {
+        this._session = new Soup.Session();
+        /* OAuth proxy used for enrolling access tokens */
+        this._oauthProxy = Rest.OAuthProxy.new(CONSUMER_KEY, CONSUMER_SECRET,
+                                               OAUTH_ENDPOINT_URL, false);
+        /* OAuth proxy used for making OSM uploads */
+        this._callProxy = Rest.OAuthProxy.new(CONSUMER_KEY, CONSUMER_SECRET,
+                                              BASE_URL + '/' + API_VERSION,
+                                              false);
+        Maps.osm_init();
+    },
+
+    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, type, null);
+                return;
+            }
+
+            try {
+                let object = Maps.osm_parse (message.response_body.data,
+                                             message.response_body.length);
+                callback(true, message.status_code, object, type, null);
+            } catch (e) {
+                callback(false, message.status_code, null, type, e);
+            }
+        }).bind(this));
+    },
+
+    _getQueryUrl: function(type, id) {
+        return BASE_URL + '/' + API_VERSION + '/' + type + '/' + id;
+    },
+
+    openChangeset: function(comment, callback) {
+        /* we assume that this would only be called if there's already been an
+           OAuth access token enrolled, so, if the currently instanciated
+           proxy instance doesn't have a token set, we could safely count on
+           it being present in the keyring */
+        if (this._oauthProxy.get_token() === null) {
+            Secret.password_lookup(SECRET_SCHEMA, {}, null, function(s, res) {
+                this._onPasswordLookedUp(res,
+                                         comment,
+                                         callback);
+            }.bind(this));
+        } else {
+            this._doOpenChangeset(comment, callback);
+        }
+    },
+
+    _onPasswordLookedUp: function(result, comment, callback) {
+        let password = Secret.password_lookup_finish(result);
+
+        if (password) {
+            let token = password.split(':')[0];
+            let secret = password.split(':')[1];
+
+            this._callProxy.token = token;
+            this._callProxy.token_secret = secret;
+            this._doOpenChangeset(comment, callback);
+        } else {
+            callback(false, null, null);
+        }
+    },
+
+    _doOpenChangeset: function(comment, callback) {
+        let changeset =
+            Maps.OSMChangeset.new(comment, 'gnome-maps ' + pkg.version);
+        let xml = changeset.serialize();
+
+        let call = Maps.OSMOAuthProxyCall.new(this._callProxy, xml);
+        call.set_method('PUT');
+        call.set_function('/changeset/create');
+
+        call.invoke_async(null, (function(call, res, userdata) {
+                    this._onChangesetOpened(call, callback);
+                                }).bind(this));
+    },
+
+    _onChangesetOpened: function(call, callback) {
+        if (call.get_status_code() !== Soup.Status.OK) {
+            callback(false, call.get_status_code(), null);
+            return;
+        }
+
+        let changesetId = parseInt(call.get_payload());
+        callback(true, call.get_status_code(), changesetId);
+    },
+
+    uploadObject: function(object, type, changeset, callback) {
+        object.changeset = changeset;
+
+        let xml = object.serialize();
+        let call = Maps.OSMOAuthProxyCall.new(this._callProxy, xml);
+
+        call.set_method('PUT');
+        call.set_function(this._getCreateOrUpdateFunction(object, type));
+
+        call.invoke_async(null, (function(call, res, userdata) {
+                    this._onObjectUploaded(call, callback);
+                                }).bind(this));
+    },
+
+    _onObjectUploaded: function(call, callback) {
+        if (call.get_status_code() !== Soup.Status.OK) {
+            callback(false, call.get_status_code(), null);
+            return;
+        }
+
+        callback(true, call.get_status_code(), call.get_payload());
+    },
+
+    deleteObject: function(object, type, changeset, callback) {
+        object.changeset = changeset;
+
+        let xml = object.serialize();
+        let call = Maps.OSMOAuthProxyCall.new(this._callProxy, xml);
+
+        call.set_method('DELETE');
+        call.set_function(this._getDeleteFunction(object, type));
+
+        call.invoke_async(null, (function(call, res, userdata) {
+                    this._onObjectDeleted(call, callback);
+                                }).bind(this));
+    },
+
+    _onObjectDeleted: function(call, callback) {
+        if (call.get_status_code() !== Soup.Status.OK) {
+            callback(false, call.get_status_code(), null);
+            return;
+        }
+
+        callback(true, call.get_status_code(), call.get_payload());
+    },
+
+    closeChangeset: function(changesetId, callback) {
+        let call = this._callProxy.new_call();
+        call.set_method('PUT');
+        call.set_function(this._getCloseChangesetFunction(changesetId));
+
+        call.invoke_async(null, (function(call, res, userdata) {
+                    this._onChangesetClosed(call, callback);
+                                }).bind(this));
+    },
+
+    _onChangesetClosed: function(call, callback) {
+        if (call.get_status_code() !== Soup.Status.OK) {
+            callback(false, call.get_status_code(), null);
+            return;
+        }
+
+        callback(true, call.get_status_code(), call.get_payload());
+    },
+
+    _getCloseChangesetFunction: function(changesetId) {
+        return '/changeset/' + changesetId + '/close';
+    },
+
+    _getCreateOrUpdateFunction: function(object, type) {
+        if (object.id)
+            return type + '/' + object.id;
+        else
+            return type + '/create';
+    },
+
+    _getDeleteFunction: function(object, type) {
+        return type + '/' + id;
+    },
+
+    requestOAuthToken: function(callback) {
+        this._oauthProxy.request_token_async('request_token', 'oob', function(p, error, w, u) {
+            this._onRequestOAuthToken(error, callback);
+        }.bind(this), this._oauthProxy, callback);
+    },
+
+    _onRequestOAuthToken: function(error, callback) {
+        if (error) {
+            callback(false);
+            return;
+        }
+
+        this._oauthToken = this._oauthProxy.get_token();
+        this._oauthTokenSecret = this._oauthProxy.get_token_secret();
+        callback(true);
+    },
+
+    authorizeOAuthToken: function(username, password, callback) {
+        /* get login session ID */
+        let loginUrl = LOGIN_URL + '?cookie_test=true';
+        let uri = new Soup.URI(loginUrl);
+        let msg = new Soup.Message({method: 'GET', uri: uri});
+
+        this._session.queue_message(msg, (function(obj, message) {
+            this._onLoginFormReceived(message, username, password, callback);
+        }).bind(this));
+    },
+
+    _onLoginFormReceived: function(message, username, password, callback) {
+        if (message.status_code !== Soup.Status.OK) {
+            callback(false);
+            return;
+        }
+
+        let osmSessionID =
+            this._extractOSMSessionID(message.response_headers);
+        let osmSessionToken =
+            this._extractToken(message.response_body.data);
+
+        if (osmSessionID === null || osmSessionToken === null) {
+            callback(false, null);
+            return;
+        }
+
+        this._login(username, password, osmSessionID, osmSessionToken, callback);
+    },
+
+    _login: function(username, password, sessionId, token, callback) {
+        /* post login form */
+        let msg = Soup.form_request_new_from_hash('POST', LOGIN_URL,
+                                                  {username: username,
+                                                   password: password,
+                                                   referer: '/',
+                                                   commit: 'Login',
+                                                   authenticity_token: token});
+        let requestHeaders = msg.request_headers;
+
+        requestHeaders.append('Content-Type',
+                              'application/x-www-form-urlencoded');
+        requestHeaders.append('Cookie', '_osm_session=' + sessionId);
+        msg.flags |= Soup.MessageFlags.NO_REDIRECT;
+
+        this._session.queue_message(msg, (function(obj, message) {
+            if (message.status_code === Soup.Status.MOVED_TEMPORARILY)
+                this._fetchAuthorizeForm(username, sessionId, callback);
+            else
+                callback(false, null);
+        }).bind(this));
+
+    },
+
+    _fetchAuthorizeForm: function(username, sessionId, callback) {
+        let auth = '/authorize?oauth_token=';
+        let authorizeUrl = OAUTH_ENDPOINT_URL + auth + this._oauthToken;
+        let uri = new Soup.URI(authorizeUrl);
+        let msg = new Soup.Message({uri: uri, method: 'GET'});
+
+        msg.request_headers.append('Cookie',
+                                   '_osm_session=' + sessionId +
+                                   '; _osm_username=' + username);
+        this._session.queue_message(msg, (function(obj, message) {
+            if (message.status_code === Soup.Status.OK) {
+                let token = this._extractToken(message.response_body.data);
+                this._postAuthorizeForm(username, sessionId, token, callback);
+            } else {
+                callback(false, null);
+            }
+        }).bind(this));
+    },
+
+    _postAuthorizeForm: function(username, sessionId, token, callback) {
+        let authorizeUrl = OAUTH_ENDPOINT_URL + '/authorize';
+        let msg = Soup.form_request_new_from_hash('POST', authorizeUrl, {
+            oauth_token: this._oauthToken,
+            oauth_callback: '',
+            authenticity_token: token,
+            allow_write_api: 'yes',
+            commit: 'Save changes'
+        });
+        let requestHeaders = msg.request_headers;
+
+        requestHeaders.append('Content-Type',
+                              'application/x-www-form-urlencoded');
+        requestHeaders.append('Cookie',
+                              '_osm_session=' + sessionId +
+                              '; _osm_username=' + username);
+
+        this._session.queue_message(msg, (function(obj, message) {
+            if (msg.status_code === Soup.Status.OK) {
+                callback(true, message.response_body.data);
+            } else
+                callback(false, null);
+        }).bind(this));
+    },
+
+    requestOAuthAccessToken: function(code, callback) {
+        this._oauthProxy.access_token_async('access_token', code, function(p, error, w, data) {
+            this._onAccessOAuthToken(error, callback);
+        }.bind(this), this._oauthProxy, callback);
+    },
+
+    _onAccessOAuthToken: function(error, callback) {
+        if (error) {
+            callback(false);
+            return;
+        }
+
+        let token = this._oauthProxy.token;
+        let secret = this._oauthProxy.token_secret;
+
+        this._callProxy.token = token;
+        this._callProxy.token_secret = secret;
+        Secret.password_store(SECRET_SCHEMA, {}, Secret.COLLECTION_DEFAULT,
+                              "OSM OAuth access token and secret",
+                              this._oauthProxy.token + ":" +
+                              this._oauthProxy.token_secret, null,
+                              function(source, result, userData) {
+                                this._onPasswordStored(result, callback);
+                              }.bind(this));
+    },
+
+    _onPasswordStored: function(result, callback) {
+        let res = false;
+        if (result)
+            res = Secret.password_store_finish(result);
+        callback(res);
+    },
+
+    signOut: function() {
+        /* clear token on call proxy, so it will use a new token if the user
+           signs in again (with a new access token) during this running
+           session */
+        this._callProxy.token = null;
+        this._callProxy.token_secret = null;
+
+        Secret.password_clear(SECRET_SCHEMA, {}, null,
+            this._onPasswordCleared.bind(this));
+    },
+
+    _onPasswordCleared: function(source, result) {
+        Secret.password_clear_finish(result);
+    },
+
+    /* extract the session ID from the login form response headers */
+    _extractOSMSessionID: function(responseHeaders) {
+        let cookie = responseHeaders.get('Set-Cookie');
+
+        if (cookie === null)
+            return null;
+
+        let cookieParts = cookie.split(';');
+        for (let index in cookieParts) {
+            let kvPair = cookieParts[index].trim();
+            let kv = kvPair.split('=');
+
+            if (kv.length !== 2) {
+                continue;
+            } else if (kv[0] === '_osm_session') {
+                return kv[1];
+            }
+        }
+
+        return null;
+    },
+
+    /* extract the authenticity token from the hidden input field of the login
+       form */
+    _extractToken: function(messageBody) {
+        let regex = /.*authenticity_token.*value=\"([^\"]+)\".*/;
+        let lines = messageBody.split('\n');
+
+        for (let i in lines) {
+            let line = lines[i];
+            let match = line.match(regex);
+
+            if (match && match.length === 2)
+                return match[1];
+        }
+
+        return null;
+    }
+});
+
+/*
+ * 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..62f15d5
--- /dev/null
+++ b/src/osmEdit.js
@@ -0,0 +1,184 @@
+/* -*- 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 Application = imports.application;
+const OSMAccountDialog = imports.osmAccountDialog;
+const OSMEditDialog = imports.osmEditDialog;
+const OSMConnection = imports.osmConnection;
+const Utils = imports.utils;
+
+const OSMEdit = new Lang.Class({
+    Name: 'OSMEdit',
+    Extends: GObject.Object,
+
+    _init: function() {
+        this._osmConnection = new OSMConnection.OSMConnection();
+        this._osmObject = null; // currently edited object
+        this._username = Application.settings.get('osm-username');
+        this._isSignedIn = this._username !== null && this._username.length > 0;
+    },
+
+    get object() {
+        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;
+    },
+
+    showAccountDialog: function(parentWindow, closeOnSignIn) {
+        let dialog = new OSMAccountDialog.OSMAccountDialog({
+            transient_for: parentWindow,
+            closeOnSignIn: closeOnSignIn
+        });
+        let response = dialog.run();
+        dialog.destroy();
+        return response;
+    },
+
+    fetchObject: function(place, callback, cancellable) {
+        let osmType = Utils.osmTypeToString(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);
+    },
+
+    uploadObject: function(object, type, comment, callback) {
+        this._openChangeset(object, type, comment,
+                            this._uploadObject.bind(this), callback);
+    },
+
+    _onChangesetOpened: function(success, status, changesetId, object, type,
+                                 action, callback) {
+        if (success) {
+            let osmType = Utils.osmTypeToString(type);
+            action(object, osmType, changesetId, callback);
+        } else {
+            callback(false, status);
+        }
+    },
+
+    _openChangeset: function(object, type, comment, action, callback) {
+        this._osmConnection.openChangeset(comment, (function(success, status, changesetId) {
+            this._onChangesetOpened(success, status, changesetId, object, type, action, callback);
+        }).bind(this));
+    },
+
+    _onObjectUploaded: function(success, status, response, changesetId, callback) {
+        if (success)
+            this._closeChangeset(changesetId, callback);
+        else
+            callback(false, status);
+    },
+
+    _uploadObject: function(object, type, changesetId, callback) {
+        this._osmObject = object;
+        this._osmConnection.uploadObject(object, type, changesetId, (function(success, status, response) {
+            this._onObjectUploaded(success, status, response, changesetId, callback);
+        }).bind(this));
+    },
+
+    deleteObject: function(object, type, comment, callback) {
+        this._openChangeset(object, type, comment,
+                            this._deleteObject.bind(this), callback);
+    },
+
+    _onObjectDeleted: function(success, status, response, changesetId, callback) {
+        if (success)
+            this._closeChangeset(changesetId, callback);
+        else
+            callback(false, status);
+    },
+
+    _deleteObject: function(object, type, changesetId, callback) {
+        this._osmObject = object;
+        this._osmConnection.deleteObject(object, type, changesetId, (function(success, status, response) {
+            this._onObjectDeleted(success, status, response, changesetId, callback);
+        }).bind(this));
+    },
+
+    _closeChangeset: function(changesetId, callback) {
+        this._osmConnection.closeChangeset(changesetId, callback);
+    },
+
+    performOAuthSignIn: function(username, password, callback) {
+        this._osmConnection.requestOAuthToken(function(success) {
+            if (success)
+                this._onOAuthTokenRequested(username, password, callback);
+            else
+                callback(false, null);
+        }.bind(this));
+    },
+
+    _onOAuthTokenRequested: function(username, password, callback) {
+        /* keep track of authorizing username */
+        this._username = username;
+        this._osmConnection.authorizeOAuthToken(username, password, callback);
+    },
+
+    requestOAuthAccessToken: function(code, callback) {
+        this._osmConnection.requestOAuthAccessToken(code, (function(success, token) {
+            this._onOAuthAccessTokenRequested(success, callback);
+        }).bind(this));
+    },
+
+    _onOAuthAccessTokenRequested: function(success, callback) {
+        if (success) {
+            this._isSignedIn = true;
+            Application.settings.set('osm-username', this._username);
+        } else {
+            /* clear out username if verification was unsuccessful */
+            this._username = null;
+        }
+
+        callback(success);
+    },
+
+    signOut: function() {
+        this._username = null;
+        this._isSignedIn = false;
+
+        Application.settings.set('osm-username', '');
+        this._osmConnection.signOut();
+    },
+
+    get isSignedIn() {
+        return this._isSignedIn;
+    },
+
+    get username() {
+        return this._username;
+    }
+});
diff --git a/src/osmEditDialog.js b/src/osmEditDialog.js
new file mode 100644
index 0000000..599e98f
--- /dev/null
+++ b/src/osmEditDialog.js
@@ -0,0 +1,396 @@
+/* -*- 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 = {
+    TEXT: 0,
+    INTEGER: 1,
+    // selection of yes|no|limited|designated
+    YES_NO_LIMITED_DESIGNATED: 2
+};
+
+let _osmWikipediaRewriteFunc = function(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;
+};
+
+/*
+ * 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 OSM_FIELDS = [{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}];
+
+
+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',
+                        'addFieldPopoverGrid',
+                        '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.osmEdit.fetchObject(this._place,
+                                        this._onObjectFetched.bind(this),
+                                        this._cancellable);
+    },
+
+    _onNextClicked: function() {
+        if (this._isEditing) {
+            // switch to the upload view
+            this._switchToUpload();
+        } else {
+            // turn on spinner
+            this._stack.visible_child_name = 'loading';
+            this._nextButton.sensitive = false;
+            this._backButton.sensitive = false;
+            // upload data to OSM
+            let comment = this._commentTextView.buffer.text;
+            Application.osmEdit.uploadObject(this._osmObject,
+                                             this._place.osmType, comment,
+                                             this._onObjectUploaded.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.visible = false;
+        this._isEditing = false;
+    },
+
+    _onCancelClicked: function() {
+        this.response(Response.CANCELLED);
+    },
+
+    _onBackClicked: function() {
+        this._backButton.visible = false;
+        this._cancelButton.visible = true;
+        this._nextButton.label = _("Next");
+        this._stack.set_visible_child_name('editor');
+        this._isEditing = true;
+        this._commentTextView.buffer.text = '';
+    },
+
+    _onObjectFetched: function(success, status, osmObject, osmType, error) {
+        if (success) {
+            this._isEditing = true;
+            this._loadOSMData(osmObject);
+        } else {
+            this._showError(status, error);
+        }
+    },
+
+    _onObjectUploaded: function(success, status) {
+        if (success) {
+            this.response(Response.UPLOADED);
+        } else {
+            this._showError(status);
+            this.response(Response.ERROR);
+        }
+    },
+
+    _showError: function(status, error) {
+        /* set error message from specific error if available, otherwise use
+           a generic error message for the HTTP status code */
+        let statusMessage =
+            error ? error.message : 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;
+        }
+
+        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._currentRow, 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._currentRow--;
+            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._currentRow, 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._currentRow, 1, 1);
+        entry.show();
+
+        /* TODO: should we allow deleting the name field? */
+        this._addOSMEditDeleteButton(tag);
+
+        this._currentRow++;
+    },
+
+    _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._currentRow, 1, 1);
+        spinbutton.show();
+
+        this._addOSMEditDeleteButton(tag);
+        this._currentRow++;
+    },
+
+    _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._currentRow, 1, 1);
+        combobox.show();
+
+        this._addOSMEditDeleteButton(tag);
+        this._currentRow++;
+    },
+
+    /* update visible items in the "Add Field" popover */
+    _updateAddFieldMenu: function() {
+        /* clear old items */
+        let children = this._addFieldPopoverGrid.get_children();
+        let hasAllFields = true;
+
+        for (let i = 0; i < children.length; i++) {
+            let button = children[i];
+            button.destroy();
+        }
+
+        /* add selectable items */
+        for (let i = 0; i < OSM_FIELDS.length; i++) {
+            let fieldSpec = OSM_FIELDS[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 field
+                       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._addFieldPopoverGrid.add(button);
+            }
+        }
+
+        /* update sensitiveness of the add details button, set it as
+           insensitive if all tags we 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;
+
+        /* keeps track of the current insertion row in the grid for editing
+           widgets */
+        this._currentRow = 0;
+
+        /* create edit widgets */
+        for (let i = 0; i < OSM_FIELDS.length; i++) {
+            let fieldSpec = OSM_FIELDS[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..4c2f183
--- /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, object) {
+    place.name = object.get_tag('name');
+    place.population = object.get_tag('population');
+    place.wiki = object.get_tag('wikipedia');
+    place.openingHours = object.get_tag('opening_hours');
+    place.wheelchair = object.get_tag('wheelchair');
+
+    Application.placeStore.updatePlace(place);
+}
diff --git a/src/placeBubble.js b/src/placeBubble.js
index 4759b68..c22a125 100644
--- a/src/placeBubble.js
+++ b/src/placeBubble.js
@@ -26,6 +26,9 @@ const Lang = imports.lang;
 const Application = imports.application;
 const ContactPlace = imports.contactPlace;
 const MapBubble = imports.mapBubble;
+const OSMAccountDialog = imports.osmAccountDialog;
+const OSMEditDialog = imports.osmEditDialog;
+const OSMUtils = imports.osmUtils;
 const Overpass = imports.overpass;
 const Place = imports.place;
 const PlaceFormatter = imports.placeFormatter;
@@ -39,7 +42,9 @@ const PlaceBubble = new Lang.Class({
     _init: function(params) {
         let ui = Utils.getUIObject('place-bubble', [ 'stack',
                                                      'box-content',
-                                                     'label-title']);
+                                                     'grid-content',
+                                                     'label-title',
+                                                     'edit-button']);
         params.buttons = (MapBubble.Button.ROUTE |
                           MapBubble.Button.SEND_TO);
 
@@ -55,6 +60,8 @@ const PlaceBubble = new Lang.Class({
         this._stack = ui.stack;
         this._title = ui.labelTitle;
         this._boxContent = ui.boxContent;
+        this._gridContent = ui.gridContent;
+        this._editButton = ui.editButton;
 
         let overpass = new Overpass.Overpass();
         if (Application.placeStore.exists(this.place, null)) {
@@ -79,6 +86,8 @@ const PlaceBubble = new Lang.Class({
             this._populate(this.place);
         }
         this.content.add(this._stack);
+
+        this._initEditButton(this._editButton);
     },
 
     _formatWikiLink: function(wiki) {
@@ -133,6 +142,47 @@ const PlaceBubble = new Lang.Class({
             this._boxContent.pack_start(label, false, true, 0);
         }).bind(this));
 
-        this._stack.visible_child = this._boxContent;
+        this._stack.visible_child = this._gridContent;
+    },
+
+    // clear the view widgets to be able to re-populate 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) {
+        button.visible = true;
+        button.connect('clicked', this._onEditClicked.bind(this));
+    },
+
+    _onEditClicked: function() {
+        let osmEdit = Application.osmEdit;
+        /* if the user is not alread signed in, show the account dialog */
+        if (!osmEdit.isSignedIn) {
+            let response = osmEdit.showAccountDialog(this.get_toplevel(), true);
+            if (!response === OSMAccountDialog.Response.SIGNED_IN)
+                return;
+        }
+
+        let response = osmEdit.showEditDialog(this.get_toplevel(), this._place);
+
+        switch (response) {
+        case OSMEditDialog.Response.UPLOADED:
+            // update place
+            let object = osmEdit.object;
+            OSMUtils.updatePlaceFromOSMObject(this._place, object);
+            // 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]