diff --git a/docs/user/site.rst b/docs/user/site.rst
index 85d69cbdd45d2b1bb42408151c6353b11d86e443..3f24970d6b667bfd577815b7801c0f5faf76cd5c 100644
--- a/docs/user/site.rst
+++ b/docs/user/site.rst
@@ -407,6 +407,13 @@ config_mode \: optional
     package. Set *geo_location.show_altitude* to *true* if you want the altitude
     field to be visible.
 
+    The *geo_location.osm* section is only relevant when the *gluon-config-mode-geo-location-osm*
+    package is used. The *center.lon* and *center.lat* values are mandatory in this case and
+    define the default center of the map when no position has been picked yet. The *zoom* level
+    defaults to 12 in this case. *openlayers_url* allows to override the base URL of the
+    *build/ol.js* and *css/ol.css* files (the default is
+    ``https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.2.0``).
+
     The remote login page only shows SSH key configuration by default. A
     password form can be displayed by setting *remote_login.show_password_form*
     to true; in this case, *remote_login.min_password_length* defines the
@@ -419,6 +426,14 @@ config_mode \: optional
           },
           geo_location = {
             show_altitude = true,
+            osm = {
+              center = {
+                lon = 52.951947558,
+                lat = 7.844238281,
+              },
+              zoom = 13,
+              openlayers_url = 'http://ffac/ol',
+            },
           },
           remote_login = {
             show_password_form = true,
diff --git a/package/gluon-config-mode-geo-location-osm/Makefile b/package/gluon-config-mode-geo-location-osm/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..4dbaf9d00b551d16177579e021c032b9941b2a79
--- /dev/null
+++ b/package/gluon-config-mode-geo-location-osm/Makefile
@@ -0,0 +1,20 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-config-mode-geo-location-osm
+PKG_VERSION:=1
+
+include ../gluon.mk
+
+define Package/gluon-config-mode-geo-location-osm
+  TITLE:=Set geographic location of a node (map support)
+  DEPENDS:=+gluon-config-mode-geo-location +gluon-web-osm
+endef
+
+define Package/gluon-config-mode-geo-location-osm/install
+	$(Gluon/Build/Install)
+
+	$(INSTALL_DIR) $(1)/lib/gluon/config-mode/www/static/
+	$(LN) /lib/gluon/web/www/static/gluon-web-osm.js $(1)/lib/gluon/config-mode/www/static/
+endef
+
+$(eval $(call BuildPackageGluon,gluon-config-mode-geo-location-osm))
diff --git a/package/gluon-config-mode-geo-location-osm/check_site.lua b/package/gluon-config-mode-geo-location-osm/check_site.lua
new file mode 100644
index 0000000000000000000000000000000000000000..8e69ba0a14f637e15e6422efeb3a28f54717583a
--- /dev/null
+++ b/package/gluon-config-mode-geo-location-osm/check_site.lua
@@ -0,0 +1,4 @@
+need_number(in_site({'config_mode', 'geo_location', 'osm', 'center', 'lon'}))
+need_number(in_site({'config_mode', 'geo_location', 'osm', 'center', 'lat'}))
+need_number(in_site({'config_mode', 'geo_location', 'osm', 'zoom'}), false)
+need_string(in_site({'config_mode', 'geo_location', 'osm', 'openlayers_url'}), false)
diff --git a/package/gluon-config-mode-geo-location-osm/i18n/de.po b/package/gluon-config-mode-geo-location-osm/i18n/de.po
new file mode 100644
index 0000000000000000000000000000000000000000..dc0e23dfd24e14d1271418afc794048f60148715
--- /dev/null
+++ b/package/gluon-config-mode-geo-location-osm/i18n/de.po
@@ -0,0 +1,17 @@
+msgid ""
+msgstr ""
+"Content-Type: text/plain; charset=UTF-8\n"
+"Project-Id-Version: gluon-config-mode-geo-location\n"
+"Last-Translator: Matthias Schiffer <mschiffer@universe-factory.net>\n"
+"Language-Team: German\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid ""
+"You may also select the position on the map displayed below if your computer "
+"is connected to the internet at the moment."
+msgstr ""
+"Wenn dein Computer aktuell mit dem Internet verbunden ist, kann die Position "
+"auch auf der hier angezeigten Karte ausgewählt werden."
diff --git a/package/gluon-config-mode-geo-location-osm/i18n/gluon-config-mode-geo-location-osm.pot b/package/gluon-config-mode-geo-location-osm/i18n/gluon-config-mode-geo-location-osm.pot
new file mode 100644
index 0000000000000000000000000000000000000000..0a987630f2f943ecb197c240fc443e5ba527dc81
--- /dev/null
+++ b/package/gluon-config-mode-geo-location-osm/i18n/gluon-config-mode-geo-location-osm.pot
@@ -0,0 +1,7 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+msgid ""
+"You may also select the position on the map displayed below if your computer "
+"is connected to the internet at the moment."
+msgstr ""
diff --git a/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua b/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua
new file mode 100644
index 0000000000000000000000000000000000000000..4969f1ac8f450d483a160629905033e9d6611381
--- /dev/null
+++ b/package/gluon-config-mode-geo-location-osm/luasrc/usr/lib/lua/gluon/config-mode/geo-location-osm.lua
@@ -0,0 +1,26 @@
+local osm = require 'gluon.web.model.osm'
+local site = require 'gluon.site'
+
+local tonumber = tonumber
+
+
+module 'gluon.config-mode.geo-location-osm'
+
+MapValue = osm.MapValue
+
+function help(i18n)
+	local pkg_i18n = i18n 'gluon-config-mode-geo-location-osm'
+	return pkg_i18n.translate(
+		'You may also select the position on the map displayed below if your computer is connected to the internet at the moment.'
+	)
+end
+
+function options()
+	local config = site.config_mode.geo_location.osm
+
+	return {
+		openlayers_url = config.openlayers_url(),
+		zoom = config.zoom(12),
+		pos = config.center(),
+	}
+end
diff --git a/package/gluon-config-mode-geo-location/i18n/de.po b/package/gluon-config-mode-geo-location/i18n/de.po
index 1ef10a2b06b81ae427f549ef931c40fd964faaa9..0ff3d43394d0a99518d4a578017f7e96cd2eabdb 100644
--- a/package/gluon-config-mode-geo-location/i18n/de.po
+++ b/package/gluon-config-mode-geo-location/i18n/de.po
@@ -14,11 +14,11 @@ msgid "Altitude"
 msgstr "Höhe"
 
 msgid ""
-"If you want the location of your node to be displayed on the map, you can "
-"enter its coordinates here."
+"If you want the location of your node to be displayed on public maps, you "
+"can enter its coordinates here."
 msgstr ""
-"Um deinen Knoten auf der Karte anzeigen zu können, benötigen wir seine "
-"Koordinaten. Hier hast du die Möglichkeit, diese zu hinterlegen."
+"Um deinen Knoten auf öffentlichen Karten anzeigen zu können, benötigen wir "
+"seine Koordinaten. Hier hast du die Möglichkeit, diese zu hinterlegen."
 
 msgid "Latitude"
 msgstr "Breitengrad"
@@ -26,8 +26,8 @@ msgstr "Breitengrad"
 msgid "Longitude"
 msgstr "Längengrad"
 
-msgid "Show node on the map"
-msgstr "Knoten auf der Karte anzeigen"
+msgid "Advertise node position"
+msgstr "Knotenposition veröffentlichen"
 
 msgid ""
 "Specifying the altitude is optional; it should only be filled in if an "
diff --git a/package/gluon-config-mode-geo-location/i18n/fr.po b/package/gluon-config-mode-geo-location/i18n/fr.po
index b66aacb2167cd5b06eac3ba4e8793a161e2b5893..5a725cb68b82a1968432bd45225faa821b46d385 100644
--- a/package/gluon-config-mode-geo-location/i18n/fr.po
+++ b/package/gluon-config-mode-geo-location/i18n/fr.po
@@ -14,11 +14,9 @@ msgid "Altitude"
 msgstr "Hauteur"
 
 msgid ""
-"If you want the location of your node to be displayed on the map, you can "
-"enter its coordinates here."
+"If you want the location of your node to be displayed on public maps, you "
+"can enter its coordinates here."
 msgstr ""
-"Pour Afficher votre nœud sur la Carte nous avons besoin de ses coordonnées. "
-"Ici vous pouvez entrer sa position."
 
 msgid "Latitude"
 msgstr "Latitude"
@@ -26,8 +24,8 @@ msgstr "Latitude"
 msgid "Longitude"
 msgstr "Longitude"
 
-msgid "Show node on the map"
-msgstr "Afficher le nœud sur la carte"
+msgid "Advertise node position"
+msgstr ""
 
 msgid ""
 "Specifying the altitude is optional; it should only be filled in if an "
diff --git a/package/gluon-config-mode-geo-location/i18n/gluon-config-mode-geo-location.pot b/package/gluon-config-mode-geo-location/i18n/gluon-config-mode-geo-location.pot
index 04bbd3648e7b1dd36af644df99cdacc0e4977421..27ab905d6a2030c800db88daf472d68e11e264da 100644
--- a/package/gluon-config-mode-geo-location/i18n/gluon-config-mode-geo-location.pot
+++ b/package/gluon-config-mode-geo-location/i18n/gluon-config-mode-geo-location.pot
@@ -5,8 +5,8 @@ msgid "Altitude"
 msgstr ""
 
 msgid ""
-"If you want the location of your node to be displayed on the map, you can "
-"enter its coordinates here."
+"If you want the location of your node to be displayed on public maps, you "
+"can enter its coordinates here."
 msgstr ""
 
 msgid "Latitude"
@@ -15,7 +15,7 @@ msgstr ""
 msgid "Longitude"
 msgstr ""
 
-msgid "Show node on the map"
+msgid "Advertise node position"
 msgstr ""
 
 msgid ""
diff --git a/package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua b/package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua
index 2e5111bd02e42021aa3eb8281d4bb843d603495b..6a211b643c9771f8e06d378a3bfa603632110360 100644
--- a/package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua
+++ b/package/gluon-config-mode-geo-location/luasrc/lib/gluon/config-mode/wizard/0400-geo-location.lua
@@ -4,6 +4,9 @@ return function(form, uci)
 
 	local site = require 'gluon.site'
 
+	local osm
+	pcall(function() osm = require 'gluon.config-mode.geo-location-osm' end)
+
 	local location = uci:get_first("gluon-node-info", "location")
 
 	local show_altitude = site.config_mode.geo_location.show_altitude(false)
@@ -14,6 +17,9 @@ return function(form, uci)
 			'If you want the location of your node to ' ..
 			'be displayed on the map, you can enter its coordinates here.'
 		)
