diff --git a/Makefile b/Makefile
index 81f13eb7181aeec7067e7789565f2688667b2b40..6eb725f3ac4a8938cf70026a8aead8e26c3257fa 100644
--- a/Makefile
+++ b/Makefile
@@ -39,12 +39,16 @@ CONF_EXT_DIR = $(abspath conf/ext)
 SRC_CORE_DIR = $(abspath src/core)
 SRC_EXT_DIR = $(abspath src/ext)
 SRC_SSS_DIR = $(abspath src/server_side_scripting)
+SRC_LEGACY_DIR = $(abspath src/legacy)
 LIBS_DIR = $(abspath libs)
 TEST_CORE_DIR = $(abspath test/core/)
 TEST_EXT_DIR = $(abspath test/ext)
 TEST_SSS_DIR =$(abspath test/server_side_scripting)
-LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js js/leaflet.js css/leaflet.css css/images js/leaflet-latlng-graticule.js js/proj4.js js/proj4leaflet.js js/leaflet-coordinates.js css/leaflet-coordinates.css js/leaflet-graticule.js js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js js/ckeditor.js
+LIBS = fonts css/fonts css/bootstrap.css css/bootstrap-icons.css js/state-machine.js js/jquery.js js/showdown.js js/dropzone.js css/dropzone.css js/loglevel.js css/images js/bootstrap-select.js css/bootstrap-select.css js/bootstrap-autocomplete.min.js js/plotly.js js/pako.js js/utif.js js/bootstrap.js js/qrcode.js js/ckeditor.js
 
+LEGACY_MAP_JS = leaflet.js leaflet-latlng-graticule.js proj4.js proj4leaflet.js leaflet-coordinates.js leaflet-graticule.js ext_map.js
+LEGACY_MAP_LIBS = $(addprefix js/, $(LEGACY_MAP_JS)) css/leaflet.css css/leaflet-coordinates.css
+LEGACY_QUERY_FORM_LIBS = js/query_form.js
 
 TEST_LIBS = $(LIBS) js/qunit.js css/qunit.css $(subst $(TEST_CORE_DIR)/,,$(shell find $(TEST_CORE_DIR)/))
 
@@ -53,9 +57,10 @@ LIBS_SUBDIRS = $(addprefix $(LIBS_DIR)/, js css fonts)
 
 ALL: install
 
-install: clean-ignore-zips install-sss cp-src cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) merge_js build_properties merge_xsl
+install: clean-ignore-zips install-sss cp-src cp-legacy cp-ext cp-conf $(addprefix $(PUBLIC_DIR)/, $(LIBS)) merge_js build_properties merge_xsl
 
-test: clean-ignore-zips install-sss cp-src cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS)) merge_js build_properties merge_xsl
+test: clean-ignore-zips install-sss cp-src cp-legacy cp-ext cp-ext-test cp-conf $(addprefix $(PUBLIC_DIR)/, $(TEST_LIBS))
+	$(MAKE) EXTRA_BUILD_FILE="$(TEST_CORE_DIR)/test.properties" merge_js build_properties merge_xsl
 	@for f in $(shell find $(TEST_EXT_DIR) -type f -iname *.js) ; do \
 		sed -i "/EXTENSIONS/a \<script src=\"$${f#$(TEST_EXT_DIR)/}\" ></script>" $(PUBLIC_DIR)/index.html ; \
 		echo include $$f; \
@@ -70,15 +75,17 @@ merge_xsl:
 
 merge_js:
 	for f in ${BUILDFILELIST} ; do source "$$f" ; done ; \
+	if [ "$${BUILD_MODULE_LEGACY_MAP}" = "ENABLED" ] ; then \
+	MODULE_DEPENDENCIES+=($(LEGACY_MAP_JS)); \
+	fi ; \
 	JS_DIST_BUNDLE=$${JS_DIST_BUNDLE} AUTO_DISCOVER_MODULES=$$AUTO_DISCOVER_MODULES misc/merge_js.sh $${MODULE_DEPENDENCIES[*]}
 
 EXCLUDE_EXPR = %~ %.backup