+		if osm then
+			text = text .. ' ' .. osm.help(i18n)
+		end
 		if show_altitude then
 			text = text .. ' ' .. pkg_i18n.translate(
 				'Specifying the altitude is optional; it should only be filled in if an accurate ' ..
@@ -26,7 +32,7 @@ return function(form, uci)
 
 	local o
 
-	local share_location = s:option(Flag, "location", pkg_i18n.translate("Show node on the map"))
+	local share_location = s:option(Flag, "location", pkg_i18n.translate("Advertise node position"))
 	share_location.default = uci:get_bool("gluon-node-info", location, "share_location")
 	function share_location:write(data)
 		uci:set("gluon-node-info", location, "share_location", data)
@@ -37,6 +43,12 @@ return function(form, uci)
 		end
 	end
 
+	local map = {}
+	if osm then
+		map = s:option(osm.MapValue, "map", osm.options())
+		map:depends(share_location, true)
+	end
+
 	o = s:option(Value, "latitude", pkg_i18n.translate("Latitude"), pkg_i18n.translatef("e.g. %s", "53.873621"))
 	o.default = uci:get("gluon-node-info", location, "latitude")
 	o:depends(share_location, true)
@@ -44,6 +56,7 @@ return function(form, uci)
 	function o:write(data)
 		uci:set("gluon-node-info", location, "latitude", data)
 	end
+	map.lat = o
 
 	o = s:option(Value, "longitude", pkg_i18n.translate("Longitude"), pkg_i18n.translatef("e.g. %s", "10.689901"))
 	o.default = uci:get("gluon-node-info", location, "longitude")
@@ -52,6 +65,7 @@ return function(form, uci)
 	function o:write(data)
 		uci:set("gluon-node-info", location, "longitude", data)
 	end
+	map.lon = o
 
 	if show_altitude then
 		o = s:option(Value, "altitude",
diff --git a/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css b/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
index 3547371112585582da18fba37d2f4734a49b2572..bbb82a8ab492117f4cafbee34d256af3a0076abe 100644
--- a/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
+++ b/package/gluon-config-mode-theme/files/lib/gluon/config-mode/www/static/gluon.css
@@ -1 +1 @@
-.lang_he{direction:RTL;unicode-bidi:embed}.hidden{display:none}html{min-height:100%;height:auto;position:relative}body,input,select,option{font-family:'Open Sans', Arial, sans-serif;font-size:12pt}body{color:#4d4e53;line-height:1.5em;margin:0;display:flex;flex-direction:column;min-height:100vh;background-color:#f3f3f3}a img{border:none;text-decoration:none}.tabmenu1{text-align:center}ul.tabmenu{list-style:none;padding:0;margin:2em 0;display:inline-flex}ul.tabmenu li{white-space:nowrap;margin:0 0.5em;padding:0;text-align:center}ul.tabmenu li a{display:block;text-decoration:none;padding:1em;margin:0;color:#333;border-radius:2em}ul.tabmenu li a:hover{background:#ffe9b3}ul.tabmenu li.active a{font-weight:bold;background:white;color:#333}abbr,acronym{font-style:normal;font-variant:normal}abbr[title],acronym[title]{border-bottom:1px dotted;cursor:help}a:link abbr[title],a:visited abbr[title],a:link acronym[title],a:visited acronym[title]{cursor:pointer}code{font-family:monospace;white-space:pre}#maincontent ul{margin-left:2em}.clear{clear:both}.error{color:#ff0000;background-color:white}#menubar{display:flex;background:#dc0067;color:#ffffff}#menubar a:link.topcat,#menubar a:visited.topcat{position:relative;display:block;padding:0.5em;text-decoration:none;font-size:80%;font-weight:normal;color:white}#menubar a:link.topcat:hover,#menubar a:link.topcat:focus,#menubar a:visited.topcat:hover,#menubar a:visited.topcat:focus{background:#ffb400;color:black}#menubar a:link.topcat.active,#menubar a:visited.topcat.active{background:#ffb400;color:black;font-weight:bold}#menubar div.hostinfo{position:relative;margin:0;padding:0.5em;flex:1;font-weight:bold;font-size:80%}#menubar div.hostinfo a:link,#menubar div.hostinfo a:visited{text-decoration:none;font-weight:bold;color:white}#menubar div.hostinfo a:link:hover,#menubar div.hostinfo a:link:focus,#menubar div.hostinfo a:visited:hover,#menubar div.hostinfo a:visited:focus{text-decoration:underline}#topmenu{list-style:none;margin:0;padding:0}#topmenu li{display:inline-block}#maincontent{padding:0 1em 2em;max-width:60em;min-width:40em;margin:1em auto}#maincontent p{margin-bottom:1em}.gluon-section{margin:0;padding:0;border:none;margin-bottom:1.3em}.gluon-section:last-child{margin-bottom:0.7em}.gluon-section legend{font-size:1.4em;font-weight:bold;position:relative;padding:0;margin-bottom:0.5em}.gluon-section h2{margin:0em 0 0.5em -0.5em !important}.gluon-section h3{text-decoration:none !important;font-weight:bold !important;color:#555555 !important;margin:0.25em !important;font-size:100% !important}.gluon-section-descr{margin-bottom:2em}input:placeholder{color:#aaaaaa}input:-webkit-input-placeholder{color:#aaaaaa}input:-moz-placeholder{color:#aaaaaa}input:-ms-input-placeholder{color:#aaaaaa}input[type=checkbox]{display:none}input[type=checkbox]+label{display:inline-block;position:relative;width:1em;height:1em;margin:0}input[type=checkbox]:checked+label:after{content:'✔';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.5em;width:100%;text-align:center;font-size:1.7em}input[type=radio]{display:none}input[type=radio]+label{display:inline-block;position:relative;width:0.8em;height:0.8em;padding:0.5em;margin:0.2em 0.2em 0.2em 0.1em;border:none;background:#ffe199;vertical-align:middle;border-radius:50%}input[type=radio]:checked+label:after{content:'•';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.4em;width:100%;text-align:center;font-size:2em}input[type=submit],input[type=reset],input[type=image],input[type=button]{cursor:pointer}select,input,textarea,input[type=checkbox]+label{color:#003247;border:none;background:#ffe199;border-radius:3pt;padding:0.5em;margin-top:1px;margin-bottom:2px;box-sizing:content-box;outline:0}option{color:#003247;background:#ffe199}input[type=image]{border:none}select,input[type=text],input[type=password]{min-width:20em}input.gluon-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;user-select:none;font-size:100%;padding:0.5em 1em;color:rgba(0,0,0,0.8);border:none transparent;background-color:#E6E6E6;text-decoration:none;border-radius:2px;transition:0.1s linear box-shadow;margin-left:0.5em;background-repeat:no-repeat}input.gluon-button::-moz-focus-inner{padding:0;border:0}input.gluon-button:active{box-shadow:0 0 0 1px rgba(0,0,0,0.15) inset,0 0 6px rgba(0,0,0,0.2) inset}input.gluon-button:focus{outline:0}input.gluon-button:hover,input.gluon-button:focus{background-image:linear-gradient(transparent, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1))}input.gluon-button[disabled]{border:none;background-image:none;opacity:0.40;cursor:not-allowed;box-shadow:none}input.gluon-button-reset{background-color:#e30;color:#fff}input.gluon-button-submit{background-color:#009ee0;color:#fff}input.gluon-button-submit:active{background:grey}.gluon-input-invalid{background:#e30 !important;color:white}div.gluon-section-remove input{border-bottom:none}textarea{margin-left:-1px;margin-bottom:0.5em}.gluon-section .gluon-rowstyle-1 h3{background-color:#eeeeff;color:#555555}.gluon-rowstyle-2{color:#000000}div.gluon-value{display:flex;flex-direction:row;margin-bottom:0.5em}.gluon-section-node .gluon-value:last-child{margin-bottom:0}.gluon-value-title{flex:2;text-align:right;padding-top:0.39em;padding-right:1em;font-weight:bold}div.gluon-value-field{flex:3;position:relative}div.gluon-value-field input,div.gluon-value-field select,div.gluon-value-field input+label{position:relative}div.gluon-value-field-text{flex:3;padding-top:0.39em}div.gluon-value-field-long{flex:10;position:relative;margin-top:0.65em}div.gluon-value-field-long input,div.gluon-value-field-long select,div.gluon-value-field-long input+label{position:relative}div.gluon-value-field-long-after{flex:2}div.gluon-value-description{font-size:8pt}div.gluon-section-create{clear:left;white-space:nowrap;vertical-align:top}div.gluon-section-create .gluon-button{margin:0.25em}input.gluon-section-create-name{margin-right:-0.25em}div.gluon-map-descr{margin-bottom:1em}.gluon-map-descr:empty,.gluon-section-descr:empty{display:none}.gluon-map-descr,.gluon-section-descr,.gluon-page-actions{padding:1em;background:#ececec}.gluon-page-actions{text-align:right;display:flex;flex-flow:row-reverse}div.gluon-optionals{padding:0.25em;border-bottom:1px dotted #bbbbbb}div.gluon-section-remove{float:right}.gluon-section-node{clear:both;position:relative;border:none}.gluon-section-node-tabbed{border-top-left-radius:0}div.gluon-error{font-size:95%;font-weight:bold;color:#ff0000;background-color:#ffffff}.gluon-value-error input,.gluon-value-error select{background-color:#ffcccc}.gluon-section-error{color:red;background-color:white;font-size:95%;border:1px dotted red;margin:3px;padding:3px}.gluon-value-field var{color:#2222FF}.gluon-add:after,.gluon-remove:after{cursor:pointer;display:inline-block;text-align:center;vertical-align:middle;font-size:180%;width:1.2em;height:1em}.gluon-add{color:#008000;position:relative;left:21em}input+.gluon-add{left:0;top:0.04em}.gluon-add:first-child{top:0.53em;left:-0.08em}.gluon-add:after{content:'+'}.gluon-remove{color:#800000;position:relative;top:-0.03em}.gluon-remove:after{content:'–'}.left{text-align:left !important}.right{text-align:right !important}.inline{display:inline}.error500{border:1px dotted #ff0000;background-color:#ffffff;color:#000000;padding:0.5em}.errorbox{border:1px solid #FF0000;background-color:#FFCCCC;padding:5px;margin-bottom:5px}.errorbox a{color:#000000 !important}.the-key{text-align:left;font-size:1.4em;background:#ffe9b3;border:3pt dashed #dc0067;margin-bottom:0.5em;padding:0.5em}
+.lang_he{direction:RTL;unicode-bidi:embed}.hidden{display:none}html{min-height:100%;height:auto;position:relative}body,input,select,option{font-family:'Open Sans', Arial, sans-serif;font-size:12pt}body{color:#4d4e53;line-height:1.5em;margin:0;display:flex;flex-direction:column;min-height:100vh;background-color:#f3f3f3}a img{border:none;text-decoration:none}.tabmenu1{text-align:center}ul.tabmenu{list-style:none;padding:0;margin:2em 0;display:inline-flex}ul.tabmenu li{white-space:nowrap;margin:0 0.5em;padding:0;text-align:center}ul.tabmenu li a{display:block;text-decoration:none;padding:1em;margin:0;color:#333;border-radius:2em}ul.tabmenu li a:hover{background:#ffe9b3}ul.tabmenu li.active a{font-weight:bold;background:white;color:#333}abbr,acronym{font-style:normal;font-variant:normal}abbr[title],acronym[title]{border-bottom:1px dotted;cursor:help}a:link abbr[title],a:visited abbr[title],a:link acronym[title],a:visited acronym[title]{cursor:pointer}code{font-family:monospace;white-space:pre}#maincontent ul{margin-left:2em}.clear{clear:both}.error{color:#ff0000;background-color:white}#menubar{display:flex;background:#dc0067;color:#ffffff}#menubar a:link.topcat,#menubar a:visited.topcat{position:relative;display:block;padding:0.5em;text-decoration:none;font-size:80%;font-weight:normal;color:white}#menubar a:link.topcat:hover,#menubar a:link.topcat:focus,#menubar a:visited.topcat:hover,#menubar a:visited.topcat:focus{background:#ffb400;color:black}#menubar a:link.topcat.active,#menubar a:visited.topcat.active{background:#ffb400;color:black;font-weight:bold}#menubar div.hostinfo{position:relative;margin:0;padding:0.5em;flex:1;font-weight:bold;font-size:80%}#menubar div.hostinfo a:link,#menubar div.hostinfo a:visited{text-decoration:none;font-weight:bold;color:white}#menubar div.hostinfo a:link:hover,#menubar div.hostinfo a:link:focus,#menubar div.hostinfo a:visited:hover,#menubar div.hostinfo a:visited:focus{text-decoration:underline}#topmenu{list-style:none;margin:0;padding:0}#topmenu li{display:inline-block}#maincontent{padding:0 1em 2em;max-width:60em;min-width:40em;margin:1em auto}#maincontent p{margin-bottom:1em}.gluon-section{margin:0;padding:0;border:none;margin-bottom:1.3em}.gluon-section:last-child{margin-bottom:0.7em}.gluon-section legend{font-size:1.4em;font-weight:bold;position:relative;padding:0;margin-bottom:0.5em}.gluon-section h2{margin:0em 0 0.5em -0.5em !important}.gluon-section h3{text-decoration:none !important;font-weight:bold !important;color:#555555 !important;margin:0.25em !important;font-size:100% !important}.gluon-section-descr{margin-bottom:2em}.gluon-osm-map{width:100%;height:40em;margin-bottom:1em}input:placeholder{color:#aaaaaa}input:-webkit-input-placeholder{color:#aaaaaa}input:-moz-placeholder{color:#aaaaaa}input:-ms-input-placeholder{color:#aaaaaa}input[type=checkbox]{display:none}input[type=checkbox]+label{display:inline-block;position:relative;width:1em;height:1em;margin:0}input[type=checkbox]:checked+label:after{content:'✔';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.5em;width:100%;text-align:center;font-size:1.7em}input[type=radio]{display:none}input[type=radio]+label{display:inline-block;position:relative;width:0.8em;height:0.8em;padding:0.5em;margin:0.2em 0.2em 0.2em 0.1em;border:none;background:#ffe199;vertical-align:middle;border-radius:50%}input[type=radio]:checked+label:after{content:'•';color:#dc0067;vertical-align:middle;position:absolute;top:50%;left:0;margin-top:-0.4em;width:100%;text-align:center;font-size:2em}input[type=submit],input[type=reset],input[type=image],input[type=button]{cursor:pointer}select,input,textarea,input[type=checkbox]+label{color:#003247;border:none;background:#ffe199;border-radius:3pt;padding:0.5em;margin-top:1px;margin-bottom:2px;box-sizing:content-box;outline:0}option{color:#003247;background:#ffe199}input[type=image]{border:none}select,input[type=text],input[type=password]{min-width:20em}input.gluon-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;user-select:none;font-size:100%;padding:0.5em 1em;color:rgba(0,0,0,0.8);border:none transparent;background-color:#E6E6E6;text-decoration:none;border-radius:2px;transition:0.1s linear box-shadow;margin-left:0.5em;background-repeat:no-repeat}input.gluon-button::-moz-focus-inner{padding:0;border:0}input.gluon-button:active{box-shadow:0 0 0 1px rgba(0,0,0,0.15) inset,0 0 6px rgba(0,0,0,0.2) inset}input.gluon-button:focus{outline:0}input.gluon-button:hover,input.gluon-button:focus{background-image:linear-gradient(transparent, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.1))}input.gluon-button[disabled]{border:none;background-image:none;opacity:0.40;cursor:not-allowed;box-shadow:none}input.gluon-button-reset{background-color:#e30;color:#fff}input.gluon-button-submit{background-color:#009ee0;color:#fff}input.gluon-button-submit:active{background:grey}.gluon-input-invalid{background:#e30 !important;color:white}div.gluon-section-remove input{border-bottom:none}textarea{margin-left:-1px;margin-bottom:0.5em}.gluon-section .gluon-rowstyle-1 h3{background-color:#eeeeff;color:#555555}.gluon-rowstyle-2{color:#000000}div.gluon-value{display:flex;flex-direction:row;margin-bottom:0.5em}.gluon-section-node .gluon-value:last-child{margin-bottom:0}.gluon-value-title{flex:2;text-align:right;padding-top:0.39em;padding-right:1em;font-weight:bold}div.gluon-value-field{flex:3;position:relative}div.gluon-value-field input,div.gluon-value-field select,div.gluon-value-field input+label{position:relative}div.gluon-value-field-text{flex:3;padding-top:0.39em}div.gluon-value-field-long{flex:10;position:relative;margin-top:0.65em}div.gluon-value-field-long input,div.gluon-value-field-long select,div.gluon-value-field-long input+label{position:relative}div.gluon-value-field-long-after{flex:2}div.gluon-value-description{font-size:8pt}div.gluon-section-create{clear:left;white-space:nowrap;vertical-align:top}div.gluon-section-create .gluon-button{margin:0.25em}input.gluon-section-create-name{margin-right:-0.25em}div.gluon-form-descr{margin-bottom:1em}.gluon-form-descr:empty,.gluon-section-descr:empty{display:none}.gluon-form-descr,.gluon-section-descr,.gluon-page-actions{padding:1em;background:#ececec}.gluon-page-actions{text-align:right;display:flex;flex-flow:row-reverse}div.gluon-optionals{padding:0.25em;border-bottom:1px dotted #bbbbbb}div.gluon-section-remove{float:right}.gluon-section-node{clear:both;position:relative;border:none}.gluon-section-node-tabbed{border-top-left-radius:0}div.gluon-error{font-size:95%;font-weight:bold;color:#ff0000;background-color:#ffffff}.gluon-value-error input,.gluon-value-error select{background-color:#ffcccc}.gluon-section-error{color:red;background-color:white;font-size:95%;border:1px dotted red;margin:3px;padding:3px}.gluon-value-field var{color:#2222FF}.gluon-add:after,.gluon-remove:after{cursor:pointer;display:inline-block;text-align:center;vertical-align:middle;font-size:180%;width:1.2em;height:1em}.gluon-add{color:#008000;position:relative;left:21em}input+.gluon-add{left:0;top:0.04em}.gluon-add:first-child{top:0.53em;left:-0.08em}.gluon-add:after{content:'+'}.gluon-remove{color:#800000;position:relative;top:-0.03em}.gluon-remove:after{content:'–'}.left{text-align:left !important}.right{text-align:right !important}.inline{display:inline}.error500{border:1px dotted #ff0000;background-color:#ffffff;color:#000000;padding:0.5em}.errorbox{border:1px solid #FF0000;background-color:#FFCCCC;padding:5px;margin-bottom:5px}.errorbox a{color:#000000 !important}.the-key{text-align:left;font-size:1.4em;background:#ffe9b3;border:3pt dashed #dc0067;margin-bottom:0.5em;padding:0.5em}
diff --git a/package/gluon-config-mode-theme/sass/gluon.scss b/package/gluon-config-mode-theme/sass/gluon.scss
index 3aa4350a1eda6241252bfa316bf10a82665d73c7..e534995f08a3536cd09c9c7f66b496b82c41b6a3 100644
--- a/package/gluon-config-mode-theme/sass/gluon.scss
+++ b/package/gluon-config-mode-theme/sass/gluon.scss
@@ -284,6 +284,12 @@ code {
   margin-bottom: 2em;
 }
 
+.gluon-osm-map {
+  width: 100%;
+  height: 40em;
+  margin-bottom: 1em;
+}
+
 input:placeholder {
   color: #aaaaaa;
 }
@@ -498,15 +504,15 @@ input.gluon-section-create-name {
   margin-right: -0.25em;
 }
 
-div.gluon-map-descr {
+div.gluon-form-descr {
   margin-bottom: 1em;
 }
 
-.gluon-map-descr:empty, .gluon-section-descr:empty {
+.gluon-form-descr:empty, .gluon-section-descr:empty {
   display: none;
 }
 
-.gluon-map-descr, .gluon-section-descr, .gluon-page-actions {
+.gluon-form-descr, .gluon-section-descr, .gluon-page-actions {
   padding: 1em;
   background: #ececec;
 }
diff --git a/package/gluon-web-model/files/lib/gluon/web/view/model/form.html b/package/gluon-web-model/files/lib/gluon/web/view/model/form.html
index 1b642b0dcba166e35d66c1d9c2a61905aad50c45..fde1b25072314499025e04a5709ef162b2a27dc9 100644
--- a/package/gluon-web-model/files/lib/gluon/web/view/model/form.html
+++ b/package/gluon-web-model/files/lib/gluon/web/view/model/form.html
@@ -2,9 +2,9 @@
 	<input type="hidden" name="token" value="<%=token%>" />
 	<input type="hidden" name="<%=id%>" value="1" />
 
-	<div class="gluon-map" id="gluon-<%=self.config%>">
+	<div class="gluon-form" id="form-<%=id%>">
 		<% if self.title and #self.title > 0 then %><h2 name="content"><%|self.title%></h2><% end %>
-		<% if self.description and #self.description > 0 then %><div class="gluon-map-descr"><%=self.description%></div><% end %>
+		<% if self.description and #self.description > 0 then %><div class="gluon-form-descr"><%=self.description%></div><% end %>
 		<% self:render_children(renderer) %>
 	</div>
 <%- if self.message then %>
diff --git a/package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js b/package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js
index d71e1d7751756a725801afcaa2b507713d46b826..16a9ac528142738692103638ef8b4aa61f98a17d 100644
--- a/package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js
+++ b/package/gluon-web-model/files/lib/gluon/web/www/static/gluon-web-model.js
@@ -1 +1 @@
-!function(){var e={};function t(e){return/^-?\d+$/.test(e)?+e:NaN}function n(e){return/^-?\d*\.?\d+?$/.test(e)?+e:NaN}var a={integer:function(){return!isNaN(t(this))},uinteger:function(){return t(this)>=0},float:function(){return!isNaN(n(this))},ufloat:function(){return n(this)>=0},ipaddr:function(){return a.ip4addr.apply(this)||a.ip6addr.apply(this)},ip4addr:function(){var e;return!!(e=this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))&&(e[1]>=0&&e[1]<=255&&e[2]>=0&&e[2]<=255&&e[3]>=0&&e[3]<=255&&e[4]>=0&&e[4]<=255)},ip6addr:function(){return this.indexOf("::")<0?null!=this.match(/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i):!(this.indexOf(":::")>=0||this.match(/::.+::/)||this.match(/^:[^:]/)||this.match(/[^:]:$/))&&(!!this.match(/^(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}$/i)||(!!this.match(/^(?:[a-f0-9]{1,4}:){7}:$/i)||!!this.match(/^:(?::[a-f0-9]{1,4}){7}$/i)))},wpakey:function(){var e=this;return 64==e.length?null!=e.match(/^[a-f0-9]{64}$/i):e.length>=8&&e.length<=63},range:function(e,t){var a=n(this);return a>=+e&&a<=+t},min:function(e){return n(this)>=+e},max:function(e){return n(this)<=+e},irange:function(e,n){var a=t(this);return a>=+e&&a<=+n},imin:function(e){return t(this)>=+e},imax:function(e){return t(this)<=+e},minlength:function(e){return(""+this).length>=+e},maxlength:function(e){return(""+this).length<=+e}};function r(e){for(var t=0;t<e.length;t++){var n=!0;for(var a in e[t])n=n&&(r=a,i=e[t][a],o=void 0,(o=document.getElementById(r))?("checkbox"==o.type?o.checked:o.value?o.value:"")==i:!!(o=document.getElementById(r+"."+i))&&"radio"==o.type&&o.checked);if(n)return!0}var r,i,o;return!1}function i(){var t=!1;for(var n in e){var a=e[n],o=document.getElementById(n),d=document.getElementById(a.parent);if(o&&o.parentNode&&!r(a.deps))o.parentNode.removeChild(o),t=!0;else if(d&&(!o||!o.parentNode)&&r(a.deps)){var u=void 0;for(u=d.firstChild;u&&!(u.getAttribute&&parseInt(u.getAttribute("data-index"),10)>a.index);u=u.nextSibling);u?d.insertBefore(a.node,u):d.appendChild(a.node),t=!0}d&&d.parentNode&&d.getAttribute("data-optionals")&&(d.parentNode.style.display=d.options.length<=1?"none":"")}t&&i()}function o(e,t,n,a){return e.addEventListener?e.addEventListener(t,n,!!a):e.attachEvent("on"+t,function(){var e=window.event;return!e.target&&e.srcElement&&(e.target=e.srcElement),!!n(e)}),e}function d(e,t){var n=t.prefix;function a(a,l,s){for(var c=[];e.firstChild;){var p=e.firstChild;(f=+p.index)!=s&&("input"==p.nodeName.toLowerCase()?c.push(p.value||""):"select"==p.nodeName.toLowerCase()&&(c[c.length-1]=p.options[p.selectedIndex].value)),e.removeChild(p)}l>=0?(a=l+1,c.splice(l,0,"")):t.optional||0!=c.length||c.push("");for(var f=1;f<=c.length;f++){var v=document.createElement("input");if(v.id=n+"."+f,v.name=n,v.value=c[f-1],v.type="text",v.index=f,v.className="gluon-input-text",t.size&&(v.size=t.size),t.placeholder&&(v.placeholder=t.placeholder),e.appendChild(v),t.type&&u(v,!1,t.type),o(v,"keydown",i),o(v,"keypress",r),f==a)v.focus();else if(-f==a){v.focus();var h=v.value;v.value=" ",v.value=h}if(t.optional||c.length>1)(m=document.createElement("span")).className="gluon-remove",e.appendChild(m),o(m,"click",d(!1)),e.appendChild(document.createElement("br"))}var m;(m=document.createElement("span")).className="gluon-add",e.appendChild(m),o(m,"click",d(!0))}function r(e){var t=(e=e||window.event).target?e.target:e.srcElement;switch(3==t.nodeType&&(t=t.parentNode),e.keyCode){case 8:case 46:return 0!=t.value.length||(e.preventDefault&&e.preventDefault(),!1);case 13:case 38:case 40:return e.preventDefault&&e.preventDefault(),!1}return!0}function i(e){var t,r,i=(e=e||window.event).target?e.target:e.srcElement,o=0;if(i){for(3==i.nodeType&&(i=i.parentNode),o=i.index,t=i.previousSibling;t&&t.name!=n;)t=t.previousSibling;for(r=i.nextSibling;r&&r.name!=n;)r=r.nextSibling}switch(e.keyCode){case 8:case 46:if("select"==i.nodeName.toLowerCase()||0==i.value.length){e.preventDefault&&e.preventDefault();var d=i.index;return 8==e.keyCode&&(d=1-d),a(d,-1,o),!1}break;case 13:a(-1,o,-1);break;case 38:t&&t.focus();break;case 40:r&&r.focus()}return!0}function d(e){return function(t){for(var a=((t=t||window.event).target?t.target:t.srcElement).previousSibling;a&&a.name!=n;)a=a.previousSibling;return e?i({target:a,keyCode:13}):(a.value="",i({target:a,keyCode:8})),!1}}a(NaN,-1,-1)}function u(e,t,n){var r,i,d,u=(d=(r=n).match(/^([^\(]+)\(([^,]+),([^\)]+)\)$/))&&void 0!==(i=a[d[1]])?function(){return i.apply(this,[d[2],d[3]])}:(d=r.match(/^([^\(]+)\(([^,\)]+)\)$/))&&void 0!==(i=a[d[1]])?function(){return i.apply(this,[d[2]])}:a[r];if(u){var l=function(){if(e.form){e.className=e.className.replace(/ gluon-input-invalid/g,"");var n=e.options&&e.options.selectedIndex>-1?e.options[e.options.selectedIndex].value:e.value;0==n.length&&t||u.apply(n)||(e.className+=" gluon-input-invalid")}};o(e,"blur",l),o(e,"keyup",l),"select"==e.nodeName.toLowerCase()&&(o(e,"change",l),o(e,"click",l)),l()}}!function(){var t,n,a,r,l;t=document.querySelectorAll("[data-depends]");for(var s=0;void 0!==(g=t[s]);s++){var c=parseInt(g.getAttribute("data-index"),10),p=JSON.parse(g.getAttribute("data-depends"));if(!isNaN(c)&&p.length>0)for(var f=0;f<p.length;f++)n=g,a=p[f],r=c,l=void 0,(l=e[n.id])||(l={node:n,parent:n.parentNode.id,deps:[],index:r},e[n.id]=l),l.deps.push(a)}t=document.querySelectorAll("[data-update]");for(s=0;void 0!==(g=t[s]);s++)for(var v,h=g.getAttribute("data-update").split(" "),m=0;void 0!==(v=h[m]);m++)o(g,v,i);t=document.querySelectorAll("[data-type]");for(s=0;void 0!==(g=t[s]);s++)u(g,"true"===g.getAttribute("data-optional"),g.getAttribute("data-type"));t=document.querySelectorAll("[data-dynlist]");var g;for(s=0;void 0!==(g=t[s]);s++){d(g,JSON.parse(g.getAttribute("data-dynlist")))}i()}()}();
\ No newline at end of file
+!function(){var f={};function a(e){return/^-?\d+$/.test(e)?+e:NaN}function r(e){return/^-?\d*\.?\d+?$/.test(e)?+e:NaN}var u={integer:function(){return!isNaN(a(this))},uinteger:function(){return 0<=a(this)},float:function(){return!isNaN(r(this))},ufloat:function(){return 0<=r(this)},ipaddr:function(){return u.ip4addr.apply(this)||u.ip6addr.apply(this)},ip4addr:function(){var e;return!!(e=this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))&&(0<=e[1]&&e[1]<=255&&0<=e[2]&&e[2]<=255&&0<=e[3]&&e[3]<=255&&0<=e[4]&&e[4]<=255)},ip6addr:function(){return this.indexOf("::")<0?null!=this.match(/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i):!(0<=this.indexOf(":::")||this.match(/::.+::/)||this.match(/^:[^:]/)||this.match(/[^:]:$/))&&(!!this.match(/^(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}$/i)||(!!this.match(/^(?:[a-f0-9]{1,4}:){7}:$/i)||!!this.match(/^:(?::[a-f0-9]{1,4}){7}$/i)))},wpakey:function(){var e=this;return 64==e.length?null!=e.match(/^[a-f0-9]{64}$/i):8<=e.length&&e.length<=63},range:function(e,t){var n=r(this);return+e<=n&&n<=+t},min:function(e){return r(this)>=+e},max:function(e){return r(this)<=+e},irange:function(e,t){var n=a(this);return+e<=n&&n<=+t},imin:function(e){return a(this)>=+e},imax:function(e){return a(this)<=+e},minlength:function(e){return+e<=(""+this).length},maxlength:function(e){return(""+this).length<=+e}};function o(e){for(var t=0;t<e.length;t++){var n=!0;for(var a in e[t])n=n&&(r=a,i=e[t][a],o=void 0,(o=document.getElementById(r))?("checkbox"==o.type?o.checked:o.value?o.value:"")==i:!!(o=document.getElementById(r+"."+i))&&"radio"==o.type&&o.checked);if(n)return!0}var r,i,o;return!1}function v(){window.dispatchEvent(new Event("gluon-update"));var e=!1;for(var t in f){var n=f[t],a=document.getElementById(t),r=document.getElementById(n.parent);if(a&&a.parentNode&&!o(n.deps))a.parentNode.removeChild(a),a.dispatchEvent(new Event("gluon-hide")),e=!0;else if(r&&(!a||!a.parentNode)&&o(n.deps)){var i=void 0;for(i=r.firstChild;i&&!(i.getAttribute&&parseInt(i.getAttribute("data-index"),10)>n.index);i=i.nextSibling);i?r.insertBefore(n.node,i):r.appendChild(n.node),n.node.dispatchEvent(new Event("gluon-show")),e=!0}r&&r.parentNode&&r.getAttribute("data-optionals")&&(r.parentNode.style.display=r.options.length<=1?"none":"")}e&&v()}function h(e,t,n,a){return e.addEventListener?e.addEventListener(t,n,!!a):e.attachEvent("on"+t,function(){var e=window.event;return!e.target&&e.srcElement&&(e.target=e.srcElement),!!n(e)}),e}function g(l,s){var c=s.prefix;function o(e,t,n){for(var a=[];l.firstChild;){var r=l.firstChild;(i=+r.index)!=n&&("input"==r.nodeName.toLowerCase()?a.push(r.value||""):"select"==r.nodeName.toLowerCase()&&(a[a.length-1]=r.options[r.selectedIndex].value)),l.removeChild(r)}0<=t?(e=t+1,a.splice(t,0,"")):s.optional||0!=a.length||a.push("");for(var i=1;i<=a.length;i++){var o=document.createElement("input");if(o.id=c+"."+i,o.name=c,o.value=a[i-1],o.type="text",o.index=i,o.className="gluon-input-text",s.size&&(o.size=s.size),s.placeholder&&(o.placeholder=s.placeholder),l.appendChild(o),s.type&&m(o,!1,s.type),h(o,"keydown",f),h(o,"keypress",p),i==e)o.focus();else if(-i==e){o.focus();var d=o.value;o.value=" ",o.value=d}if(s.optional||1<a.length)(u=document.createElement("span")).className="gluon-remove",l.appendChild(u),h(u,"click",v(!1)),l.appendChild(document.createElement("br"))}var u;(u=document.createElement("span")).className="gluon-add",l.appendChild(u),h(u,"click",v(!0))}function p(e){var t=(e=e||window.event).target?e.target:e.srcElement;switch(3==t.nodeType&&(t=t.parentNode),e.keyCode){case 8:case 46:return 0!=t.value.length||(e.preventDefault&&e.preventDefault(),!1);case 13:case 38:case 40:return e.preventDefault&&e.preventDefault(),!1}return!0}function f(e){var t,n,a=(e=e||window.event).target?e.target:e.srcElement,r=0;if(a){for(3==a.nodeType&&(a=a.parentNode),r=a.index,t=a.previousSibling;t&&t.name!=c;)t=t.previousSibling;for(n=a.nextSibling;n&&n.name!=c;)n=n.nextSibling}switch(e.keyCode){case 8:case 46:if("select"==a.nodeName.toLowerCase()||0==a.value.length){e.preventDefault&&e.preventDefault();var i=a.index;return 8==e.keyCode&&(i=1-i),o(i,-1,r),!1}break;case 13:o(-1,r,-1);break;case 38:t&&t.focus();break;case 40:n&&n.focus()}return!0}function v(n){return function(e){for(var t=((e=e||window.event).target?e.target:e.srcElement).previousSibling;t&&t.name!=c;)t=t.previousSibling;return n?f({target:t,keyCode:13}):(t.value="",f({target:t,keyCode:8})),!1}}o(NaN,-1,-1)}function m(t,n,e){var a,r,i,o=(i=(a=e).match(/^([^\(]+)\(([^,]+),([^\)]+)\)$/))&&void 0!==(r=u[i[1]])?function(){return r.apply(this,[i[2],i[3]])}:(i=a.match(/^([^\(]+)\(([^,\)]+)\)$/))&&void 0!==(r=u[i[1]])?function(){return r.apply(this,[i[2]])}:u[a];if(o){var d=function(){if(t.form){t.className=t.className.replace(/ gluon-input-invalid/g,"");var e=t.options&&-1<t.options.selectedIndex?t.options[t.options.selectedIndex].value:t.value;0==e.length&&n||o.apply(e)||(t.className+=" gluon-input-invalid")}};h(t,"blur",d),h(t,"keyup",d),h(t,"gluon-revalidate",d),"select"==t.nodeName.toLowerCase()&&(h(t,"change",d),h(t,"click",d)),d()}}!function(){var e,t,n,a,r;e=document.querySelectorAll("[data-depends]");for(var i=0;void 0!==(p=e[i]);i++){var o=parseInt(p.getAttribute("data-index"),10),d=JSON.parse(p.getAttribute("data-depends"));if(!isNaN(o)&&0<d.length)for(var u=0;u<d.length;u++)t=p,n=d[u],a=o,r=void 0,(r=f[t.id])||(r={node:t,parent:t.parentNode.id,deps:[],index:a},f[t.id]=r),r.deps.push(n)}e=document.querySelectorAll("[data-update]");for(i=0;void 0!==(p=e[i]);i++)for(var l,s=p.getAttribute("data-update").split(" "),c=0;void 0!==(l=s[c]);c++)h(p,l,v);e=document.querySelectorAll("[data-type]");for(i=0;void 0!==(p=e[i]);i++)m(p,"true"===p.getAttribute("data-optional"),p.getAttribute("data-type"));e=document.querySelectorAll("[data-dynlist]");var p;for(i=0;void 0!==(p=e[i]);i++){g(p,JSON.parse(p.getAttribute("data-dynlist")))}v()}()}();
\ No newline at end of file
diff --git a/package/gluon-web-model/javascript/gluon-web-model.js b/package/gluon-web-model/javascript/gluon-web-model.js
index c05a171e4975776fb19fdc59074c58bcffe877a5..7cf7015190471707028ae59c0c98a9a7da9631a3 100644
--- a/package/gluon-web-model/javascript/gluon-web-model.js
+++ b/package/gluon-web-model/javascript/gluon-web-model.js
@@ -183,6 +183,8 @@
 	}
 
 	function update() {
+		window.dispatchEvent(new Event('gluon-update'));
+
 		var state = false;
 		for (var id in dep_entries) {
 			var entry = dep_entries[id];
@@ -191,6 +193,7 @@
 
 			if (node && node.parentNode && !check(entry.deps)) {
 				node.parentNode.removeChild(node);
+				node.dispatchEvent(new Event('gluon-hide'));
 				state = true;
 			} else if (parent && (!node || !node.parentNode) && check(entry.deps)) {
 				var next = undefined;
@@ -207,6 +210,7 @@
 					parent.insertBefore(entry.node, next);
 				}
 
+				entry.node.dispatchEvent(new Event('gluon-show'));
 				state = true;
 			}
 
@@ -471,6 +475,7 @@
 
 		bind(field, "blur",  validator);
 		bind(field, "keyup", validator);
+		bind(field, "gluon-revalidate", validator);
 
 		if (field.nodeName.toLowerCase() == 'select') {
 			bind(field, "change", validator);
diff --git a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
index ac7696929f6fe7850149db7f8548a101c8185d9b..927b3f0594e3645265baf912ee82a63a82a8830a 100644
--- a/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
+++ b/package/gluon-web-model/luasrc/usr/lib/lua/gluon/web/model/classes.lua
@@ -43,7 +43,7 @@ end
 
 Node = class()
 
-function Node:__init__(title, description, name)
+function Node:__init__(name, title, description)
 	self.children = {}
 	self.title = title or ""
 	self.description = description or ""
@@ -117,8 +117,8 @@ end
 
 Form = class(Node)
 
-function Form:__init__(...)
-	Node.__init__(self, ...)
+function Form:__init__(title, description, name)
+	Node.__init__(self, name, title, description)
 	self.template = "model/form"
 end
 
@@ -169,26 +169,24 @@ end
 
 Section = class(Node)
 
-function Section:__init__(...)
-	Node.__init__(self, ...)
-	self.fields = {}
+function Section:__init__(title, description, name)
+	Node.__init__(self, name, title, description)
 	self.template = "model/section"
 end
 
-function Section:option(t, option, title, description, ...)
+function Section:option(t, ...)
 	assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
 
-	local obj  = t(title, description, option, ...)
+	local obj  = t(...)
 	self:append(obj)
-	self.fields[option] = obj
 	return obj
 end
 
 
 AbstractValue = class(Node)
 
-function AbstractValue:__init__(option, ...)
-	Node.__init__(self, option, ...)
+function AbstractValue:__init__(...)
+	Node.__init__(self, ...)
 	self.deps = {}
 
 	self.default   = nil
diff --git a/package/gluon-web-osm/files/lib/gluon/web/view/model/osm/map.html b/package/gluon-web-osm/files/lib/gluon/web/view/model/osm/map.html
new file mode 100644
index 0000000000000000000000000000000000000000..1a42c5104460270423f935c033560fc1389c967d
--- /dev/null
+++ b/package/gluon-web-osm/files/lib/gluon/web/view/model/osm/map.html
@@ -0,0 +1,44 @@
+<div id="<%=id%>" class="gluon-osm-map" style="display: none"></div>
+<script type="text/javascript" src="/static/gluon-web-osm.js"></script>
+<script type="text/javascript">
+	(function() {
+		var elMap = document.getElementById(<%=json(id)%>);
+		var wrapper = elMap.parentNode;
+
+		var elLon, elLat;
+		window.addEventListener('gluon-update', function() {
+			<% if self.lon then -%>
+				elLon = document.getElementById(<%=json(self.lon:id())%>);
+			<%- end %>
+			<% if self.lat then -%>
+				elLat = document.getElementById(<%=json(self.lat:id())%>);
+			<%- end %>
+		}, {once: true});
+
+		initOSM(<%=json(self.openlayers_url)%>, function(createMap) {
+			elMap.style.display = '';
+
+			var pos = <%=json(self:cfgvalue().pos)%>;
+			var map = createMap(
+				elMap,
+				[pos.lon, pos.lat],
+				<%=json(self:cfgvalue().zoom)%>,
+				<%=json(self:cfgvalue().set)%>,
+				function(lonlat) {
+					if (elLon) {
+						elLon.value = lonlat[0].toFixed(6);
+						elLon.dispatchEvent(new Event('gluon-revalidate'));
+					}
+					if (elLat) {
+						elLat.value = lonlat[1].toFixed(6);
+						elLat.dispatchEvent(new Event('gluon-revalidate'));
+					}
+				}
+			);
+
+			wrapper.addEventListener('gluon-show', function() {
+				map.updateSize();
+			});
+		});
+	})();
+</script>
diff --git a/package/gluon-web-osm/files/lib/gluon/web/www/static/gluon-web-osm.js b/package/gluon-web-osm/files/lib/gluon/web/www/static/gluon-web-osm.js
new file mode 100644
index 0000000000000000000000000000000000000000..3d85f6f18675f132748660b56479dc2863e69e78
--- /dev/null
+++ b/package/gluon-web-osm/files/lib/gluon/web/www/static/gluon-web-osm.js
@@ -0,0 +1 @@
+"use strict";function initOSM(e,o){var t=document.createElement("link");t.rel="stylesheet",t.type="text/css",t.href=e+"/css/ol.css",document.head.appendChild(t);var n=document.createElement("script"),r=!1;n.onload=n.onreadystatechange=function(){if(!(r||this.readyState&&"loaded"!==this.readyState&&"complete"!==this.readyState)){r=!0;var t=new Image;t.onload=function(){var e=new ol.style.Style({image:new ol.style.Icon({img:t,imgSize:[30,45],anchor:[.5,1]})}),c=new ol.Feature;c.setStyle(e),o(function(e,t,o,n,r){var a=new ol.Map({target:e,layers:[new ol.layer.Tile({source:new ol.source.OSM}),new ol.layer.Vector({source:new ol.source.Vector({features:[c]})})],view:new ol.View({center:ol.proj.fromLonLat(t),zoom:o})}),l=function(e){c.setGeometry(new ol.geom.Point(e))};return a.addEventListener("click",function(e){l(e.coordinate),r(ol.proj.toLonLat(e.coordinate))}),n&&l(ol.proj.fromLonLat(t)),a})},t.src="data:image/svg+xml,"+escape('<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="30" height="45"><path d="M2,15A13,13,0,0,1,28,13Q28,28,15,45Q2,28,2,15" fill="#48b" stroke="#369" stroke-width="1.5" /><circle cx="15" cy="15" r="6" fill="#fff" /></svg>')}},n.src=e+"/build/ol.js",document.head.appendChild(n)}
\ No newline at end of file
diff --git a/package/gluon-web-osm/files/lib/gluon/web/www/static/osm.js b/package/gluon-web-osm/files/lib/gluon/web/www/static/osm.js
deleted file mode 100644
index 6ea2ea0e6d89855da8a305480d2ea3fded55becb..0000000000000000000000000000000000000000
--- a/package/gluon-web-osm/files/lib/gluon/web/www/static/osm.js
+++ /dev/null
@@ -1 +0,0 @@
-function findObj(e){for(list=document.getElementsByClassName("gluon-input-text"),i=0;i<list.length;i++)if(item=list.item(i),0<=item.id.indexOf(e))return item;return!1}function showMap(){if("object"==typeof OpenLayers&&!1!==findObj("longitude")){document.getElementById("locationPickerMap").style.display="block";var a=new OpenLayers.Projection("EPSG:4326"),o=new OpenLayers.Projection("EPSG:900913"),e=zoom,t=new OpenLayers.Layer.Markers("Markers");OpenLayers.Control.Click=OpenLayers.Class(OpenLayers.Control,{defaultHandlerOptions:{single:!0,double:!1,pixelTolerance:0,stopSingle:!1,stopDouble:!1},initialize:function(){this.handlerOptions=OpenLayers.Util.extend({},this.defaultHandlerOptions),OpenLayers.Control.prototype.initialize.apply(this,arguments),this.handler=new OpenLayers.Handler.Click(this,{click:this.trigger},this.handlerOptions)},trigger:function(e){var n=osmMap.getLonLatFromPixel(e.xy);oLon=findObj("longitude"),oLat=findObj("latitude"),lonlat1=new OpenLayers.LonLat(n.lon,n.lat).transform(o,a),oLon.value=lonlat1.lon,oLat.value=lonlat1.lat,t.clearMarkers(),t.addMarker(new OpenLayers.Marker(n)),oLon.className=oLon.className.replace(/ gluon-input-invalid/g,""),oLat.className=oLat.className.replace(/ gluon-input-invalid/g,"")}}),osmMap=new OpenLayers.Map("locationPickerMap",{controls:[new OpenLayers.Control.Navigation,new OpenLayers.Control.PanZoomBar,new OpenLayers.Control.MousePosition],maxExtent:new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34),numZoomLevels:18,maxResolution:156543,units:"m",projection:o,displayProjection:a});var n=new OpenLayers.Layer.OSM("OpenStreetMap");osmMap.addLayer(n),osmMap.addLayer(t);var r=longitude,i=latitude;oLon=findObj("longitude"),oLat=findObj("latitude"),""!=oLon.value&&(r=oLon.value),""!=oLat.value&&(i=oLat.value),t.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(r,i).transform(a,o)));var l=new OpenLayers.LonLat(r,i).transform(a,o);osmMap.setCenter(l,e);var s=new OpenLayers.Control.Click;osmMap.addControl(s),s.activate()}else setTimeout(showMap,1e3)}
\ No newline at end of file
diff --git a/package/gluon-web-osm/javascript/gluon-web-osm.js b/package/gluon-web-osm/javascript/gluon-web-osm.js
new file mode 100644
index 0000000000000000000000000000000000000000..65b06e26c89d96fef9e0449c66e7e30bb0f80e7f
--- /dev/null
+++ b/package/gluon-web-osm/javascript/gluon-web-osm.js
@@ -0,0 +1,84 @@
+/*
+	Build using:
+
+	uglifyjs javascript/gluon-web-osm.js -o files/lib/gluon/web/www/static/gluon-web-osm.js -c -m
+*/
+
+'use strict';
+
+function initOSM(openlayers_url, ready) {
+	var markerSvg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="30" height="45">'
+		+ '<path d="M2,15A13,13,0,0,1,28,13Q28,28,15,45Q2,28,2,15" fill="#48b" stroke="#369" stroke-width="1.5" />'
+		+ '<circle cx="15" cy="15" r="6" fill="#fff" />'
+		+ '</svg>';
+
+	var style = document.createElement('link');
+	style.rel = 'stylesheet';
+	style.type = 'text/css';
+	style.href = openlayers_url + '/css/ol.css';
+	document.head.appendChild(style);
+
+	var script = document.createElement('script');
+	var done = false;
+	script.onload = script.onreadystatechange = function() {
+		if (done)
+			return;
+		if (this.readyState && this.readyState !== "loaded" && this.readyState !== "complete")
+			return;
+
+		done = true;
+
+
+		var markerImg = new Image();
+		markerImg.onload = function() {
+			var markerStyle = new ol.style.Style({
+				image: new ol.style.Icon({
+					img: markerImg,
+					imgSize: [30, 45],
+					anchor: [0.5, 1]
+				})
+			});
+
+			var marker = new ol.Feature();
+			marker.setStyle(markerStyle);
+
+			ready(function(elMap, pos, zoom, set, onUpdate) {
+				var map = new ol.Map({
+					target: elMap,
+					layers: [
+						new ol.layer.Tile({
+							source: new ol.source.OSM()
+						}),
+						new ol.layer.Vector({
+							source: new ol.source.Vector({
+								features: [marker]
+							})
+						})
+					],
+					view: new ol.View({
+						center: ol.proj.fromLonLat(pos),
+						zoom: zoom,
+					})
+				});
+
+				var refresh = function(coord) {
+					marker.setGeometry(new ol.geom.Point(coord));
+				}
+
+				map.addEventListener('click', function(e) {
+					refresh(e.coordinate);
+					onUpdate(ol.proj.toLonLat(e.coordinate));
+				});
+
+				if (set)
+					refresh(ol.proj.fromLonLat(pos));
+
+				return map;
+			});
+		}
+
+		markerImg.src = 'data:image/svg+xml,' + escape(markerSvg);
+	};
+	script.src = openlayers_url + '/build/ol.js';
+	document.head.appendChild(script);
+}
diff --git a/package/gluon-web-osm/javascript/osm.js b/package/gluon-web-osm/javascript/osm.js
deleted file mode 100644
index 3aad1f62796745fad92018a25e7cbb982f39b1d5..0000000000000000000000000000000000000000
--- a/package/gluon-web-osm/javascript/osm.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
-	Build using:
-
-	uglifyjs javascript/osm.js -o files/lib/gluon/web/www/static/osm.js -c -m
-*/
-
-function findObj(name) {
-        list = document.getElementsByClassName("gluon-input-text");
-        for(i = 0; i < list.length; i++) {
-                item = list.item(i);
-                if(item.id.indexOf(name) >= 0) return item;
-        }
-        return false;
-}
-
-function showMap() {
-        if ("object" == typeof OpenLayers && false !== findObj("longitude")) {
-                document.getElementById("locationPickerMap").style.display = "block";
-                var e = new OpenLayers.Projection("EPSG:4326"),
-                        a = new OpenLayers.Projection("EPSG:900913"),
-                        t = zoom,
-                        n = new OpenLayers.Layer.Markers("Markers");
-                OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, {
-                        defaultHandlerOptions: {
-                                single: !0,
-                                "double": !1,
-                                pixelTolerance: 0,
-                                stopSingle: !1,
-                                stopDouble: !1
-                        },
-                        initialize: function() {
-                                this.handlerOptions = OpenLayers.Util.extend({}, this.defaultHandlerOptions), OpenLayers.Control.prototype.initialize.apply(this, arguments), this.handler = new OpenLayers.Handler.Click(this, {
-                                        click: this.trigger
-                                }, this.handlerOptions)
-                        },
-                        trigger: function(t) {
-                                var i = osmMap.getLonLatFromPixel(t.xy);
-                                oLon = findObj("longitude");
-                                oLat = findObj("latitude");
-                                lonlat1 = new OpenLayers.LonLat(i.lon, i.lat).transform(a, e),
-                                        oLon.value = lonlat1.lon,
-                                        oLat.value = lonlat1.lat,
-                                        n.clearMarkers(),
-                                        n.addMarker(new OpenLayers.Marker(i)),
-                                        oLon.className = oLon.className.replace(/ gluon-input-invalid/g, ""),
-                                        oLat.className = oLat.className.replace(/ gluon-input-invalid/g, "");
-                        }
-                }), osmMap = new OpenLayers.Map("locationPickerMap", {
-                        controls: [new OpenLayers.Control.Navigation, new OpenLayers.Control.PanZoomBar, new OpenLayers.Control.MousePosition],
-                        maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34),
-                        numZoomLevels: 18,
-                        maxResolution: 156543,
-                        units: "m",
-                        projection: a,
-                        displayProjection: e
-                });
-                var i = new OpenLayers.Layer.OSM("OpenStreetMap");
-                osmMap.addLayer(i), osmMap.addLayer(n);
-                var o = longitude,
-                        r = latitude;
-                oLon = findObj("longitude");
-                oLat = findObj("latitude");
-                "" != oLon.value && (o = oLon.value),
-                "" != oLat.value && (r = oLat.value),
-                n.addMarker(new OpenLayers.Marker(new OpenLayers.LonLat(o, r).transform(e, a)));
-                var l = new OpenLayers.LonLat(o, r),
-                        d = l.transform(e, a);
-                osmMap.setCenter(d, t);
-                var s = new OpenLayers.Control.Click;
-                osmMap.addControl(s), s.activate()
-        } else setTimeout(showMap, 1e3)
-}
diff --git a/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua b/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua
new file mode 100644
index 0000000000000000000000000000000000000000..138a5ec81fa13458c9b81c45ff22829b3a520e8f
--- /dev/null
+++ b/package/gluon-web-osm/luasrc/usr/lib/lua/gluon/web/model/osm.lua
@@ -0,0 +1,43 @@
+module('gluon.web.model.osm', package.seeall)
+
+local classes = require 'gluon.web.model.classes'
+local util = require "gluon.web.util"
+
+local class = util.class
+
+
+local DEFAULT_URL = 'https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.2.0'
+
+
+MapValue = class(classes.AbstractValue)
+
+function MapValue:__init__(title, options)
+	classes.AbstractValue.__init__(self, title)
+	self.subtemplate  = "model/osm/map"
+	self.openlayers_url = options.openlayers_url or DEFAULT_URL
+	self.lon = options.lon
+	self.lat = options.lat
+
+	self.pos = options.pos or {lon = 0, lat = 0}
+	self.zoom = options.zoom or 0
+	self.set = options.set or false
+end
+
+function MapValue:cfgvalue()
+	local pos_lon = tonumber(self.lon and self.lon:cfgvalue())
+	local pos_lat = tonumber(self.lat and self.lat:cfgvalue())
+
+	if pos_lon and pos_lat then
+		return {
+			zoom = 18,
+			pos = { lon = pos_lon, lat = pos_lat },
+			set = true,
+		}
+	else
+		return self
+	end
+end
+
+function MapValue:validate()
+	return true
+end
diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
index b003a08609565c137e8383c330f2ba9dd87e2072..7f5b4c8b1f3f7f5c945332fe9e50c50e167dc9a2 100644
--- a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
+++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
@@ -87,6 +87,7 @@ local function dispatch(config, http, request)
 		pcdata      = util.pcdata,
 		urlencode   = proto.urlencode,
 		attr        = attr,
+		json        = json.stringify,
 		url         = function(path) return build_url(http, path) end,
 	}, { __index = _G }))