-BUILDFILELIST = $(sort $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*)))
+EXTRA_BUILD_FILE?=
+BUILDFILELIST = $(sort $(filter-out $(EXCLUDE_EXPR),$(wildcard build.properties.d/*))) $(EXTRA_BUILD_FILE)
 build_properties:
 	@set -a -e ; \
-	pushd build.properties.files ; \
-	for f in ${BUILDFILELIST} ; do echo "processing ../$$f" && source "../$$f" ; done ; \
-	popd ; \
+	for f in ${BUILDFILELIST} ; do echo "processing $$f" && source "$$f" ; done ; \
 	BUILD_NUMBER=$(BUILD_NUMBER) ; \
 	PROPS=$$(printenv | grep -e "^BUILD_") ; \
 	echo "SET BUILD PROPERTIES:" ; \
@@ -206,6 +213,25 @@ convert-yaml:
 	  misc/yaml_to_json.py $$f > $${f%.yaml}.json ; \
 	done
 
+cp-legacy:
+	@for f in ${BUILDFILELIST} ; do source "$$f" ; done ; \
+	if [ "$${BUILD_MODULE_LEGACY_MAP}" = "ENABLED" ] ; then \
+	$(MAKE) cp-legacy-map ; \
+	fi
+	@for f in ${BUILDFILELIST} ; do source "$$f" ; done ; \
+	if [ "$${BUILD_MODULE_LEGACY_QUERY_FORM}" = "ENABLED" ] ; then \
+	$(MAKE) cp-legacy-query-form ; \
+	fi
+
+cp-legacy-map: $(addprefix $(PUBLIC_DIR)/, $(LEGACY_MAP_LIBS))
+	@echo "/*disabled in favor of legacy ext_map.js*/" > "$(PUBLIC_DIR)/js/map.bundle.js"
+
+cp-legacy-query-form: $(addprefix $(PUBLIC_DIR)/, $(LEGACY_QUERY_FORM_LIBS))
+	@echo "/*disabled in favor of legacy query_form.js*/" > "$(PUBLIC_DIR)/js/query-form.bundle.js"
+
+$(PUBLIC_DIR)/%: $(SRC_LEGACY_DIR)/%
+	cp $< $@
+
 cp-src:
 	cp -r $(SRC_CORE_DIR) $(PUBLIC_DIR)
 
@@ -267,7 +293,7 @@ $(LIBS_DIR)/css/leaflet.css: $(LIBS_DIR)/css/images
 	ln -s $(LIBS_DIR)/leaflet-1.5.1/leaflet.css $@
 
 $(LIBS_DIR)/css/images: unzip $(LIBS_DIR)/css
-	ln -s $(LIBS_DIR)/leaflet-1.5.1/images $@
+	ln -s $(LIBS_DIR)/leaflet-1.5.1/images $@ || true
 
 $(LIBS_DIR)/js/leaflet-graticule.js: unzip $(LIBS_DIR)/js
 	ln -s $(LIBS_DIR)/L.Graticule.js $@
@@ -319,7 +345,7 @@ $(LIBS_DIR)/css/fonts: $(LIBS_DIR)/css
 clean-ignore-zips:
 	$(RM) -r $(SSS_BIN_DIR)
 	$(RM) -r $(PUBLIC_DIR)
-	for f in $(LIBS_SUBDIRS); do unlink $$f || $(RM) -r $$f || true; done
+	for f in $(LIBS_SUBDIRS); do $(RM) -r $$f || true; done
 	$(RM) .server_done
 
 .PHONY: clean
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 7364f27b3f948209e2082d1b7bd77d236326381b..b912e6c5e0aba594c5520b69525bd541d91e9f58 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -56,6 +56,7 @@ BUILD_MODULE_EXT_COSMETICS_CUSTOMDATETIME=DISABLED
 BUILD_MODULE_EXT_QRCODE=ENABLED
 BUILD_MODULE_SHOW_ID_IN_LABEL=DISABLED
 BUILD_MODULE_LEGACY_QUERY_FORM=DISABLED
+BUILD_MODULE_LEGACY_MAP=DISABLED
 
 BUILD_MODULE_USER_MANAGEMENT=ENABLED
 BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
@@ -174,7 +175,6 @@ MODULE_DEPENDENCIES=(
     loglevel.js
     plotly.js
     webcaosdb.js
-    query_form.js
     pako.js
     utif.js
     ext_version_history.js
@@ -192,12 +192,6 @@ MODULE_DEPENDENCIES=(
     edit_mode.js
     ext_entity_state.js
     ext_file_download.js
-    leaflet.js
-    leaflet-graticule.js
-    leaflet-latlng-graticule.js
-    leaflet-coordinates.js
-    proj4.js
-    proj4leaflet.js
     tour.js
     ext_bottom_line.js
     ext_sss_markdown.js
diff --git a/src/legacy/js/ext_map.js b/src/legacy/js/ext_map.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d2e1d5d362fb4d24853fd99b81053b0198d8c27
--- /dev/null
+++ b/src/legacy/js/ext_map.js
@@ -0,0 +1,2377 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2019 IndiScale GmbH (info@indiscale.com)
+ * Copyright (C) 2019 Timm Fitschen (t.fitschen@indiscale.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+'use strict';
+
+/**
+ * @module caosdb_map
+ * @version 0.5.0
+ *
+ * For displaying a geographical map which shows entities at their associated
+ * geolocation.
+ *
+ * The configuration for this module has to be stored in
+ * `conf/ext/json/ext_map.json` and comply with the {@link MapConfig} type
+ * which is described below.
+ *
+ * The current version is 0.4.1. It is not considered to be stable because
+ * implementation of the graticule still is not satisfactory.
+ *
+ * Apart from that the specification of the configuration and the
+ * implementation can be considered stable.
+ */
+var caosdb_map = new function () {
+
+    var logger = log.getLogger("caosdb_map");
+    this.version = "0.5.0";
+    this.dependencies = ["log", {
+        "L": ["latlngGraticule", "Proj"]
+    }, "navbar", "caosdb_utils"];
+    this.logger = logger;
+
+    /**
+     * Map is initialized, map button is visible in the menu.
+     *
+     * @event caosdb_map#map_ready
+     */
+    this.map_ready = new Event("caosdb.caosdb_map.map_ready");
+
+    /**
+     * The MapConfig object is used to define all relevant parameters for the
+     * map including the tiling servers, different views and CRSs of the map,
+     * the configuration of the graticule(s) and the data model for the
+     * integrated query generator.
+     *
+     * @typedef {object} MapConfig
+     * @property {boolean} [show=false] - show the map immediately when it is
+     *     ready. This configuration option is being stored locally for keeping
+     *     the map shown/hidden accross page reloads.
+     * @property {string} version - the version of the map
+     * @property {string} default_view - the view which is shown when the user
+     *     opens the map and has no stored view from prior visits to the map.
+     *     The view are defined in the `views` property.
+     * @property {ViewConfig[]} views - array of the configurations for the
+     *     available views of the map. This includes the tiling servers, the
+     *     graticule, the CRS and more.
+     * @property {DataModelConfig} datamodel - the data model for the
+     *     display of entities in the map (also used by the query generator).
+     * @property {SelectConfig} select - config for the query generator.
+     * @property {Object.<string, EntityLayerConfig>} entityLayers -
+     *     configuration for the entity layer which are to be shown on the map.
+     */
+
+    /**
+     * The DataModelConfig object is used to define the CaosDB Properties which
+     * are interpreted as latitude and longitude in the map.
+     *
+     * Note: Both latitude and longitude are expected to be represented in
+     * decimal format. The latitude should not exceed [-90°,90°] ranges and the
+     * longitude should not exceed [-180°, 180°] ranges.
+     *
+     * @typedef {object} DataModelConfig
+     * @property {string} [lat=latitude] - the name of the latitude property.
+     * @property {string} [lng=longitude] - the name of the longitude property.
+     */
+
+    /**
+     * The SelectConfig object configures the custom {@link select_handler}
+     * plugin for the Leaflet.js module, especially the query generation (for
+     * searching for Entities in the selected area) and when retrieving
+     * entities to be shown.
+     *
+     * The generated query for a selected area has the pattern 
+     * <code>FIND {@link query.role} {@link query.entity} WITH PATH AREA<code>. 
+     * PATH can be empty or represent a configured path to some entity, e.g. 
+     * `WITH RT1 WITH RT2`.
+     * AREA stand for the area filter here.
+     *
+     * The default values of the {@link query} result in queries for any Record
+     * in the selected map area.
+     *
+     * @typedef {object} SelectConfig
+     * @property {object} [query] - The configuration of the query.
+     * @property {string} [query.role=Record] - The role of the entities which
+     *     are to be searched in the selected ares.
+     * @property {string} [query.entity] The (parent) entity to be searched
+     *     for in the area. Defaults to empty string.
+     * @property {object} [paths] - A dictionary of paths that define from
+     *     which entities the geographic location shall be taken.
+     */
+
+    /**
+     * The ViewConfig contains the most important parts of the configuration as
+     * there are no default values set for legal reasons - each person who is
+     * responsible for a CaosDB Server and WebUI has to answer for the
+     * configuration of the tiling servers and possibly has to negotiate the
+     * terms of use with the tiling service providers or provide an own tiling
+     * server.
+     *
+     * It governs most of the actual map functionality including the graticule,
+     * zoom levels, intial zoom and center of the map, and more.
+     *
+     * Furthermore it affects how this view is displayed by the menu of the
+     * {@link view_change_handler} plugin.
+     *
+     * Note: Leaflet comes with a few pre-defined coordinate reference systems
+     * (cf. {@link https://leafletjs.com/reference-1.5.1.html#crs Defined
+     * CRSs}). By default, the default CRS of leaflet will be used for the map
+     * (which is currently EPSG:3857, the Sperical Mercator). If {@link crs} is
+     * a string, e.g. "EPSG:3395" or "Simple" that matches the pre-defined CRS,
+     * the pre-defined CRS is used.
+     *
+     * @typedef {object} ViewConfig
+     * @property {string} id - the unique view id, used by the 
+     *     {@link default_view} property to identify the default view for
+     *     preserving the active view and view configuration across reloads of
+     *     the page.
+     * @property {string} name - the name is shown in the views menu.
+     * @property {string} description - a short discription of the views
+     *     purpose and properties. Also shown in the views menu when mouse
+     *     hovers over the name.
+     * @property {number} zoom - Initial zoom level. Must be an integer and
+     *     >=0.
+     * @property {object} center - the coordinates of the initial map center.
+     * @property {number} center.lat - latitude of the initial map center.
+     * @property {number} center.lng - longitude of the initial map center.
+     * @property {TileLayerConfig} tileLayer - configuration of the tiling
+     *     server for the base layer of the map.
+     * @property {GraticuleConfig} [graticule] - configuration of the graticule
+     *     of the map.
+     * @property {string|CRSConfig} [crs] - coordinate
+     *     reference system of the map.
+     */
+
+    /**
+     * The TileLayerConfig is a thin extension wrapper around the {@link
+     * https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option
+     * TileLayer.WMS options} and the {@link
+     * https://leafletjs.com/reference-1.5.1.html#tilelayer-option TileLayer
+     * options}.
+     *
+     * Only three properties are defined by the wrapper {@link type}, {@link
+     * url} and {@link options}.
+     *
+     * If {@link type} is "osm", the tiling layer is configured with an
+     * OpenStreetMap tile server and the rest of the properties are the ones
+     * defined by the TileLayer options.
+     *
+     * If the {@link type} is "wms", the tiling layer is configured with an
+     * WebMapService server and the rest of the properties are the ones defined
+     * by the TileLayer.WMS options.
+     *
+     * Other types of tiling layers are not supported as for now.
+     *
+     * The {@link url} is of course the url of either the WMS server or the OSM
+     * tiling server.
+     *
+     * The {@link options} is an object which has all properties of the
+     * respective tileLayer as defined by {@link
+     * https://leafletjs.com/reference-1.5.1.html#tilelayer-option
+     * TileLayer options} when {@link type} = "osm" or {@link
+     * https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option
+     * TileLayer.WMS options} when {@link type} = "wms".
+     *
+     * @example <caption>Example for a TileLayerConfig with OSM</caption>
+     *     { type: "osm",
+     *       url: "https://example.com/tile/{z}/{y}/{x}",
+     *       options: {
+     *         attribution: "Tiles &copy; Example.com",
+     *         maxZoom: 13
+     *       }
+     *     }
+     *
+     * @example <caption>Example for a TileLayerConfig with WMS</caption>
+     *     { type: "wms",
+     *       url: "https://example.com/wms",
+     *       options: {
+     *         layers: "0",
+     *         format: "image/png",
+     *         attribution: "WMS Server by Example.com",
+     *         version: "1.3.0",
+     *         transparent: true,
+     *       }
+     *     }
+     *
+     * @typedef {object} TileLayerConfig
+     * @property {string} [type="osm"] - the type of the tile layer, "osm" or
+     *     "wms"
+     * @property {string} url - the url of the OSM or WMS server.
+     * @property {object} options - the {@link
+     *     https://leafletjs.com/reference-1.5.1.html#tilelayer-option
+     *     TileLayer options} or {@link
+     *     https://leafletjs.com/reference-1.5.1.html#tilelayer-wms-option
+     *     TileLayer.WMS options}.
+     */
+
+    /**
+     * GraticuleConfig for configuring a graticule to be shown on the map.
+     *
+     * Experimental Feature!
+     *
+     * Currently, two graticule plug-ins are used. {@link
+     * https://github.com/turban/Leaflet.Graticule L.Graticule.js} handles
+     * polar maps better but lacks configurability and has no lat/lng labels
+     * while {@link
+     * https://github.com/Leaflet/Leaflet.Graticule leaflet.latlng-graticule}
+     * can show lat/lng labels, comes with a more flexible configurability, but
+     * cannot handle polar maps sufficiently.
+     *
+     * Unfortunately, both plug-ins seem to be unmaintained or at least without
+     * any progress in the last years.
+     *
+     * It is unclear wheather we have to write our own or refactor one of these
+     * plug-ins It is unclear which plug-in will make it, wheather we have to
+     * write our own or refactor one of these plug-ins.
+     *
+     * For the time being, both can be used and the implementation is set by
+     * the {@link type} property. If unset or "simple", the L.Graticule.js
+     * implementation is used.
+     *
+     * If {@link type} is "latlngGraticule" the leaflet.latlng-graticule
+     * implementation is used.
+     *
+     * The {@link options} must comply with the configuration options of the
+     * respective implementation.
+     *
+     * @example <caption>Example for the `simple` graticule</caption>
+     *     // this works for polar maps
+     *     { "type": "simple",
+     *       "options": {
+     *         "interval": 45, // draw line every 45th degree
+     *         "style": {
+     *           "color": "#333",
+     *           "weight": 1
+     *         }
+     *       }
+     *     }
+     *
+     *
+     * @example <caption>Example for the `latlng-graticule`</caption>
+     *     // this does not work for polar maps
+     *     { "type": "latlngGraticule",
+     *       "options": {
+     *         "showLabel": true,
+     *         "dashArray": [5, 5],
+     *         "zoomInterval": {
+     *           // interval configuration for different zoom levels
+     *           // and for lat/lng separately.
+     *           "latitude": [ 
+     *             {"start": 2, "end": 2, "interval": 20},
+     *             {"start": 3, "end": 3, "interval": 10},
+     *             {"start": 4, "end": 6, "interval": 5},
+     *             {"start": 7, "end": 10, "interval": 1},
+     *             {"start": 11, "end": 20, "interval": 1}
+     *           ],
+     *           "longitude": [
+     *             {"start": 2, "end": 2, "interval": 20},
+     *             {"start": 3, "end": 3, "interval": 10},
+     *             {"start": 4, "end": 6, "interval": 5},
+     *             {"start": 7, "end": 10, "interval": 1},
+     *             {"start": 11, "end": 20, "interval": 1}
+     *           ]
+     *         }
+     *       }
+     *     }
+     *
+     * @typedef {object} GraticuleConfig
+     * @property {string} type - either "simple" or "latlngGraticule".
+     * @property {object} options - the options for the graticule
+     *     implementation.
+     */
+
+    /**
+     * CRSConfig for the configuration of the coordinate reference system.
+     *
+     * The CRS is managed by the {@link http://kartena.github.io/Proj4Leaflet/
+     * Proj4Leaflet} plugin of the Leaflet.js module.
+     *
+     * The {@link code} defines the standardized CRS Code, e.g. "EPSG:3857" for
+     * the widely used Sperical Mercator CRS.
+     *
+     * The {@link proj4def} contains the proj4 definition of the CRS. A good
+     * soure for these definitions is {@link https://epsg.io}, e.g. {@link
+     * https://epsg.io/3857} for the Sperical Mercartor CRS.
+     *
+     * The {@link options} defines the options for the Proj4Leaflet plug-in,
+     * see {@link http://kartena.github.io/Proj4Leaflet/api/#l-proj-crs-options
+     * Proj.CRS options}.
+     *
+     * @example <caption>Example for CRSConfig</caption>
+     *     { "code": "EPSG:3995",
+     *       "proj4def": `+proj=stere +lat_0=90 +lat_ts=71 +lon_0=0 +k=1
+     *          +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs`,
+     *       "options": {
+     *         "resolutions": [ "16384", "8192", "4096", "2048",
+     *            "1024", "512", "256", "128", "64", "32", "16",
+     *            "8", "4", "2", "1", "0.5" ]
+     *       }
+     *     }
+     *
+     * @typedef {object} CRSConfig
+     * @property {string} code - the standardized CRS Code.
+     * @property {string} proj4def - the proj4 definition of the CRS.
+     * @property {object} options - options according to the 
+     *     {@link http://kartena.github.io/Proj4Leaflet/api/#l-proj-crs-options
+     *     Proj.CRS options}.
+     */
+
+    /**
+     * default configuration for the map.
+     * @type {MapConfig}
+     */
+    this._default_config = {
+        "version": this.version,
+        "show": false,
+        "datamodel": {
+            "lat": "latitude",
+            "lng": "longitude",
+        },
+        "select": {
+            "query": {
+                "role": "RECORD",
+                "entity": "",
+            },
+            "paths": {},
+        },
+    }
+
+    /**
+     * A function which returns entities which are to be displayed on the map.
+     *
+     * The parameters may be used to filter the entities. The caller should not
+     * expect anything from the entities.
+     *
+     * @async
+     * @callback {mapEntityGetter}
+     * @param {DataModelConfig} datamodel - datamodel of the entities to be returned.
+     * @param {number} north - northern bounding latitude
+     * @param {number} south - southern bounding latitude
+     * @param {number} west - western bounding longitude
+     * @param {number} east - eastern bounding longitude
+     * @returns {HTMLElement[]} array of entities in HTML representation.
+     */
+
+    /**
+     * A function which generates a condensed HTML representation of an entity
+     * which can be shown as a popup in a map.
+     *
+     * @callback {mapEntityPopupGenerator}
+     * @param {HTMLElement} entity - in HTML representation
+     * @param {DataModelConfig} datamodel
+     * @param {Number} lat - latitude
+     * @param {Number} lng - longitude
+     * @return {HTMLElement} a popup element.
+     */
+
+    /**
+     * Configuration of the entity layers which are to be shown on the map.
+     *
+     * @typedef {object} EntityLayerConfig
+     * @property {string} id - the id of the entity layer which is used
+     *     internally.
+     * @property {string} name - a short name which is shown in the entity
+     *     layers menu.
+     * @property {string} description - a short description which is shown as
+     *     hover-over text in the entity layers menu.
+     * @property {DivIcon_options} icon_tions - leaflet options for the icon
+     *     (aka the marker) which is shown on the map. These options are
+     *     espcially useful to style the icons (color etc).
+     * @property {number} zIndexOffset
+     * @property {mapEntityGetter} get_entities - returns the entities which
+     *     are to be shown on the map.
+     * @property {mapEntityPopupGenerator} make_popup - returns the popup which
+     *     is to be shown when a user clicks on the map marker.
+     */
+
+    /**
+     * Generates a Property Operator Value (POV) expression by chaining the
+     * provided arguments with "WITH".
+     *
+     * @param {string[]} props - array with the names of RecordTypes
+     * @returns {string} string with the the filter
+     */
+    this._get_with_POV = function (props) {
+        var pov = ""
+        for (let p of props) {
+            pov = pov + ` WITH "${p}" `;
+        }
+        return pov;
+    }
+
+    /**
+     * Generates a Property Operator Value (POV) by joining ids with OR.
+     *
+     * @param {number[]} ids - array of ids for the filter
+     * @returns {string} string with the the filter
+     */
+    this._get_id_POV = function (ids) {
+        ids = ids.map(x => "id=" + x);
+        return "WITH " + ids.join(" or ")
+    }
+
+    /**
+     * Generates a SELECT query string that applies the provided path of 
+     * properties as POV and as selector
+     *
+     * If ids is provided, the condition is not created from the path, but
+     * from ids.
+     *
+     * @param {DataModelConfig} datamodel - datamodel of the entities to be returned.
+     * @param {string[]} path - array with the names of RecordTypes
+     * @param {number[]} ids - array of ids for the filter
+     * @returns {string} query string
+     */
+    this._get_select_with_path = function (datamodel, path, ids) {
+        if (typeof datamodel === "undefined") {
+            throw new Error("Supply the datamodel.")
+        }
+        if (typeof path === "undefined" || path.length == 0) {
+            throw new Error("Supply at least a RecordType.")
+        }
+        const recordtype = path[0];
+        const props = path.slice(1, path.length)
+        var selector = props.join(".")
+        if (selector != "") {
+            selector = selector + "."
+        }
+        var pov = undefined;
+        if (typeof ids === "undefined") {
+            pov = (caosdb_map._get_with_POV(props) +
+                ` WITH ( "${datamodel.lat}" AND "${datamodel.lng}" )`);
+
+        } else {
+            pov = caosdb_map._get_id_POV(ids);
+        }
+        return `SELECT parent,${selector}${datamodel.lat},${selector}${datamodel.lng} FROM ENTITY "${recordtype}" ${pov} `;
+
+    }
+
+
+    /**
+     * Returns a dictionary where for each top level record in the xmldoc 
+     * The long and lat is assigned.
+     *
+     * depth: the depth of the tree including the top level record type:
+     * e.g. RT1->prop1->prop2->lat/long would be a depth=3
+     *
+     * @param {XMLDocument} xmldoc - xml document containing the entities
+     * @param {number} depth - the depth at which the properties shall be taken
+     * @param {DataModelConfig} datamodel - datamodel of the entities to be returned.
+     * @returns {Object} a dictionary where for each id as key the location ist
+     *                   stored as [lat, lng]
+     */
+    this._get_leaf_prop = function (xmldoc, depth, datamodel) {
+        const paths = [
+            ["id"],
+            // The following creates a list: ["", "", ... (depth times), lat/long]
+            ("__split__".repeat(depth) + datamodel.lat).split("__split__"),
+            ("__split__".repeat(depth) + datamodel.lng).split("__split__"),
+        ];
+        const propertyValues = getPropertyValues(xmldoc, paths);
+
+        const leaves = {};
+        for (let row of propertyValues) {
+            leaves[row[0]] = [row[1], row[2]];
+        }
+        return leaves;
+    }
+
+    /**
+     * Template for {@link mapEntityGetter}.
+     *
+     * This implementation has a single additional parameter which is not
+     * defined by {@link mapEntityGetter}:
+     *
+     * @param {string[]} path - array of strings defining the path to the
+     *                          related entity
+     */
+    this._generic_get_current_page_entities = async function (
+        datamodel, north, south, west, east, path) {
+        var container = $(".caosdb-f-main-entities")[0];
+
+        if (typeof path !== "undefined" && path.length) {
+            var ids = []
+            for (let rec of getEntities(container)) {
+                ids.push(getEntityID(rec))
+            }
+            if (ids.length) {
+                const qs = caosdb_map._get_select_with_path(datamodel, path, ids);
+                let entities = await connection.get("Entity/?query=" + qs);
+                caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel);
+                let results = await transformation.transformEntities(entities);
+                container = $('<div>').append(results)[0];
+            } else {
+                return [];
+            }
+        }
+        // it is possible, the the page contains entities which do not have
+        // lat/lng and there doesn't exist any related entity with lat/lng.
+        return caosdb_map.get_map_entities(container, datamodel);
+    }
+
+    /**
+     * Returns a top level record entity from xml.
+     *
+     * @param {XMLDocument} entities - xml document containing the entities
+     * @param {number} rec_id - id of the record to be returned
+     * @returns {XMLDocument} the corresponding record
+     */
+    this._get_toplvl_rec_with_id = function (entities, rec_id) {
+        let tmp = $(entities).find(`Response >[id='${rec_id}']`);
+        if (tmp.length != 1) {
+            throw new Error("There should be exactly one result record. Not " +
+                tmp.length)
+        }
+        return tmp[0];
+    }
+
+    /**
+     * Set the longitude/latitude from subproperties to the top level 
+     * records in the xml and convert everything to html.
+     *
+     * @param {XMLDocument} entities - xml document containing the entities
+     * @param {number} depth - the depth of the path (full: including the first
+     *                         recordtype)
+     */
+    this._set_subprops_at_top = function (entities, depth, datamodel) {
+        // @review Florian Spreckelsen 2022-05-06
+        var latlong = caosdb_map._get_leaf_prop(entities, depth, datamodel);
+
+        for (let rec_id in latlong) {
+            const is_list_lat = Array.isArray(latlong[rec_id][0]);
+            const is_list_lng = Array.isArray(latlong[rec_id][0]);
+            var lat, lng;
+            if (is_list_lat) {
+                var lat_val = `<Value>${latlong[rec_id][0].join("</Value><Value>")}</Value>`;
+                lat = `<Property name="${datamodel.lat}" datatype="LIST&lt;lat&gt;">${lat_val}</Property>`;
+                var lng_val = `<Value>${latlong[rec_id][1].join("</Value><Value>")}</Value>`;
+                lng = `<Property name="${datamodel.lng}" datatype="LIST&lt;lng&gt;">${lng_val}</Property>`;
+            } else {
+                lat = `<Property name="${datamodel.lat}">${latlong[rec_id][0]}</Property>`;
+                lng = `<Property name="${datamodel.lng}">${latlong[rec_id][1]}</Property>`;
+            }
+            let tmp_rec = caosdb_map._get_toplvl_rec_with_id(entities, rec_id);
+            tmp_rec.append(str2xml(lat).firstElementChild);
+            tmp_rec.append(str2xml(lng).firstElementChild);
+        }
+    }
+
+    /**
+     * Template for {@link mapEntityGetter}.
+     *
+     * This implementation has a single additional parameter which is not
+     * defined by {@link mapEntityGetter}:
+     *
+     * @param {string[]} path - array of strings defining the path to the
+     *                          related entity
+     */
+    this._generic_query_all_entities = async function (
+        datamodel, north, south, west, east, path) {
+        var results = undefined;
+        if (typeof path !== "undefined" && path.length) {
+            const qs = caosdb_map._get_select_with_path(datamodel, path);
+            let entities = await connection.get("Entity/?query=" + qs);
+            caosdb_map._set_subprops_at_top(entities, path.length - 1, datamodel);
+            results = await transformation.transformEntities(entities);
+        } else {
+            results = await caosdb_map.query(
+                `FIND ENTITY WITH ( "${datamodel.lat}" AND "${datamodel.lng}" )`);
+        }
+        const container = $('<div>').append(results)[0];
+
+        // As soon as the SELECT query can handle subtyping, the results don't
+        // have to filtered anymore.
+        return caosdb_map.get_map_entities(container, datamodel);
+    }
+
+    /**
+     * Create a HTML representation of an entity which is suitable for being
+     * displayed in the map as a marker's popup.
+     *
+     * Implements {@link mapEntityPopupGenerator}
+     * @param {HTMLElement} entity - an entity in HTML representation.
+     * @param {DataModelConfig} datamodel - configuration of the properties
+     *     used for the coordinates.
+     * @param {Number} lat - latitude
+     * @param {Number} lng - longitude
+     * @returns {HTMLElement} a popup element.
+     */
+    this._make_map_popup = function (entity, datamodel, lat, lng) {
+        // @review Florian Spreckelsen 2022-05-06
+        const role_label = $(entity).find(
+            ".label.caosdb-f-entity-role").first().clone();
+        const parent_list = caosdb_map.make_parent_labels(entity);
+        const name = caosdb_map.make_entity_name_label(entity);
+        const dms_lat = L.NumberFormatter.toDMS(lat);
+        const dms_lng = L.NumberFormatter.toDMS(lng);
+        let extra_loc_hint = "";
+        let path = caosdb_map._get_current_path();
+        if (path && path.length > 1) {
+            extra_loc_hint = `<div>Location of related ${path[path.length-1]}<div>`;
+        }
+        const loc = $(`<div class="small text-muted">${extra_loc_hint}
+            Lat: ${dms_lat} Lng: ${dms_lng}</div>`);
+        const ret = $('<div/>')
+            .append(role_label)
+            .append(parent_list)
+            .append(name)
+            .append(loc);
+        return ret[0];
+    }
+
+    /**
+     * Returns the path from the config corresponding to the value stored in
+     * the session storage (i.e. the storage should be updated before calling
+     * this method if the value changes).
+     *
+     * @returns {string[]} path - array of strings defining the path to the
+     *                          related entity
+     */
+    this._get_current_path = function () {
+        return caosdb_map.config.select.paths[sessionStorage["caosdb_map.display_path"]];
+    }
+
+
+    /**
+     * Default entities layers configuration with two layers:
+     * "current_page_entities" which shows the entities on the current page and
+     * "all_map_entities" which shows all entities in the database with
+     * coordinates.
+     *
+     * @type {Object.<string, EntityLayerConfig>}
+     */
+    this._default_entity_layer_config = {
+        "current_page_entities": {
+            "name": "Entities on the current page.",
+            "description": "Show all entities on the current page.",
+            "icon_options": {
+                html: '<i class="bi-geo-alt-fill"  style="font-size: 20px; color: #00F;"></i>',
+                iconAnchor: [10, 19],
+                className: "",
+            },
+            "zIndexOffset": 1000,
+            "datamodel": {
+                "lat": "latitude",
+                "lng": "longitude",
+                "role": "ENTITY",
+                "entity": "",
+            },
+            "get_entities": (datamodel, north, south, west, east) => {
+                let path = caosdb_map._get_current_path()
+                return caosdb_map._generic_get_current_page_entities(
+                    datamodel, north, south, west, east, path)
+            },
+            "make_popup": this._make_map_popup,
+        },
+        "all_map_entities": {
+            "name": "All entities",
+            "description": "Show all entities with coordinates.",
+            "icon_options": {
+                html: '<i class="bi-geo-alt-fill"  style="font-size: 20px; color: #F00;"></i>',
+                iconAnchor: [10, 19],
+                className: "",
+            },
+            "zIndexOffset": 0,
+            "datamodel": {
+                "lat": "latitude",
+                "lng": "longitude",
+                "role": "ENTITY",
+                "entity": "",
+            },
+            "get_entities": (datamodel, north, south, west, east) => {
+                let path = caosdb_map._get_current_path()
+                return caosdb_map._generic_query_all_entities(
+                    datamodel, north, south, west, east, path)
+            },
+            "make_popup": this._make_map_popup,
+        },
+    };
+
+
+    /**
+     * If no views are configured this dummy view servers as a placeholder.
+     *
+     * @type {ViewConfig}
+     */
+    this._unconfigured_views = [{
+        "id": "UNCONFIGURED",
+        "zoom": 10,
+        "center": {
+            "lat": 0,
+            "lng": 0,
+        },
+        "select": false,
+        "view_change": false,
+        "tileLayer": {
+            "type": "wms",
+            "url": "/webinterface/${BUILD_NUMBER}/pics/map_tile_caosdb_logo.png"
+        }
+    }];
+
+    /**
+     * Default configuration for every map view.
+     *
+     * This configuration sets a default zoom level and center of the map.
+     *
+     * Furthermore, it enables the select handler and the view_change handler.
+     *
+     * Leaflets boxZoom handler is disabled, because the select handler uses
+     * the box for selection.
+     *
+     */
+    this._default_leaflet_config = {
+        "center": {
+            "lat": 53.5332554,
+            "lng": 8.5783268
+        },
+        "zoom": 5,
+        "boxZoom": false,
+        "select": true,
+        "view_change": true,
+    }
+
+    /** (Re-)set this module's functions to standard implementation.
+     */
+    this._init_functions = function () {
+        logger.trace("enter _init_functions");
+
+        /**
+         * Create button with the caosdb-f-toggle-map-button class.
+         *
+         * @param {string} content - the button content. Optional, defaults to
+         *     "Map"
+         */
+        this.create_toggle_map_button = function (content = "Map") {
+            logger.trace("enter create_toggle_map_button");
+            let button = $(
+                `<a class="nav-link" role="button"></a>`);
+            button.toggleClass("caosdb-f-toggle-map-button", true);
+            button.text(content);
+            logger.trace("leave create_toggle_map_button");
+            return button[0];
+        }
+
+        /**
+         * Create map panel (div.caosdb-f-map-panel).
+         */
+        this.create_map_panel = function () {
+            let panel = $("<div>");
+            panel.toggleClass("caosdb-f-map-panel", true);
+
+            // for centered and responsive display
+            panel.toggleClass(["container", "mb-2"], true);
+
+            return panel[0];
+        }
+
+
+        /**
+         * Create a Leaflet map in the container and return the map object.
+         *
+         * @param {HTMLElement} container - the map container.
+         * @param {ViewConfig} config - configuration for the map.
+         *
+         * @returns {L.Map} the map
+         */
+        this.create_map_view = function (container, config) {
+            logger.trace("enter create_map_view", container,
+                config);
+            caosdb_utils.assert_html_element(container, "param `container`");
+
+            // crs
+            // TODO move to separate function
+            if (typeof config.crs === "string" || config
+                .crs instanceof String) {
+                config.crs = L.CRS[config.crs.replace(":", "")];
+                logger.debug("use pre-defined CRS ", config.crs);
+            } else if (typeof config.crs === "object") {
+                config.crs = new L.Proj.CRS(config.crs.code,
+                    config.crs.proj4def, config.crs.options);
+                logger.debug("use custom proj4 CRS ", config.crs);
+            }
+
+            // set tiling server
+            // TODO move to separate function
+            var tileLayer;
+            if (config.tileLayer.type && config.tileLayer.type ===
+                "wms") {
+                tileLayer = L.tileLayer
+                    .wms(config.tileLayer.url, config.tileLayer
+                        .options);
+            } else if (config.tileLayer.type === "osm" ||
+                typeof config.tileLayer.type === "undefined") {
+                tileLayer = L
+                    .tileLayer(config.tileLayer.url, config
+                        .tileLayer.options);
+            } else {
+                throw new Error("unknown tileLayer type: " +
+                    config.tileLayer.type);
+            }
+
+            const wrapped = $("<div/>");
+            $(container).append(wrapped);
+            var map = L.map(wrapped[0], config);
+            map._crs = config.crs;
+            tileLayer.addTo(map);
+
+            // add listeners which store the current zoom and center
+            map.on("zoomend", (e) => {
+                sessionStorage["caosdb_map.view." + config.id + ".zoom"] = map.getZoom();
+            });
+
+            map.on("moveend", (e) => {
+                sessionStorage["caosdb_map.view." + config.id + ".center"] = JSON.stringify(map.getCenter());
+            });
+
+            // add mouse coords
+            // TODO do this outside of this function (one level up)
+            L.control.coordinates({
+                // TODO move to config
+                "position": "bottomleft",
+                "decimals": 2,
+                "enableUserInput": false,
+                "useDMS": true,
+            }).addTo(map);
+
+            this._toggle_cb = () => {
+                // TODO this is shit
+                // resizing of the jquery toggle() function is the problem
+                map.invalidateSize(true);
+            };
+
+            // TODO move one level up
+            if (config.graticule) {
+                this.add_graticule(map, config.graticule);
+            }
+
+            return map;
+        }
+
+
+        /**
+         * Initialize the map panel.
+         *
+         * The map panel is the HTMLElement which contains the map. Is is
+         * hidden or shown when the user clicks on the toggle map button.
+         *
+         * @param {boolean} show - show this panel immediately.
+         */
+        this.init_map_panel = function (show) {
+            logger.trace("enter init_map_panel");
+
+            // remove old
+            $('.caosdb-f-map-panel').remove();
+
+            let panel = this.create_map_panel();
+
+            if (!show) {
+                $(panel).hide();
+            }
+            $('nav').first().after(panel);
+
+            logger.trace("leave init_map_panel");
+            return panel;
+        }
+
+
+        /**
+         * Toggle the map (show/hide).
+         */
+        this.toggle_map = function () {
+            logger.trace("enter toggle_map");
+            sessionStorage["caosdb_map.show"] = !$(".caosdb-f-map-panel").is(":visible");
+            $(".caosdb-f-map-panel").toggle(900, () => this
+                ._toggle_cb());
+        }
+
+
+        this.show_map = function () {
+            logger.trace("enter show_map");
+            sessionStorage["caosdb_map.show"] = "true";
+            $(".caosdb-f-map-panel").show(900, () => this
+                ._toggle_cb());
+        }
+
+
+        /**
+         * To be called after the map panel has been toggled.
+         */
+        this._toggle_cb = undefined;
+
+        /**
+         * Bind the toggle_cb function to the click event of the button.
+         *
+         * @param {HTMLElement} button
+         * @param {function} toggle_cb - to be called on click.
+         * @throws TypeError if the parameters have wrong types.
+         * @returns {HTMLElement} the button
+         */
+        this.bind_toggle_map = function (button, toggle_cb) {
+            logger.trace("enter bind_toggle_map");
+            caosdb_utils.assert_html_element(button,
+                "parameter 'button'");
+            caosdb_utils.assert_type(toggle_cb, "function",
+                "parameter 'toggle_cb'");
+            $(button).on("click", toggle_cb);
+            logger.trace("leave bind_toggle_map");
+            return button;
+        }
+
+        /**
+         * Test if the dependencies are defined in window.
+         *
+         * @throws Error - if a dependency is unmet.
+         * @returns {boolean} true if all dependencies are defined.
+         */
+        this.check_dependencies = function () {
+            logger.trace("enter check_dependencies");
+            for (let dep of this.dependencies) {
+                if (typeof dep == "string") {
+                    if (typeof window[dep] == "undefined") {
+                        throw new Error("Unmet dependency: " +
+                            dep);
+                    }
+                } else {
+                    // TODO
+                    logger.warn("could not check dep", dep);
+                }
+            }
+            logger.trace("leave check_dependencies");
+            return true;
+        }
+
+
+        /**
+         * Return the view with the given id from an array of views.
+         *
+         * @param {ViewConfig[]} views - array of views
+         * @param {string} id - the id of the view to be returned.
+         * @throws {Error} if the view is not in the array.
+         * @return {ViewConfig} with the given id.
+         */
+        this.get_view_config = function (views, id) {
+            caosdb_utils.assert_string(id, "param `id`");
+            for (const view of views) {
+                if (view.id === id)
+                    return view;
+            }
+            throw new Error("Could not find view " + id);
+        }
+
+        /**
+         * Reload layers.
+         */
+        this._reload_layers = function () {
+            caosdb_map._show_load_info()
+            const promises = []
+            const entity_layer_config = $.extend(true, {}, caosdb_map._default_entity_layer_config, caosdb_map.config["entityLayers"]);
+            for (const layer of caosdb_map.layers) {
+                promises.push(caosdb_map._fill_layer(layer.layer_group,
+                    entity_layer_config[layer.id]));
+            }
+            Promise.all(promises).then((val) => {
+                caosdb_map._hide_load_info()
+            })
+        }
+
+
+
+        /** Initialize the caosdb_map module.
+         *
+         * 1) load config
+         * 2) initialize leaflet plug-ins (select_handler, change-view-handler)
+         * 3) initialize the container panel
+         * 4) inttialize the map itself
+         * 5) add the map toggle button to the navbar
+         *
+         * @fires caosdb_map#map_ready
+         */
+        this.init = async function () {
+            logger.trace("enter init");
+            var panel = undefined;
+            var toggle_button = undefined;
+            try {
+                const config = await this.load_config();
+                if (config.disabled) {
+                    return;
+                }
+                if (!config.views || config.views.length === 0) {
+                    logger.warn("no views in config");
+                    return;
+                }
+                this.config = config;
+                var show_map = config.show;
+                if (sessionStorage["caosdb_map.show"]) {
+                    show_map = JSON.parse(sessionStorage["caosdb_map.show"]);
+                }
+                this.init_select_handler();
+                this.init_view_change_handler();
+                panel = this.init_map_panel(show_map);
+
+                // TODO split in smaller pieces and move callback to separate function
+                this.change_map_view = (view) => {
+                    if (this._map) {
+                        this._map._container.remove();
+                        this._map.remove();
+                    }
+
+                    var nextview = view || sessionStorage[
+                            "caosdb_map.view"] || config
+                        .default_view || config.views[0].id;
+                    sessionStorage["caosdb_map.view"] =
+                        nextview;
+                    var nextview_config;
+
+                    // try to get the next view config.
+                    try {
+                        nextview_config = this
+                            .get_view_config(config.views,
+                                nextview);
+                    } catch (err) {
+                        logger.warn(err);
+
+                        // try the default
+                        nextview = config.default_view || config.views[0].id;
+                        nextview_config = this
+                            .get_view_config(config.views,
+                                nextview);
+                    }
+
+                    // remember zoom and center for each view:
+                    var local_conf = {};
+                    local_conf["zoom"] = sessionStorage["caosdb_map.view." +
+                        nextview + ".zoom"];
+                    local_conf["center"] = sessionStorage["caosdb_map.view." +
+                        nextview + ".center"];
+                    if (local_conf["center"]) {
+                        local_conf["center"] =
+                            JSON.parse(local_conf["center"]);
+                    }
+
+                    // merge the view config with default values
+                    var view_config = $.extend(true, {},
+                        this._default_leaflet_config,
+                        nextview_config,
+                        local_conf);
+
+                    // create map
+                    this._map = this.create_map_view(panel,
+                        view_config);
+
+                    // init entity layers
+                    const entity_layer_config = $.extend(true, {}, this._default_entity_layer_config, config["entityLayers"]);
+                    caosdb_map.entityLayers = entity_layer_config;
+                    this.layers = this.init_entity_layers(entity_layer_config);
+                    var layerControl = L.control.layers();
+
+                    const promises = []
+                    for (const layer of this.layers) {
+
+                        promises.push(caosdb_map._fill_layer(layer.layer_group,
+                            entity_layer_config[layer.id]));
+                        layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML);
+                        layer.layer_group.addTo(this._map);
+                    }
+                    Promise.all(promises).then((val) => {
+                        caosdb_map._hide_load_info()
+                    })
+                    layerControl.addTo(this._map);
+
+                    // initialize handlers
+                    this.add_select_handler(this._map);
+
+
+                    this.path_ddm = this._get_path_ddm(
+                        (event) => {
+                            sessionStorage["caosdb_map.display_path"] = event.target.value;
+                            caosdb_map._reload_layers();
+                        },
+                        this.config.select.paths
+                    );
+                    this._map.addControl(this.path_ddm);
+
+
+                    this.add_view_change_handler(
+                        this._map,
+                        config.views,
+                        nextview,
+                        this.change_map_view
+                    );
+                };
+                this.change_map_view();
+
+                toggle_button = this.init_toggle_map_button();
+
+                // indicate that the map is ready: map button is present and
+                // map is hidden or shown but initialized in either case.
+                this._map.whenReady(() => {
+                    document.body.dispatchEvent(caosdb_map.map_ready);
+                });
+            } catch (err) {
+                logger.error("Could not initialize the map.",
+                    err);
+                if (typeof toggle_button !== "undefined") {
+                    $(toggle_button).remove();
+                }
+                if (typeof panel !== "undefined") {
+                    $(panel).remove();
+                }
+            }
+            logger.trace("leave init");
+        }
+
+
+        /**
+         * The _EntityLayer object is used to pass around map overlay layers
+         * between functions. It is not part of the public API.
+         *
+         * @typedef {object} _EntityLayer
+         * @property {string} id
+         * @property {HTMLElement} chooser_html
+         * @property {L.LayerGroup} layer_group
+         * @property {boolean} active
+         */
+
+        /**
+         * @param {Object.<string, EntityLayerConfig>} config
+         * @returns {_EntityLayer[]}
+         */
+        this.init_entity_layers = function (configs) {
+            var ret = []
+            for (let name in configs) {
+                configs[name]["id"] = name;
+                ret.push(this._init_single_entity_layer(configs[name]));
+            }
+            return ret;
+        }
+
+        /**
+         * Initialize an entity layer.
+         *
+         * @param {EntityLayerConfig} config
+         * @return {_EntityLayer}
+         */
+        this._fill_layer = async function (layer_group, config) {
+            // in case load is called on a filled layer: clear first
+            layer_group.clearLayers();
+
+            var entities = await config.get_entities(config.datamodel);
+            layer_group.entities = entities;
+            var markers = caosdb_map.create_entity_markers(
+                entities, config.datamodel, config.make_popup,
+                config.zIndexOffset, config.icon_options);
+
+            for (const marker of markers) {
+                layer_group.addLayer(marker);
+            }
+        };
+
+        /**
+         * Initialize an entity layer.
+         *
+         * @param {EntityLayerConfig} config
+         * @return {_EntityLayer}
+         */
+        this._init_single_entity_layer = function (config) {
+            logger.trace("enter _init_single_entity_layer", config);
+
+            var layer_group = L.layerGroup();
+
+            var ret = {
+                "id": config.id,
+                "active": typeof config.active === "undefined" || config.active,
+                "chooser_html": this.make_layer_chooser_html(config),
+                "layer_group": layer_group
+            };
+
+
+            return ret;
+        }
+
+
+        /**
+         * @param {EntityLayerConfig} config
+         * @return {HTMLElement}
+         */
+        this.make_layer_chooser_html = function (config) {
+            return $('<span/>')
+                .attr("title", config.description)
+                .append(config.icon_options.html)
+                .append(config.name)[0];
+        }
+
+
+        /**
+         * Add the graticule to the current map view.
+         *
+         * @parameter {L.Map} map - the leaflet map.
+         * @parameter {GraticuleConfig} config - the graticule config.
+         */
+        this.add_graticule = function (map, config) {
+            if (config.type === "latlngGraticule") {
+                L.latlngGraticule(config.options).addTo(map);
+            } else if (typeof config.type === "undefined" ||
+                config.type === "simple") {
+                L.graticule(config.options).addTo(map);
+            } else {
+                this.logger.warn("unknown graticule type: ", type);
+            }
+        }
+
+        /**
+         * Add the select_handler to the map
+         *
+         * @param {L.Map} map
+         */
+        this.add_select_handler = function (map) {
+            map.addHandler("select", L.SelectHandler);
+        }
+
+        /**
+         * @callback ViewChangeCB
+         * @param {string} view - the id of the view to be changed to.
+         */
+
+        /**
+         * Add the view_change_handler to the map.
+         *
+         *
+         * @parameter {L.Map} map
+         * @parameter {ViewConfig[]} views - the available views.
+         * @parameter {string} active - the id of the currently active view.
+         * @parameter {ViewChangeCB} view_changer_cb - callback which changes the view.
+         */
+        this.add_view_change_handler = function (map, views, active,
+            view_changer_cb) {
+            map.addHandler("view_change", L.ViewChangeHandler);
+            map.view_change.init(views, active, view_changer_cb);
+        }
+
+
+        /** Initialize the L.ViewChangeHandler as a leaflet extension of
+         * L.Handler.
+         */
+        this.init_view_change_handler = function () {
+            L.ViewChangeHandler =
+                L.Handler.extend(this.view_change_handler);
+        }
+
+        /** Initialize the L.SelectHandler as a leaflet extension of L.Handler.
+         */
+        this.init_select_handler = function () {
+            L.SelectHandler =
+                L.Handler.extend(this.select_handler);
+        }
+
+        /**
+         * Show the query panel if not visible, collapse the query shortcuts
+         * if visible and fill the query string into the text input of the
+         * query panel.
+         *
+         * Also, the query form's paging setting is set to return *all* results
+         * one page.
+         *
+         * The query shortcuts are collapsed because they would otherwise take
+         * up too much space of the view port and push the map outside of the
+         * view port.
+         *
+         * @param {string} query - the generated query.
+         */
+        this.open_query_panel = function (query) {
+            var query_panel = $("#caosdb-query-panel");
+
+            // hide query shortcuts if available and visible
+            query_panel.find(
+                ".caosdb-f-shortcuts-panel-toggle-button:not('.caosdb-f-shortcuts-panel-hidden')"
+            ).click();
+
+            $("#caosdb-query-panel-collapsible")
+                .collapse("show");
+
+            // fill query into text field
+            query_panel.find("#caosdb-query-textarea").val(query);
+
+            // remove paging for queries generated by the map
+            query_panel.find("#caosdb-query-paging-input")
+                .remove();
+        }
+
+
+
+        /**
+         * Generate a query to search inside a map area bounded by maximum and
+         * minimum latitudes and longitudes.
+         *
+         * This function uses the configuration from {@link SelectConfig} for
+         * the role and entity, the configuration from {@link DataModelConfig}
+         * for the latitude and longitude properties, and constructs a query
+         * filter from the rectangular bounding box such that all entities with
+         * coordinates inside that area are returned.
+         *
+         * The area is specified as follows:
+         *
+         * <code>
+         *                  north
+         *            +-----------------+
+         *            |                 |
+         *            |                 |
+         *        west|                 |east
+         *            |                 |
+         *            |                 |
+         *            +-----------------+
+         *                  south
+         * </code>
+         *
+         * The horizontal lines (-) mark the maximum and minimum latitude and
+         * the vertical lines (|) mark the maximum and minimum longitude of the
+         * area to be searched in.
+         *
+         * @param {number} north
+         * @param {number} south
+         * @param {number} west
+         * @param {number} east
+         * @return {string} a query string.
+         */
+        this.generate_query_from_bounds = function (north, south, west,
+            east) {
+            const role = this.config.select.query.role;
+            var entity = this.config.select.query.entity;
+            const lat = this.config.datamodel.lat;
+            const lng = this.config.datamodel.lng;
+            let path = caosdb_map._get_current_path();
+            if (path && path.length > 0 && entity == "") {
+                entity = path[0];
+            }
+            var additional_path = ""
+            if (path && path.length > 1) {
+                additional_path = caosdb_map._get_with_POV(
+                    path.slice(1, path.length))
+            }
+
+            const query_filter = " ( " + lat + " < '" + north +
+                "' AND " + lat +
+                " > '" + south + "' AND " + lng + " > '" + west +
+                "' AND " +
+                lng + " < '" + east + "' ) ";
+
+            const query = "FIND " + role + " " + entity + additional_path +
+                " WITH " + query_filter;
+            return query
+        }
+
+
+        /**
+         * Retrieve the map config from the CaosDBServer.
+         *
+         * @param {string} [resource=ext_map.json] - location of the config
+         *     file.
+         * @returns {MapConfig} config
+         */
+        this.load_config = async function (resource) {
+            logger.trace("enter load_config");
+
+            var conf = {};
+            try {
+                resource = resource || "ext_map.json";
+                conf = await load_config(resource);
+            } catch (err) {
+                logger.error(err);
+            }
+
+
+            var result = $.extend(true, {}, this
+                ._default_config, conf);
+
+            if (!result.views || result.views.length === 0) {
+                logger.debug(
+                    "Could not find any view config. using a dummy tiling server."
+                );
+                result.views = this._unconfigured_views;
+            }
+            try {
+                this.check_config(result);
+            } catch (error) {
+                logger.error(error.message);
+            }
+            logger.trace("leave load_config", result);
+            return result;
+        }
+
+
+        /**
+         * Check config (version and all important keys).
+         *
+         * @param {MapConfig} config - the config which is to be checked.
+         * @throws TypeError, if version does not match this modules version.
+         * @throws Error, if any assertion about the config fails.
+         * @returns {boolean} true, iff everything is fine.
+         */
+        this.check_config = function (config) {
+            logger.trace("enter check_config");
+            caosdb_utils.assert_type(config, "object", "config");
+            if (config.version !== caosdb_map.version) {
+                throw new TypeError(
+                    "The version of the configuration does not match the version of this implementation of the caosdb_map module. Should be '" +
+                    caosdb_map.version + "', was '" + config
+                    .version + "'.");
+            }
+
+            logger.trace("enter check_config");
+            return true;
+        }
+
+
+        /**
+         * Create a button, bind the toggle_map function to the on-click event
+         * and append it to the navbar.
+         */
+        this.init_toggle_map_button = function () {
+            logger.trace("enter init_toggle_map_button");
+
+            // remove old
+            $('.caosdb-f-toggle-map-button').remove();
+
+            let button = this.create_toggle_map_button();
+            this.bind_toggle_map(button, () => this.toggle_map());
+
+            var ret = navbar.add_button(button);
+            logger.trace("leave init_toggle_map_button", ret);
+            return ret;
+        }
+
+
+        /**
+         * Return all entities which have the properties specified in the
+         * datamodel configuration.
+         *
+         * @param {HTMLElement} container - where the entities in HTML
+         *     representation are kept.
+         * @param {DataModelConfig} datamodel
+         * @returns {HTMLElement[]} entities with coordinate properties.
+         */
+        this.get_map_entities = function (container, datamodel) {
+            var map_entities = $(container)
+                .find(".caosdb-entity-panel").has(
+                    ".caosdb-property-name:contains('" + datamodel
+                    .lng + "')").has(
+                    ".caosdb-property-name:contains('" + datamodel
+                    .lat + "')");
+            return map_entities.toArray();
+        }
+
+
+
+        /**
+         * Return an array containing spans with the entity's parents' names in
+         * the pop-up which is shown when the user clicks on an entity marker
+         * in the map.
+         *
+         * @param {HTMLElement} entity - the entity in HTML representation.
+         * @param {HTMLElement[]} array of spans
+         */
+        this.make_parent_labels = function (entity) {
+            var parents = getParents(entity);
+            var ret = [];
+            for (const par of parents) {
+                var label = $('<span class="badge">' + par.name +
+                        '</span>')
+                    // TODO move to global css
+                    .css({
+                        "color": "#333",
+                        "border": "1px solid #333"
+                    });
+                ret.push(label[0]);
+            }
+            return ret;
+        }
+
+
+        /**
+         * Create a div which shows the name of the entity and contains a link
+         * which points to the entity on the page.
+         *
+         * This is shown as a part of the pop-up when the user click on an
+         * entity marker in the map.
+         *
+         * @param {HTMLElement} entity - the entity in HTML representation.
+         * @returns {HTMLElement} a label with the entities name.
+         */
+        this.make_entity_name_label = function (entity) {
+            const name = getEntityName(entity);
+            const id = getEntityID(entity);
+
+            const entity_on_page = $(`#${id}`).length > 0;
+            const href = entity_on_page ? `#${id}` : connection.getBasePath() + `Entity/${id}`
+            const link_title = entity_on_page ? "Jump to this entity." : "Browse to this entity.";
+            const link = $(`<a title="${link_title}" href="${href}"/>`)
+                .addClass("float-end")
+                .append(`<i class="bi bi-box-arrow-up-right"></i></a>`);
+
+            const name_label = $('<div/>')
+                // TODO move to global css
+                .css({
+                    "margin-top": "4px",
+                    "margin-bottom": "4px"
+                })
+                .text(name)
+                .append(link);
+            return name_label[0];
+        }
+
+
+        /**
+         * Retrieve entities from the server and return a container.
+         *
+         * @param {string} query_str - a CQL query.
+         * @returns {HTMLElement[]} an array of entities in HTML representation.
+         */
+        this.query = query;
+
+        /*
+         * Create a single map marker for the given entity.
+         *
+         * @param {HTMLElement} map_entity - the entity.
+         * @param {DataModelConfig} datamodel - specifies the properties for
+         *     coordinates.
+         * @param {number} lat - latitude
+         * @param {number} lng - longitude
+         * @param {DivIcon_options} icon_options
+         * @param {number} zIndexOffset - zIndexOffset of the marker.
+         * @param {mapEntityPopupGenerator} [make_popup] - creates popup content.
+         * @returns {L.Marker} the marker for the map.
+         */
+        var _create_single_entity_marker = function (map_entity, datamodel, lat,
+            lng, icon_options, zIndexOffset, make_popup) {
+            // @review Florian Spreckelsen 2022-05-06
+            var marker = L.marker([lat, lng], {
+                icon: L.divIcon(icon_options)
+            });
+
+            if (zIndexOffset) {
+                marker.setZIndexOffset(zIndexOffset);
+            }
+            if (make_popup) {
+                marker.bindPopup(make_popup(map_entity, datamodel, lat, lng));
+            }
+            return marker;
+        }
+
+        /**
+         * Create markers for the map for an array of entities.
+         *
+         * @param {HTMLElement[]} entities - an array of entities in HTML
+         *     representation.
+         * @param {DataModelConfig} datamodel - specifies the properties for
+         *     coordinates.
+         * @param {mapEntityPopupGenerator} [make_popup] - creates popup content.
+         * @param {number} zIndexOffset - zIndexOffset of the marker.
+         * @param {DivIcon_options} icon_options
+         * @returns {L.Marker[]} an array of markers for the map.
+         */
+        this.create_entity_markers = function (entities, datamodel, make_popup,
+            zIndexOffset, icon_options) {
+            // @review Florian Spreckelsen 2022-05-06
+            logger.trace("enter create_entity_markers", entities, datamodel,
+                zIndexOffset, icon_options);
+
+            var ret = []
+            for (const map_entity of entities) {
+                var lat_vals = getProperty(map_entity, datamodel.lat);
+                var lng_vals = getProperty(map_entity, datamodel.lng);
+
+                if (!lng_vals || !lng_vals || lng_vals == "undefined" || lng_vals== "undefined") {
+                    logger.debug("undefined latitude or longitude",
+                        map_entity, lat_vals, lng_vals);
+                    continue;
+                }
+
+                // we need lat_vals and lng_lavs to be arrays so we make them
+                // be one
+                var is_list_lat = true;
+                if (!Array.isArray(lat_vals)) {
+                    lat_vals = [lat_vals];
+                    is_list_lat = false;
+                }
+                var is_list_lng = true;
+                if (!Array.isArray(lng_vals)) {
+                    lng_vals = [lng_vals];
+                    is_list_lng = false;
+                }
+
+                // both array's length must match
+                if (is_list_lng !== is_list_lat ||
+                    (is_list_lat && is_list_lng &&
+                        lat_vals.length !== lng_vals.length)) {
+                    logger.error("Cannot show this entity on the map. " +
+                        "Its lat/long properties have different lenghts: ",
+                        map_entity);
+                    continue;
+                }
+
+
+                // zip both arrays
+                //   [lat1, lat2, ... latN]
+                //   [lng1, lng2, ... lngN]
+                // into one
+                //   [[lat1,lng1],[lat2,lng2],... [latN,lngN]]
+                var latlngs = lat_vals.map(function (e, i) {
+                    return [e, lng_vals[i]];
+                });
+                logger.debug(`create point marker(s) at ${latlngs} for`,
+                    map_entity);
+                for (let latlng of latlngs) {
+                    var marker = _create_single_entity_marker(map_entity,
+                        datamodel, latlng[0], latlng[1],
+                        icon_options, zIndexOffset, make_popup);
+                    ret.push(marker);
+                }
+
+                /* Code for showing a PATH on the map.
+                 * Maybe we re-use it later
+                 *
+                logger.debug(`create path line at ${latlngs} for`,
+                    map_entity);
+
+                var opts = {color:'red', smoothFactor: 10.0, weight: 1.5, opacity: 0.5};
+                var opts_2 = {color:'green', smoothFactor: 10.0, weight: 3, opacity: 0.5};
+                var path = L.polyline(latlngs, opts);
+                if (make_popup) {
+                    path.bindPopup(make_popup(map_entity, datamodel, lat, lng));
+                }
+                path.on("mouseover",()=>path.setStyle(opts_2));
+                path.on("mouseout",()=>path.setStyle(opts));
+                ret.push(path);
+                 *
+                 *
+                 */
+            }
+            return ret;
+        }
+
+
+        /**
+         * Plug-in for leaflet which adds a menu to the map for changing the view.
+         *
+         * A view is a combination of a base map tile layer, a coordinate
+         * reference system and possibly other properties of the map.
+         *
+         * When a new view is activated the old map is being destroyed and a
+         * new map is contstructed.
+         */
+        this.view_change_handler = {
+            init: function (views, active, view_changer) {
+                this._add_view_options(this._view_menu, views,
+                    active, view_changer);
+            },
+
+            /**
+             * Add a all defined views to the menu.
+             *
+             * The menu is a set of checkboxes. The currently active view is
+             * checked initially and when another view is selected a callback
+             * function intiates change to the new view.
+             *
+             * @param {HTMLElement} menu - A form element where the options are
+             *     added.
+             * @param {ViewConfig[]} view - The view to select from.
+             * @param {string} active - The id of the currently active view.
+             * @param {function} view_changer - The callback which initiates
+             *     the reset of the map in the new view.
+             */
+            _add_view_options: function (menu, views, active,
+                view_changer) {
+                const click = function (e) {
+                    logger.trace(
+                        "click on view_menu option", e);
+                    var next = e.target.value;
+                    if (next !== active) {
+                        view_changer(e.target.value);
+                    }
+                }
+                for (const key of Object.keys(views)) {
+                    const id = views[key].id;
+                    const name = views[key].name || id;
+                    const description = views[key]
+                        .description || name;
+                    const checked = id === active;
+                    const option = $('<div title="' +
+                            description +
+                            '"><input name="view" type="radio" value="' +
+                            id +
+                            '"/><label style="margin-left: 8px">' +
+                            name + '</label></div>')
+                        .css({
+                            "width": "max-content"
+                        });
+                    option
+                        .find(":input")
+                        .on("click", click)
+                        .prop("checked", checked)
+                        .prop("name", "nextview");
+                    $(menu).append(option);
+                }
+            },
+
+            /**
+             * Initialize the handler after it has been added to the map.
+             *
+             * This function is called by the map after the handler has been
+             * added via {@link L.Map.addHandler} or the handler added itself
+             * to the map via {@link L.Handler.addTo}.
+             *
+             * This method adds the menu button which opens the menu for
+             * selecting views.
+             */
+            addHooks: function () {
+                this._view_menu = this._make_view_menu();
+                const toggle_view_menu_button = this.
+                _make_toggle_view_menu_button(this._view_menu);
+
+                const ChangeViewControl = L.Control.extend({
+                    options: {
+                        position: "bottomleft"
+                    },
+                    onAdd: function () {
+                        return toggle_view_menu_button;
+                    }
+                })
+
+                this._map.addControl(new ChangeViewControl());
+            },
+
+            /**
+             * Return an empty menu, i.e. a form element where later on the
+             * view options will be added to.
+             *
+             * @returns {HTMLElement} A form element.
+             */
+            _make_view_menu: function () {
+                var form = $('<form/>')
+                    .hide()
+                    .css({ // TODO move to css
+                        "padding": "4px 10px",
+                        "text-align": "left",
+                        "background-color": "white",
+                        "position": "absolute",
+                        "bottom": "0px",
+                        "left": "34px",
+                        "border": "1px solid black",
+                        "border-radius": "4px",
+                        "min-width": "100px"
+                    });
+
+                form[0].addEventListener("click", function (
+                    e) {
+                    // just stop the click here, such that it does not change
+                    // anything else in the map's state.
+                    logger.trace("click on view menu",
+                        e);
+                    e.stopPropagation();
+                });
+
+                return form[0];
+            },
+
+            /**
+             * Return a button which toggles the view menu.
+             *
+             * @param {HTMLElement} the view menu which is to be toggled.
+             * @returns {HTMLElement} the button.
+             */
+            _make_toggle_view_menu_button: function (view_menu) {
+                var click = (e) => {
+                    e.stopPropagation();
+                    logger.trace(
+                        "click on toggle_view_menu_button",
+                        e, view_menu);
+                    $(view_menu).toggle();
+                };
+
+                // TODO refactor and extract function for map controls and
+                // merge with similar code from the select_handler.
+                var button = L.DomUtil.create("div",
+                    "leaflet-bar leaflet-control leaflet-control-custom caosdb-f-map-change-view-btn"
+                );
+                button.title = "Change the view";
+                // TODO move to css
+                button.style.backgroundColor = "white";
+                button.style.width = "34px";
+                button.style.height = "34px";
+                button.style.textAlign = "center";
+                button.style.marginTop = "2px";
+                button.innerHTML =
+                    '<i style="font-size: 20px" class="bi-three-dots-vertical"></i>';
+
+                button.addEventListener("click", click);
+                $(button).prepend(view_menu);
+
+                return button;
+            },
+
+            /**
+             * Clean up after the view change handler has been removed from the
+             * map.
+             *
+             * This method removes the menu button from the map.
+             */
+            removeHooks: function () {
+                $(this._toggle_view_menu_button).remove();
+            },
+        }
+
+        /**
+         * Shows the loading information div of the map
+         */
+        this._show_load_info = function () {
+            $(".caosdb-f-map-loading").attr("style", "display:inherit");
+        }
+
+        /**
+         * Hides the loading information div of the map
+         */
+        this._hide_load_info = function () {
+            $(".caosdb-f-map-loading").attr("style", "display:None");
+        }
+
+        /**
+         * Return a new leaflet control for setting paths to use for geo location
+         *
+         * @param {function} callback - a callback applies the effect of a
+         *                              changed path
+         * @returns {L.Control} the drop down menu button.
+         */
+        this._get_path_ddm = function (callback, paths) {
+
+            // TODO flatten the structure of the code and possibly merge it with the query_button code.
+            var path_ddm = L.Control.extend({
+                options: {
+                    position: "bottomright"
+                },
+
+                onAdd: function (m) {
+                    return this.button;
+                },
+
+                button: function () {
+                    // TODO refactor to make_map_control function
+                    var button = L.DomUtil
+                        .create("div",
+                            "leaflet-bar leaflet-control leaflet-control-custom"
+                        );
+                    button.title = `Show the location of related entities.
+By default ('Same Entity') entities are shown that have
+a geographic location. The other options allow to show
+entities on the map using the location of a related
+entity.`;
+                    button.style.backgroundColor = "white";
+                    button.style.textAlign = "center";
+                    // Distance to zoom buttons:
+                    button.style.marginTop = "10px";
+                    // TODO implement helper for pictures
+                    let tmp_html = ('<div class="caosdb-f-map-loading" style="display:inherit">Loading Entities...</div><select><option value="same">Same Entity</option>');
+                    for (let pa in paths) {
+                        tmp_html += `<option value="${pa}">${pa}</option>`;
+                    }
+                    tmp_html += '</select>';
+                    button.innerHTML = tmp_html;
+                    const select = $(button).find('select');
+                    select.on("change", callback);
+
+                    const current_path = sessionStorage["caosdb_map.display_path"] || "same";
+                    sessionStorage["caosdb_map.display_path"] = current_path;
+                    select[0].value = current_path
+
+                    $(button).on("mousedown", (
+                        event) => {
+                        event
+                            .stopPropagation();
+                    });
+                    $(button).on("mouseup", (
+                        event) => {
+                        event
+                            .stopPropagation();
+                    });
+                    return button;
+                }(),
+            });
+            return new path_ddm();
+        }
+
+        /**
+         * Plug-in for leaflet which lets the user select an area in the map
+         * and execute a query using a latitude/longitude filter for entities.
+         *
+         * This handler adds a select button as a control to the map. The
+         * select button toggles and indicates the select mode (on/off).
+         *
+         * The selection can be started by either clicking on the map when the
+         * select mode is on (after enabling it via the select button).
+         *
+         * The query button apears as soon as an selection has being finished.
+         *
+         * The query button generates a query from for searching inside the
+         * selected area, opens the query panel and fills the generated query
+         * into the query panel's text field.
+         */
+        this.select_handler = {
+
+
+            /**
+             * Initialize the handler after it has been added to the map.
+             *
+             * This function is called by the map after the handler has been
+             * added via {@link L.Map.addHandler} or the handler added itself
+             * to the map via {@link L.Handler.addTo}.
+             *
+             * This method
+             * 1) Adds the select button.
+             * 2) Adds a listener for the `mousedown` event.
+             *
+             * Both are means to start the process of selecting an area in the
+             * map.
+             */
+            addHooks: function () {
+                const select_button = this._get_select_button((
+                    event) => {
+                    logger.trace(
+                        "select button clicked",
+                        this);
+                    event.preventDefault();
+                    event.stopPropagation();
+                    this._toggle_select_mode();
+                });
+                this._select_button = select_button;
+                this._map.addControl(select_button);
+                this._map.on('mousedown', this
+                    ._mousedown_listener);
+            },
+
+            /**
+             * Clean up after the select handler has been removed from the map.
+             *
+             * This method removes the select button and the `mousedown`
+             * listener.
+             */
+            removeHooks: function () {
+                this._select_button.remove();
+                this._map.off('mousedown', this
+                    ._mousedown_listener);
+            },
+
+            /**
+             * Change the color of the select button in order to highlight it.
+             *
+             * This is used to indicate that the select mode is on and that the
+             * user can select something by clicking and moving the mouse on
+             * the map.
+             */
+            _highlight_select_button: function () {
+                $(this._select_button.button).css({
+                    backgroundColor: "#90EE90"
+                });
+            },
+
+
+            /**
+             * Change the color of the select button back to normal.
+             */
+            _unhighlight_select_button: function () {
+                $(this._select_button.button).css({
+                    backgroundColor: "white"
+                });
+            },
+
+            /**
+             * Toggle the select mode (on/off).
+             *
+             * This includes setting the _select_mode_on to true/false,
+             * highlighting/unhighlighting the select button and
+             * disabling/enabling the moving of the map center by dragging.
+             */
+            _toggle_select_mode: function () {
+                logger.trace("toggle select mode", this);
+                if (this._select_mode_on) {
+                    this._unhighlight_select_button();
+                    this._map.dragging.enable();
+                    this._select_mode_on = false;
+                } else {
+                    this._highlight_select_button();
+                    this._map.dragging.disable();
+                    this._select_mode_on = true;
+                }
+            },
+
+            /**
+             * Return a button for toggling and indicating the select mode.
+             *
+             * The select button shows a litte dashed square as its icon.
+             *
+             * @param {function} callback - a callback which toggles the select
+             *     mode.
+             * @returns {L.Control} the select button.
+             */
+            _get_select_button: function (callback) {
+
+                // TODO flatten the structure of the code and possibly merge it with the query_button code.
+                var select_button = L.Control.extend({
+                    options: {
+                        position: "topleft"
+                    },
+
+                    onAdd: function (m) {
+                        return this.button;
+                    },
+
+                    button: function () {
+                        // TODO refactor to make_map_control function
+                        var button = L.DomUtil
+                            .create("div",
+                                "leaflet-bar leaflet-control leaflet-control-custom"
+                            );
+                        button.title =
+                            "Select an area";
+                        button.style
+                            .backgroundColor =
+                            "white";
+                        button.style.width =
+                            "34px";
+                        button.style.height =
+                            "34px";
+                        button.style.textAlign =
+                            "center";
+                        // Distance to zoom buttons:
+                        button.style.marginTop =
+                            "10px";
+                        // TODO implement helper for pictures
+                        button.innerHTML =
+                            '<img width="20px" height="20px" style="margin-top: 5px;" src="/webinterface/${BUILD_NUMBER}/pics/select.svg.png">';
+                        button.onclick = callback;
+
+                        $(button).on("mousedown", (
+                            event) => {
+                            event
+                                .stopPropagation();
+                        });
+                        $(button).on("mouseup", (
+                            event) => {
+                            event
+                                .stopPropagation();
+                        });
+                        return button;
+                    }(),
+                });
+                return new select_button();
+            },
+
+            /**
+             * Return a button for opening the query panel with a pre-filled query.
+             *
+             * The query button has a loupe icon.
+             *
+             * The query button is added after an area has been selected and
+             * only visible as long an area is selected.
+             *
+             * @param {function} callback - a callback for opening the query
+             *     panel and fill in the query.
+             * @returns {L.Control} the query button.
+             */
+            _get_query_button: function (callback) {
+
+                // TODO flatten the structure of the code and possibly merge it with the select_button code.
+                var query_button = L.Control.extend({
+                    options: {
+                        position: "topleft"
+                    },
+
+                    onAdd: function (m) {
+                        return this.button;
+                    },
+
+                    button: function () {
+                        var button = L.DomUtil
+                            .create("div",
+                                "leaflet-bar leaflet-control leaflet-control-custom"
+                            );
+                        button.title =
+                            "Search within this area";
+                        button.style
+                            .backgroundColor =
+                            "#ff8700";
+                        button.style.width =
+                            "34px";
+                        button.style.height =
+                            "34px";
+                        button.style.textAlign =
+                            "center";
+                        button.style.marginTop =
+                            "2px";
+                        button.innerHTML =
+                            '<i style="margin-top: 5px; font-size: 15px" class="bi-search"></i>';
+                        button.onclick = callback;
+
+                        $(button).on("mousedown", (
+                            event) => {
+                            event
+                                .stopPropagation();
+                        });
+                        $(button).on("mouseup", (
+                            event) => {
+                            event
+                                .stopPropagation();
+                        });
+                        return button;
+                    }(),
+                });
+                return new query_button();
+            },
+
+
+            /**
+             * Listens on the mousedown event of the map and calls the
+             * _startSelect method if either (1) the select mode is on or (2)
+             * the shift key is pressed during the click on the map.
+             */
+            _mousedown_listener: function (event) {
+                logger.trace("mousedown", event, "on", this);
+                if (!event.originalEvent.shiftKey && !this
+                    .select._select_mode_on) {
+                    return;
+                }
+                event.originalEvent.preventDefault();
+                event.originalEvent.stopPropagation();
+                this.select._startSelect(event.latlng);
+            },
+
+            /**
+             * Remove a pre-existing selection and start the process of a new
+             * selection.
+             *
+             * When the user clicks on the map with shift key pressed or
+             * _select_mode this method is called with the coordinates of the
+             * click.
+             *
+             * This also adds listeners on `mousemove` events (for redrawing
+             * the selected area) and on `mouseup` events for finishing the
+             * process of selection.
+             *
+             * @param {L.LatLng} start_point - the coordinates where the
+             *     selection begins.
+             */
+            _startSelect: function (start_point) {
+                this._reset_selection();
+                this._point1 = start_point;
+                logger.trace("point1", this._point1);
+
+                this._map.on("mousemove", this._drawRect);
+                this._map.on("mouseup", this._endSelect);
+            },
+
+            /**
+             * If present, remove the selected area and the query button from
+             * the map.
+             */
+            _reset_selection: function () {
+                this._point1 = undefined;
+                if (this._rectangle) {
+                    this._rectangle.remove();
+                }
+                this._remove_query_button();
+            },
+
+            /**
+             * (Re-)draw the rectangle which indicates the currently selected
+             * area.
+             *
+             * This method is added as a listener on the `mousemove` event by
+             * the _startSelect method.
+             */
+            _drawRect: function (event) {
+                logger.trace("mousemove", event, "on", this);
+                event.originalEvent.preventDefault();
+                event.originalEvent.stopPropagation();
+
+                // remove old rectangle
+                if (this.select._rectangle) {
+                    this.select._rectangle.remove();
+                }
+
+                // draw new rectangle
+                const point2 = event.latlng;
+                const area = this.select._get_area(this.select
+                    ._point1, point2)
+                this.select._rectangle = this.select
+                    ._get_select_rectangle(area);
+                this.select._rectangle.addTo(this);
+            },
+
+
+            /**
+             * Return a colored rectangle covering an area.
+             *
+             * @param {L.LatLngBounds} area.
+             * @returns {L.Rectangle} the colored rectangle.
+             */
+            _get_select_rectangle: function (area) {
+                return L.rectangle(area, {
+                    color: "#ff7800",
+                    weight: 1
+                });
+            },
+
+            /**
+             * Finish the process of selection, add the query button to the map.
+             *
+             * This method is a listener on the `mouseup` event and is added by
+             * the _startSelect method.
+             *
+             * It removes itself as a listener and also the _drawRect listener
+             * on the `mousemove` event.
+             */
+            _endSelect: function (event) {
+                logger.trace("mouseup", event, "on", this);
+                event.originalEvent.preventDefault();
+                event.originalEvent.stopPropagation();
+
+                const point2 = event.latlng;
+                logger.trace("point1", this.select._point1);
+                logger.trace("point2", point2);
+
+                const area = this.select._get_area(this.select
+                    ._point1, point2)
+
+                this.select._add_query_button(area);
+
+                this.select._point1 = undefined;
+                this.off("mouseup", this.select._endSelect);
+                this.off("mousemove", this.select._drawRect);
+            },
+
+            /**
+             * Add a `query` button to the map (showing a loupe icon) which
+             * opens the query panel with a pre-filled query.
+             *
+             * The generated query searches for entities inside the selected
+             * area `a`.
+             *
+             * A pre-existing query button is removed and the new query button
+             * is stored into this._query_button for later references.
+             *
+             * @param {L.LatLngBounds} a - the selected area.
+             * @return {L.Control} the new query button.
+             */
+            _add_query_button: function (a) {
+                logger.trace("_add_query_button", a, this);
+
+                // remove older query button
+                this._remove_query_button();
+
+                const north = this._round(a.getNorth());
+                const south = this._round(a.getSouth());
+                const east = this._round(a.getEast());
+                const west = this._round(a.getWest());
+                const query = caosdb_map
+                    .generate_query_from_bounds(north, south,
+                        west, east)
+
+
+                // generate a call-back which opens the query panel with the
+                // generated query
+                const callback = (event) => {
+                    logger.trace("click query_button",
+                        this);
+                    event.stopPropagation();
+                    caosdb_map.open_query_panel(query);
+                };
+
+                this._query_button = this._get_query_button(
+                    callback);
+                this._map.addControl(this._query_button);
+                return this._query_button;
+            },
+
+            /**
+             * Remove a `query` button if present.
+             *
+             * @return {L.Control} the old query button if present, `undefined`
+             *     otherwise.
+             */
+            _remove_query_button: function () {
+                const old = this._query_button;
+                if (old) {
+                    old.remove();
+                    this._query_button = undefined;
+                    return old;
+                }
+            },
+
+            /**
+             * Return the area specified by two coordinates.
+             *
+             * The edges are parallel to the latitude and longitude of the two
+             * points.
+             *
+             * @param {L.LatLng} point1
+             * @param {L.LatLng} point2
+             * @returns {L.LatLngBounds} the area.
+             */
+            _get_area: function (point1, point2) {
+                return L.latLngBounds(point1, point2);
+            },
+
+            /**
+             * Round the double value to 3 decimal places.
+             *
+             * Note: This function is used to round map coordinates to
+             * meaningful values.
+             *
+             * @param {number} d - a double value
+             * @returns {number} a rounded double value.
+             */
+            _round: function (d) {
+                return Math.round(d * 1000) / 1000;
+            },
+
+            /**
+             * The first point of the selected area.
+             *
+             * It is stored by the _startSelect method is used by subsequent
+             * execution of the _drawRect method and eventually the _endSelect
+             * method.
+             */
+            _point1: undefined,
+
+            /**
+             * _select_mode_on indicates whether clicks on the map without
+             * pressing shift will start/end/reset selection.
+             */
+            _select_mode_on: false,
+        };
+
+        logger.trace("leave _init_functions");
+    }
+    this._init_functions();
+}
+
+$(document).ready(function () {
+    caosdb_modules.register(caosdb_map);
+});
diff --git a/src/core/js/query_form.js b/src/legacy/js/query_form.js
similarity index 100%
rename from src/core/js/query_form.js
rename to src/legacy/js/query_form.js
diff --git a/test/core/test.properties b/test/core/test.properties
new file mode 100644
index 0000000000000000000000000000000000000000..efbe015bc5cbcfa5e01418e8f4841d8658ea3786
--- /dev/null
+++ b/test/core/test.properties
@@ -0,0 +1,2 @@
+BUILD_MODULE_LEGACY_QUERY_FORM=ENABLED
+BUILD_MODULE_LEGACY_MAP=ENABLED