diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 803ffb5d5c39c60760e60e2fd30963f9f60779a4..4b04f0dbfa4ba199f3c2ea365287dd6cf0570caa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,10 +22,6 @@ variables: CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb-webui-testenv # When using dind, it's wise to use the overlayfs driver for # improved performance. - DOCKER_DRIVER: overlay2 - -services: - - docker:19.03-dind image: $CI_REGISTRY_IMAGE:latest @@ -63,7 +59,7 @@ trigger_build: script: - echo $TOKEN - /usr/bin/curl -X POST - -F token=$TOKEN + -F token=$DEPLOY_TRIGGER_TOKEN -F "variables[WEBUI]=$CI_COMMIT_REF_NAME" -F "variables[TriggerdBy]=WEBUI" -F "variables[TriggerdByHash]=$CI_COMMIT_SHORT_SHA" @@ -71,12 +67,12 @@ trigger_build: # Build a docker image in which tests for this repository can run build-testenv: - tags: [ docker ] + tags: [ cached-dind ] image: docker:19.03 stage: setup script: - cd test/docker - - docker login -u testuser -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker login -u indiscale -p $CI_REGISTRY_PASSWORD $CI_REGISTRY # use here general latest or specific branch latest... - docker pull $CI_REGISTRY_IMAGE:latest || true - docker build diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties index d7e81457236d33995e0f21d72488c33198ce4caa..1e1df3997f414a027439225b6074d9d1ce9a4cb8 100644 --- a/build.properties.d/00_default.properties +++ b/build.properties.d/00_default.properties @@ -59,3 +59,6 @@ BUILD_FOOTER_DATA_POLICY_HREF=https://indiscale.com/?page_id=156 # element). BUILD_FOOTER_CUSTOM_ELEMENT_ONE= BUILD_FOOTER_CUSTOM_ELEMENT_TWO= +BUILD_CUSTOM_IMPRINT='<p> Put an imprint note here </p>' + + diff --git a/conf/core/json/ext_map.json b/conf/core/json/ext_map.json new file mode 100644 index 0000000000000000000000000000000000000000..33bcc2e53b6445f6262927bb941817dbe2e0f198 --- /dev/null +++ b/conf/core/json/ext_map.json @@ -0,0 +1,3 @@ +{ + "disabled": true +} diff --git a/makefile b/makefile index b1072187d2e34ffd4bb318110a04a0e7e700be87..22a274eb3f6836278791e4607f090af86e80bcb7 100644 --- a/makefile +++ b/makefile @@ -97,7 +97,7 @@ run-qunit: # start firefox screen -S caosdb-webui-test -X screen -t firefox $(XVFB-RUN) firefox \ - "http://localhost:$(PORT)/webinterface/index.html?loggerPort=$(PORT)" + "http://localhost:$(PORT)/?loggerPort=$(PORT)" # wait until server stops while [ 1 -eq 1 ]; do \ diff --git a/misc/unit_test_http_server.py b/misc/unit_test_http_server.py index 0667572361b35b47c65475b0af35955b650fa39f..a83d762ae69940d0eb43083ad9eff892e54f182b 100755 --- a/misc/unit_test_http_server.py +++ b/misc/unit_test_http_server.py @@ -30,6 +30,7 @@ import sys import os from http.server import SimpleHTTPRequestHandler, HTTPServer +counter = 0 class UnitTestsHandler(SimpleHTTPRequestHandler): """UnitTestsHandler @@ -88,10 +89,12 @@ class UnitTestsHandler(SimpleHTTPRequestHandler): Print the body of a request. """ + global counter + counter += 1 content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length).decode("utf-8") with open("qunit.log", "a") as logfile: - logfile.write("[LOG] " + post_data + "\n") + logfile.write("[LOG {}] ".format(counter) + post_data + "\n") return post_data diff --git a/src/core/css/webcaosdb.css b/src/core/css/webcaosdb.css index beb8ab76b7922cd092f9467621199b57817866db..cc704be6cdd816cfe3214116a07275b3118bc363 100644 --- a/src/core/css/webcaosdb.css +++ b/src/core/css/webcaosdb.css @@ -29,6 +29,7 @@ body { .caosdb-v-main-col { flex-grow: 1; + max-width: 90vw; } .caosdb-v-show-only-child { @@ -39,6 +40,11 @@ body { cursor: pointer; } +/* Ugly workaround for https://gitlab.com/caosdb/caosdb-webui/issues/78 */ +.caosdb-select-table tbody > tr > td { + max-width: 30vw; +} + .caosdb-v-show-only-child:only-child { display: initial; } @@ -69,9 +75,10 @@ body { .caosdb-f-main { display: flex; + width: unset; } .caosdb-f-main-entities { - width: unset; + width: calc(100% - 5px); min-width: 50vw; } diff --git a/src/core/html/imprint.html b/src/core/html/imprint.html index ec9ae6a397f79bfda8f89b0630c9b487f33b09c7..6ae3813e4d99ad3587ca7d3487fffeecf8ab3b37 100644 --- a/src/core/html/imprint.html +++ b/src/core/html/imprint.html @@ -17,9 +17,7 @@ </head> <body style="width: 80%; margin: auto;"> <h1>Imprint</h1> - <p>The entity responsible for running this service is:</p> - <p>Please contact the system administrator and ask them to enter the correct - information here.</p> + ${BUILD_CUSTOM_IMPRINT} <h1>Liability notice</h1> <p>The authors of the software are not responsible for the way it is provided to the public or used otherwise.</p> diff --git a/src/core/js/annotation.js b/src/core/js/annotation.js index 25260abcaf076c0174af1455802e85fcaeeea825..685930da76d73980985210484a60f51690e45a46 100644 --- a/src/core/js/annotation.js +++ b/src/core/js/annotation.js @@ -234,7 +234,7 @@ this.annotation = new function() { // convert and send form var xml = annotation.convertNewCommentForm(form); - var xslPromise = annotation.loadAnnotationXsl(window.sessionStorage.caosdbBasePath); + var xslPromise = annotation.loadAnnotationXsl(connection.getBasePath()); var responsePromise = annotation.postCommentXml(xml); var commentPromise = annotation.convertNewCommentResponse(responsePromise, xslPromise); commentPromise.then(function(resolve) { @@ -306,7 +306,7 @@ this.annotation = new function() { * Shortcut for postXml. The renaming is also good to be able to replace the * function during unit tests with a dummy postXml function */ - this.postCommentXml = (xml) => postXml(xml, window.sessionStorage.caosdbBasePath, "?H"); + this.postCommentXml = (xml) => postXml(xml, connection.getBasePath(), "?H"); /** * Convert a the response of a POST request for a new CommentAnnotation to @@ -377,7 +377,7 @@ this.annotation = new function() { return $.ajax({ cache: true, dataType: 'xml', - url: window.sessionStorage.caosdbBasePath + "Entity/?H&query=FIND+Annotation+WHICH+REFERENCES+" + referencedId + "+WITH+ID=" + referencedId, + url: connection.getBasePath() + "Entity/?H&query=FIND+Annotation+WHICH+REFERENCES+" + referencedId + "+WITH+ID=" + referencedId, }); } @@ -391,7 +391,7 @@ this.annotation = new function() { this.loadComments = async function(annotationSection) { var entityId = annotation.getEntityId(annotationSection); - var annotations = await annotation.getAnnotationsForEntity(entityId, annotation.queryAnnotation, annotation.loadAnnotationXsl(window.sessionStorage.caosdbBasePath)); + var annotations = await annotation.getAnnotationsForEntity(entityId, annotation.queryAnnotation, annotation.loadAnnotationXsl(connection.getBasePath())); $(annotationSection).append(annotations); } diff --git a/src/core/js/edit_mode.js b/src/core/js/edit_mode.js index c9605dc2665ef4b7f3215b97d0950b296b16ff79..5b11866a4a3d50ee6dc2c3ae133612a4b20836ff 100644 --- a/src/core/js/edit_mode.js +++ b/src/core/js/edit_mode.js @@ -1147,9 +1147,7 @@ var edit_mode = new function() { for (var j = 0; j < prlist.length; j++) { prdict.push(prlist[j].name + ": " + prlist[j].value); } - if (prdict.length == 0) { - prdict.push("ID: " + getEntityID(eli)); - } + prdict.push("CaosDB ID: " + getEntityID(eli)); $("select.caosdb-list-" + datatype).not('[data-resolved="true"]').append( $("<option value=\"" + getEntityID(eli) + "\">" + prdict.join(", ") + "</option>")); } diff --git a/src/core/js/ext_map.js b/src/core/js/ext_map.js index 7340d4339ac4a6889a03563fafcee23948b7357c..aba52042292df7d2ead3b42b104db29855ca275a 100644 --- a/src/core/js/ext_map.js +++ b/src/core/js/ext_map.js @@ -24,15 +24,26 @@ 'use strict'; /** - * caosdb_map module for displaying a geographical map which shows entities at their associated geo location. + * caosdb_map module for displaying a geographical map which shows entities at + * their associated geo location. * - * 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 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.3. 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 caosdb_map = new function () { var logger = log.getLogger("caosdb_map"); - this.version = "0.2"; - this.dependencies = ["log", {"L": ["latlngGraticule", "Proj"]}, "navbar", "caosdb_utils"]; + this.version = "0.3"; + this.dependencies = ["log", { + "L": ["latlngGraticule", "Proj"] + }, "navbar", "caosdb_utils"]; this.logger = logger; /** @@ -41,7 +52,7 @@ var caosdb_map = new function() { * the configuration of the graticule(s) and the data model for the * integrated query generator. * - * @typedef {Object} MapConfig + * @typedef {object} MapConfig * @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. @@ -62,7 +73,7 @@ var caosdb_map = new function() { * decimal format. The latitude should not exceed [-90°,90°] ranges and the * longitude should not exceed [-180°, 180°] ranges. * - * @typedef {Object} DataModelConfig + * @typedef {object} DataModelConfig * @property {string} [lat=latitude] - the name of the latitude property. * @property {string} [lng=longitude] - the name of the longitude property. */ @@ -101,11 +112,11 @@ var caosdb_map = new function() { * {@link view_change_handler} plugin. * * Note: Leaflet comes with a few pre-defined coordinate reference systems - * (cf. {@link https://leafletjs.com/reference-1.5.0.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. + * (cf. {@link https://leafletjs.com/reference-1.5.0.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 @@ -212,11 +223,14 @@ var caosdb_map = new function() { * 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. + * 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. + * 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. + * 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 @@ -258,8 +272,10 @@ var caosdb_map = new function() { * } * } * + * @typedef {object} GraticuleConfig * @property {string} type - either "simple" or "latlngGraticule". - * @property {object} options - the options for the graticule implementation. + * @property {object} options - the options for the graticule + * implementation. */ /** @@ -290,6 +306,7 @@ var caosdb_map = new function() { * } * } * + * @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 @@ -302,7 +319,7 @@ var caosdb_map = new function() { * @type {MapConfig} */ this._default_config = { - "version": "0.2", + "version": this.version, "datamodel": { "lat": "latitude", "lng": "longitude", @@ -313,28 +330,199 @@ var caosdb_map = new function() { "entity": "", }, }, - // TODO move to css - "panel": { - "css": { - "height": "500px", - }, - }, } /** - * (Re-)set this module's functions to standard implementation. + * 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 + * @return {HTMLElement} a popup element. + */ + + /** + * @typedef {object} EntityLayerConfig + * @property {string} id + * @property {string} name + * @property {string} description + * @property {DivIcon_options} icon_options + * @property {number} zIndexOffset + * @property {mapEntityGetter} get_entities + * @property {mapEntityPopupGenerator} make_popup + */ + + /** + * Implements {@link mapEntityGetter}. + */ + this._get_current_page_entities = function ( + datamodel, north, south, west, east) { + const container = $(".caosdb-f-main-entities")[0]; + return caosdb_map.get_map_entities(container, datamodel); + } + + /** + * Implements {@link mapEntityGetter}. + */ + this._query_all_entities = async function ( + datamodel, north, south, west, east) { + const results = await caosdb_map.query(`FIND ENTITY WITH ${datamodel.lat} AND ${datamodel.lng}`); + const container = $('<div>').append(results)[0]; + 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. + * @returns {HTMLElement} a popup element. + */ + this._make_map_popup = function (entity, datamodel) { + const lat = getProperty(entity, datamodel.lat); + const lng = getProperty(entity, datamodel.lng); + 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); + const loc = $(`<div class="small text-muted"> + Lat: ${dms_lat} Lng: ${dms_lng} + </div>`); + const ret = $('<div/>') + .append(role_label) + .append(parent_list) + .append(name) + .append(loc); + return ret[0]; + } + + + /** + * 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 {EntityLayerConfig[]} + */ + this._default_entity_layer_config = [{ + "id": "current_page_entities", + "name": "Entities on the current page.", + "description": "Show all entities on the current page.", + "icon": { + html: '<span style="color: #00F; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 1000, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": this._get_current_page_entities, + "make_popup": this._make_map_popup, + }, { + "id": "all_map_entities", + "name": "All entities", + "description": "Show all entities with coordinates.", + "icon": { + html: '<span style="color: #F00; font-size: 20px" class="glyphicon glyphicon-map-marker"></span>', + iconAnchor: [10, 19], + className: "", + }, + "zIndexOffset": 0, + "datamodel": { + "lat": "latitude", + "lng": "longitude", + "role": "ENTITY", + "entity": "", + }, + "get_entities": this._query_all_entities, + "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._init_functions = function() { + 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" + * @param {string} content - the button content. Optional, defaults to + * "Map" */ - this.create_toggle_map_button = function(content = "Map") { + this.create_toggle_map_button = function (content = "Map") { logger.trace("enter create_toggle_map_button"); - let button = $(`<button class="navbar-btn btn btn-link"/>`); + let button = $( + `<button class="navbar-btn btn btn-link"/>`); button.toggleClass("caosdb-f-toggle-map-button", true); button.text(content); logger.trace("leave create_toggle_map_button"); @@ -344,12 +532,19 @@ var caosdb_map = new function() { /** * Create map panel (div.caosdb-f-map-panel). */ - this.create_map_panel = function() { + this.create_map_panel = function () { let panel = $("<div>"); panel.toggleClass("caosdb-f-map-panel", true); // for centered and responsive display panel.toggleClass("container", true); + + // TODO move to css file + $(panel).css({ + "height": "500px" + }); + + return panel[0]; } @@ -362,12 +557,16 @@ var caosdb_map = new function() { * * @returns {L.Map} the map */ - this.create_map_view = function(container, config) { - logger.trace("enter create_map_view", container, config); + this.create_map_view = function (container, config) { + logger.trace("enter create_map_view", container, + config); + caosdb_utils.assert_html_element(container, "param `container`"); // crs - if (typeof config.crs === "string" || config.crs instanceof String) { - config.crs = L.CRS[crs.replace(":","")]; + // 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, @@ -376,25 +575,40 @@ var caosdb_map = new function() { } // set tiling server + // TODO move to separate function var tileLayer; - if (config.tileLayer.type && config.tileLayer.type === "wms") { + if (config.tileLayer.type && config.tileLayer.type === + "wms") { tileLayer = L.tileLayer - .wms(config.tileLayer.url, config.tileLayer.options); - } else if (config.tileLayer.type === "osm") { + .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); + .tileLayer(config.tileLayer.url, config + .tileLayer.options); } else { - throw new Error("unknown tileLayer type: " - + config.tileLayer.type); + throw new Error("unknown tileLayer type: " + + config.tileLayer.type); } - var map = L.map(container, 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 move to config + // TODO do this outside of this function (one level up) L.control.coordinates({ + // TODO move to config "position": "bottomleft", "decimals": 2, "enableUserInput": false, @@ -407,7 +621,8 @@ var caosdb_map = new function() { map.invalidateSize(true); }; - if(config.graticule) { + // TODO move one level up + if (config.graticule) { this.add_graticule(map, config.graticule); } @@ -415,7 +630,13 @@ var caosdb_map = new function() { } - this.init_map_panel = function(config) { + /** + * 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. + */ + this.init_map_panel = function () { logger.trace("enter init_map_panel"); // remove old @@ -423,9 +644,6 @@ var caosdb_map = new function() { let panel = this.create_map_panel(); - $(panel).css(config.css); - - $(panel).hide(); $('nav').first().after(panel); @@ -437,9 +655,10 @@ var caosdb_map = new function() { /** * Toggle the map (show/hide). */ - this.toggle_map = function() { + this.toggle_map = function () { logger.trace("enter toggle_map"); - $(".caosdb-f-map-panel").toggle(900, ()=>this._toggle_cb()); + $(".caosdb-f-map-panel").toggle(900, () => this + ._toggle_cb()); } @@ -456,10 +675,12 @@ var caosdb_map = new function() { * @throws TypeError if the parameters have wrong types. * @returns {HTMLElement} the button */ - this.bind_toggle_map = function(button, toggle_cb) { + 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'"); + 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; @@ -471,12 +692,13 @@ var caosdb_map = new function() { * @throws Error - if a dependency is unmet. * @returns {boolean} true if all dependencies are defined. */ - this.check_dependencies = function() { + 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); + throw new Error("Unmet dependency: " + + dep); } } else { // TODO @@ -489,7 +711,24 @@ var caosdb_map = new function() { /** - * Initialize the caosdb_map module. + * 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); + } + + + /** Initialize the caosdb_map module. * * 1) load config * 2) initialize leaflet plug-ins (select_handler, change-view-handler) @@ -497,60 +736,97 @@ var caosdb_map = new function() { * 4) inttialize the map itself * 5) add the map toggle button to the navbar */ - this.init = async function() { + 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; this.init_select_handler(); this.init_view_change_handler(); - panel = this.init_map_panel(config.panel); + panel = this.init_map_panel(); + // TODO split in smaller pieces and move callback to separate function this.change_map_view = (view) => { - var local_conf = {}; - if(this._map) { - // TODO it would be nice if a change of the view would not reset the center and zoom. - // However, if the old center is not on the current - // map, leaflet throws exceptions. Furthermore, the - // zoom level is view-specific - level 2 in one view - // does not necessarily have the same resolution as the - // level 2 in another view. No idea, how to find a good - // zoom approximation... - // At least we know this - the following does not work: - //local_conf["zoom"] = this._map.getZoom(); - //local_conf["center"] = this._map.getCenter(); + if (this._map) { this._map.remove(); } - var nextview = view || sessionStorage["caosdb_map.view"] || config.leaflet.default_view; - sessionStorage["caosdb_map.view"] = nextview; - var view_config = $.extend(true, {}, { - // TODO move to defaults object - "select": true, - "boxZoom": false, - "view_change": true, - "zoom": 1, - "center": { "lat": 0, "lng": 0 }, - }, config.leaflet.views[nextview], local_conf); - this._map = this.create_map_view(panel, view_config); - this.init_layer_groups(this._map); - - this.create_layer_current_page_entities(); + 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 + var layers = this.init_entity_layers(this._default_entity_layer_config); + var layerControl = L.control.layers(); + for (const layer of layers) { + layerControl.addOverlay(layer.layer_group, layer.chooser_html.outerHTML); + layer.layer_group.addTo(this._map); + } + layerControl.addTo(this._map); + + // initialize handlers this.add_select_handler(this._map); this.add_view_change_handler( this._map, - config.leaflet.views, + config.views, nextview, this.change_map_view ); - } + }; this.change_map_view(); toggle_button = this.init_toggle_map_button(); } catch (err) { - logger.error("Could not initialize the map.", err); + logger.error("Could not initialize the map.", + err); if (typeof toggle_button !== "undefined") { $(toggle_button).remove(); } @@ -561,17 +837,90 @@ var caosdb_map = new function() { 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 {EntityLayerConfig[]} configs + * @returns {_EntityLayer[]} + */ + this.init_entity_layers = function (configs) { + var ret = [] + for (const conf of configs) { + ret.push(this.init_entity_layer(conf)); + } + return ret; + } + + + /** + * Initialize an entity layer. + * + * @param {EntityLayerConfig} config + * @return {_EntityLayer} + */ + this.init_entity_layer = function (config) { + logger.trace("enter init_entity_layer", config); + + var layer_group = L.layerGroup(); + + // load all entities into layer group + var _load = async function (layer_group, config) { + var entities = await config.get_entities(config.datamodel); + var markers = caosdb_map.create_entitiy_markers( + entities, config.datamodel, config.make_popup, + config.zIndexOffset, config.icon); + + for (const marker of markers) { + layer_group.addLayer(marker); + } + }; + _load(layer_group, config); + + 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.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") { + 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") { + } else if (typeof config.type === "undefined" || + config.type === "simple") { L.graticule(config.options).addTo(map); } else { this.logger.warn("unknown graticule type: ", type); @@ -583,7 +932,7 @@ var caosdb_map = new function() { * * @param {L.Map} map */ - this.add_select_handler = function(map){ + this.add_select_handler = function (map) { map.addHandler("select", L.SelectHandler); } @@ -601,62 +950,147 @@ var caosdb_map = new function() { * @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) { + 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); } - this.init_view_change_handler = function(views) { - L.ViewChangeHandler = L.Handler.extend(this.view_change_handler); + /** 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); } - this.init_select_handler = function() { - L.SelectHandler = L.Handler.extend(this.select_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); } - this.open_query_panel = function () { + /** + * 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 in available - query_panel.find(".caosdb-f-shortcuts-panel-toggle-button:not('.caosdb-f-shortcuts-panel-hidden')").click(); + // hide query shortcuts if available and visible + query_panel.find( + ".caosdb-f-shortcuts-panel-toggle-button:not('.caosdb-f-shortcuts-panel-hidden')" + ).click(); query_panel .collapse("show"); - } + // fill query into text field + query_panel.find("#caosdb-query-textarea").val(query); - this.fill_query_filter = function (query_filter) { - let role = this.config.select.query.role; - let entity = this.config.select.query.entity; - var query = "FIND " + role + " " + entity + " WITH " + query_filter; - $("#caosdb-query-textarea").val(query); // remove paging for queries generated by the map - $("#caosdb-query-paging-input").remove(); + 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; + const entity = this.config.select.query.entity; + const lat = this.config.datamodel.lat; + const lng = this.config.datamodel.lng; + + const query_filter = " ( " + lat + " < '" + north + + "' AND " + lat + + " > '" + south + "' AND " + lng + " > '" + west + + "' AND " + + lng + " < '" + east + "' ) "; + + const query = "FIND " + role + " " + entity + + " WITH " + query_filter; + return query } /** - * Return a config. + * Retrieve the map config from the CaosDBServer. * - * @returns {Object} config + * @param {string} [resource=ext_map.json] - location of the config + * file. + * @returns {MapConfig} config */ - this.load_config = async function() { + this.load_config = async function (resource) { logger.trace("enter load_config"); var conf = {}; try { - conf = await load_config("ext_map.json"); + resource = resource || "ext_map.json"; + conf = await load_config(resource); } catch (err) { logger.error(err); } - logger.debug("Could not find any config. returning default config."); - logger.trace("leave load_config"); - var result = $.extend(true, {}, this._default_config, conf); - return result; + 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; + } + logger.trace("leave load_config", result); + return result; } @@ -668,11 +1102,14 @@ var caosdb_map = new function() { * @throws Error, if any assertion about the config fails. * @returns {boolean} true, iff everything is fine. */ - this.check_config = function(config) { + 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+"'."); + 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"); @@ -681,9 +1118,10 @@ var caosdb_map = new function() { /** - * Create a button, bind the toggle_map function to the on-click event and append it to the navbar. + * 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() { + this.init_toggle_map_button = function () { logger.trace("enter init_toggle_map_button"); // remove old @@ -698,154 +1136,234 @@ var caosdb_map = new function() { } - this.get_map_objects = function() { - var datamodel = this.config.datamodel; - var map_objects = $(".caosdb-entity-panel").has(".caosdb-property-name:contains('"+ datamodel.lng + "')").has(".caosdb-property-name:contains('" + datamodel.lat + "')"); - return map_objects.toArray(); + /** + * 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(); } - this.make_parent_labels = function(entity) { + + /** + * 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) { - // TODO handle case where the parent is shown - var label = $('<span class="label">' + par.name + '</span>') - .css({"color": "#333", "border": "1px solid #333"}); + for (const par of parents) { + var label = $('<span class="label">' + par.name + + '</span>') + // TODO move to global css + .css({ + "color": "#333", + "border": "1px solid #333" + }); ret.push(label[0]); } return ret; } - this.make_entity_name_label = function(entity) { - var name = getEntityName(entity); - var id = getEntityId(entity); - var name_label = $('<div/>') - .css({"margin-top": "4px", "margin-bottom":"4px"}) + /** + * 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 entitiy." : "Browse to this entity."; + const link = $(`<a title="${link_title}" href="${href}"/>`) + .addClass("pull-right") + .append(`<span class="glyphicon glyphicon-share-alt"/></a>`); + + const name_label = $('<div/>') + // TODO move to global css + .css({ + "margin-top": "4px", + "margin-bottom": "4px" + }) .text(name) - .append('<a title="Jump to this entity" class="pull-right" href="#' + id + '"><span class="glyphicon glyphicon-share-alt"/></a>'); + .append(link); return name_label[0]; } - this.make_map_popup_representation = function(entity) { - var role_label = $(entity).find(".label.caosdb-f-entity-role").first().clone(); - var parent_list = this.make_parent_labels(entity); - var name = this.make_entity_name_label(entity); - var ret = $('<div/>') - .append(role_label) - .append(parent_list) - .append(name); - return ret[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; - this.create_layer_current_page_entities = function() { - logger.trace("enter create_layer_current_page_entities"); - var datamodel = this.config.datamodel; - var map_objects = this.get_map_objects(); - for (const map_object of map_objects) { - var lat = getProperty(map_object, datamodel.lat); - var lng = getProperty(map_object, datamodel.lng); + /** + * 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_entitiy_markers = function (entities, datamodel, make_popup, zIndexOffset, icon_options) { + logger.trace("enter create_entitiy_markers", entities, datamodel, zIndexOffset, icon_options); + + var ret = [] + for (const map_entity of entities) { + var lat = getProperty(map_entity, datamodel.lat); + var lng = getProperty(map_entity, datamodel.lng); if (lat && lng) { - logger.debug("found map-object", map_object, lat, lng); - var marker = L.marker([lat, lng]); - marker.bindPopup(this.make_map_popup_representation(map_object)); - this.add_to_layer_group("current_page_entities", marker); + logger.debug(`create entity marker at [${lat}, ${lng}] for`, + map_entity); + 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)); + } + ret.push(marker); } else { - logger.debug("no valid latitude or longitude", map_object, lat, lng); + logger.debug("undefined latitude or longitude", + map_entity, lat, lng); } } - logger.debug("current_page_layer", this.get_layer_group("current_page_layer")); - logger.trace("leave create_layer_current_page_entities"); - } - - this.get_standard_layer_groups = function() { - logger.trace("enter get_standard_layer_groups"); - var new_groups = {}; - - // current page entities - var cpe_group = L.layerGroup(); - new_groups["current_page_entities"] = cpe_group; - - logger.trace("leave get_standard_layer_groups"); - return new_groups; - } - - this.init_layer_groups = function(map) { - logger.trace("enter init_layer_groups"); - if (typeof this._layer_groups == "undefined") { - this._layer_groups = this.get_standard_layer_groups(); - } - - for (var name in this._layer_groups) { - let lg = this.get_layer_group(name); - logger.debug("add layer group to map:", name, lg); - lg.addTo(map); - } - logger.trace("leave init_layer_groups"); - } - - this.add_to_layer_group = function(group_name, layer) { - logger.trace("enter add_to_layer_group"); - this.get_layer_group(group_name).addLayer(layer); - logger.trace("leave add_to_layer_group"); + return ret; } - this.get_layer_group = function(name) { - return this._layer_groups[name]; - } + /** + * 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); + init: function (views, active, view_changer) { + this._add_view_options(this._view_menu, views, + active, view_changer); }, - _add_view_options: function(menu, views, active, view_changer) { - const click = function(e) { - logger.trace("click on view_menu option", e); + /** + * 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) { + if (next !== active) { view_changer(e.target.value); } } - for( const key of Object.keys(views)) { - const name = views[key].name || key; - const description = views[key].description || key; - const checked = key === active; - const option = $('<div title="' - + description - + '"><input name="view" type="radio" value="' - + key + '"/><label style="margin-left: 8px">' - + name + '</label></div>') - .css({"width": "max-content"}); + 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) // TODO + .on("click", click) .prop("checked", checked) .prop("name", "nextview"); $(menu).append(option); } }, - addHooks: function() { + /** + * 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); + _make_toggle_view_menu_button(this._view_menu); const ChangeViewControl = L.Control.extend({ - options: {position: "bottomleft"}, - onAdd: function() { + options: { + position: "bottomleft" + }, + onAdd: function () { return toggle_view_menu_button; } }) - this._map.addControl(new ChangeViewControl()); + this._map.addControl(new ChangeViewControl()); }, - _make_view_menu: function() { + /** + * 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 @@ -854,40 +1372,53 @@ var caosdb_map = new function() { "background-color": "white", "position": "absolute", "bottom": "0px", - "left": "34px", + "left": "34px", "border": "1px solid black", "border-radius": "4px", "min-width": "100px" }); - form[0].addEventListener("click", function(e) { - logger.trace("click on view menu", e); + 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(); - // TODO - //caosdb_map.set_map_view(views[this.nextview]); - //caosdb_map._active_view = this.nextview; }); return form[0]; }, - _make_toggle_view_menu_button: function(view_menu) { + /** + * 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); + logger.trace( + "click on toggle_view_menu_button", + e, view_menu); $(view_menu).toggle(); }; - // TODO refactor and extract function for map controls - var button = L.DomUtil.create("div", "leaflet-bar leaflet-control leaflet-control-custom"); + // 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" + ); 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.textAlign = "center"; button.style.marginTop = "2px"; - button.innerHTML = '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-option-vertical"></span>'; + button.innerHTML = + '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-option-vertical"></span>'; button.addEventListener("click", click); $(button).prepend(view_menu); @@ -895,7 +1426,13 @@ var caosdb_map = new function() { return button; }, - removeHooks: function() { + /** + * 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(); }, } @@ -904,212 +1441,451 @@ var caosdb_map = new function() { /** * 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 = { - addHooks: function() { - const select_button = this._get_select_button((event)=>{ - logger.trace("select button clicked", this); + + + /** + * 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(); + this._toggle_select_mode(); }); this._select_button = select_button; - this._map.addControl(select_button); - this._map.on('mousedown', this._startSelect); + this._map.addControl(select_button); + this._map.on('mousedown', this + ._mousedown_listener); }, - removeHooks: function() { - // TODO remove button - this._map.off('mousedown', this._startSelect); + /** + * 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); }, - _highlight_select_button: function() { - $(this._select_button.button).css({backgroundColor: "#90EE90"}); + /** + * 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" + }); }, - _unhighlight_select_button: function() { - $(this._select_button.button).css({backgroundColor: "white"}); + + /** + * Change the color of the select button back to normal. + */ + _unhighlight_select_button: function () { + $(this._select_button.button).css({ + backgroundColor: "white" + }); }, - _toggle_select: function() { - logger.trace("toggle select", this); - if(this._select_active) { + /** + * 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_active = false; + this._select_mode_on = false; } else { this._highlight_select_button(); this._map.dragging.disable(); - this._select_active = true; + this._select_mode_on = true; } }, - _get_select_button: function(callback) { + /** + * 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"}, + options: { + position: "topleft" + }, - onAdd: function(m) { + onAdd: function (m) { return this.button; }, - button: function() { + 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"; + 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"; + 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.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(); }); + $(button).on("mousedown", ( + event) => { + event + .stopPropagation(); + }); + $(button).on("mouseup", ( + event) => { + event + .stopPropagation(); + }); return button; }(), }); return new select_button(); }, - _get_query_button: function(callback){ + /** + * 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"}, + options: { + position: "topleft" + }, - onAdd: function(m) { + 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 = '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-search"></span>'; + 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 = + '<span style="margin-top: 5px; font-size: 15px" class="glyphicon glyphicon-search"></span>'; button.onclick = callback; - $(button).on("mousedown", (event) => { event.stopPropagation(); }); - $(button).on("mouseup", (event) => { event.stopPropagation(); }); + $(button).on("mousedown", ( + event) => { + event + .stopPropagation(); + }); + $(button).on("mouseup", ( + event) => { + event + .stopPropagation(); + }); return button; }(), }); return new query_button(); }, - _startSelect: function(event) { + + /** + * 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_active) { + if (!event.originalEvent.shiftKey && !this + .select._select_mode_on) { return; } event.originalEvent.preventDefault(); event.originalEvent.stopPropagation(); - this.select._reset_selection(); - this.select._point1 = event.latlng; - this.select._point2 = undefined; - logger.trace("point1", this.select._point1); + 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.on("mousemove", this.select._drawRect); - this.on("mouseup", this.select._endSelect); + this._map.on("mousemove", this._drawRect); + this._map.on("mouseup", this._endSelect); }, - _reset_selection: function() { + /** + * 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(); }, - _drawRect: function(event) { + /** + * (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(); } - this.select._point2 = event.latlng; - const area = this.select._get_area(this.select._point1, this.select._point2) - this.select._rectangle = this.select._get_select_rectangle(area); + + // 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); }, - _get_select_rectangle: function(area) { - return L.rectangle(area, {color: "#ff7800", weight: 1}); + /** + * 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 + }); }, - _endSelect: function(event) { + /** + * 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(); - if(this.select._point2) { - // was moved, update last time - this.select._point2 = event.latlng; - logger.trace("point1", this.select._point1); - logger.trace("point2", this.select._point2); - const area = this.select._get_area(this.select._point1, this.select._point2) + 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); - var query_filter = this.select._generate_query_filter(area); - logger.debug("generated query filter", query_filter); - this.select._add_query_button(query_filter); - } else { - // mouse has not been moved, was a click - } this.select._point1 = undefined; - this.select._point2 = undefined; this.off("mouseup", this.select._endSelect); this.off("mousemove", this.select._drawRect); - }, - _add_query_button: function(query_filter) { - logger.trace("_add_query_button", query_filter, this); + /** + * 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(); - var callback = (event) => { - logger.trace("click query_button", this); + + 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(); - caosdb_map.fill_query_filter(query_filter); - } - this._query_button = this._get_query_button(callback); + 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_query_button: function() { - if(this._query_button) { - this._query_button.remove(); + /** + * 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; } }, - _get_area: function(point1, point2) { + /** + * 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); }, /** - * @param {LatLngBounds} a - the area. + * 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. */ - _generate_query_filter: function (a) { - let lat = caosdb_map.config.datamodel.lat; - let lng = caosdb_map.config.datamodel.lng; - let no = this._round(a.getNorth()); - let so = this._round(a.getSouth()); - let ea = this._round(a.getEast()); - let we = this._round(a.getWest()); - return " ( " + lat + " < '" + no + "' AND " + lat + " > '" + so + "' AND " + lng + " > '" + we + "' AND " + lng + " < '" + ea + "' ) "; - }, - - _round: function(d) { - return Math.round(d*1000)/1000; + _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, - _point2: 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"); @@ -1117,6 +1893,6 @@ var caosdb_map = new function() { this._init_functions(); } -$(document).ready(function() { +$(document).ready(function () { caosdb_modules.register(caosdb_map); }); diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js index 0e87d0ddb07c3a2db18aad6efe66e2551cde984e..68a52a023e49d8e9f092883817ad82b1da1c87cd 100644 --- a/src/core/js/ext_references.js +++ b/src/core/js/ext_references.js @@ -136,6 +136,11 @@ var resolve_references = new function () { return undefined; } + const _stripe_re = /Stripe$/i; + this.isStripe = function(el) { + return _stripe_re.test(el.name) + } + /* * Function that retrieves meaningful information for a single element. @@ -167,7 +172,7 @@ var resolve_references = new function () { var persel = await retrieve(getProperty(el[0], "Borrower")); var loan_state = awi_demo.get_loan_state_string(getProperties(el[0])); rseditable.textContent = "Borrowed by " + this.get_person_str(persel[0]) + " (" + loan_state.replace("_", " ") + ")"; - } else if (pr[0].name === "ArchiveStripe") { // TODO *Stripe, Bag, *Sample + } else if (pr[0].name === "SubSample" || this.isStripe(pr[0])) { var bag = await this.find_bag_of_sample(el[0]); if (!bag) { var icecore = await this.find_ice_core_of_sample(el[0]); @@ -181,12 +186,18 @@ var resolve_references = new function () { if (!icecore) { rseditable.textContent = `${id} (Bag ${getProperty(bag, "Number", false)}, no Ice Core)`; } else { - rseditable.textContent = `${id} (Bag ${getProperty(bag, "Number", false)}, Ice Core ${getEntityName(icecore)})`; + rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Bag ${getProperty(bag, "Number", false)})`; } } - } else if (pr[0].name === "Box") { - rseditable.textContent = getProperty(el[0], "Number"); } else if (pr[0].name === "Bag") { + var bag = el[0]; + var icecore = (await query("SELECT name FROM IceCore WHICH REFERENCES " + getEntityID(bag)))[0]; + if (!icecore) { + rseditable.textContent = `${id} (Number ${getProperty(bag, "Number", false)}, no Ice Core)`; + } else { + rseditable.textContent = `${id} (Ice Core ${getEntityName(icecore)}, Number ${getProperty(bag, "Number", false)})`; + } + } else if (pr[0].name === "Box") { rseditable.textContent = getProperty(el[0], "Number"); } else if (pr[0].name === "Palette") { rseditable.textContent = getProperty(el[0], "Number"); diff --git a/src/core/js/form_elements.js b/src/core/js/form_elements.js index cb08ac7083b352883fa4bafb806711eccb850b27..3c4b195410abc8b58571c99bd4b1673f46368594 100644 --- a/src/core/js/form_elements.js +++ b/src/core/js/form_elements.js @@ -63,7 +63,7 @@ * TODO * */ -var form_elements = new function() { +var form_elements = new function () { this.version = "0.1"; this.dependencies = ["log", "caosdb_utils", "markdown"]; @@ -80,13 +80,13 @@ var form_elements = new function() { this.form_error_event = new Event("caosdb.form.error"); - this.get_cache_key = function(form, field) { + this.get_cache_key = function (form, field) { var form_key = $(form).prop("name"); var field_key = $(field).attr("data-field-name"); - return "form_elements." + form_key + "." + field_key; + return "form_elements.cache." + form_key + "." + field_key; } - this.get_cache_value = function(field) { + this.get_cache_value = function (field) { var ret = $(field) .find(":input") .val(); @@ -97,11 +97,11 @@ var form_elements = new function() { } } - this.cache_form = function(cache, form) { + this.cache_form = function (cache, form) { this.logger.trace("enter cache_form", cache, form); $(form) .find(".caosdb-f-field.caosdb-f-form-field-cached") - .each(function(index, field) { + .each(function (index, field) { var value = form_elements.get_cache_value(field); const key = form_elements.get_cache_key(form, field); if (value !== null) { @@ -114,7 +114,7 @@ var form_elements = new function() { } - this.set_cached = function(field) { + this.set_cached = function (field) { $(field).toggleClass("caosdb-f-form-field-cached", true); } @@ -126,9 +126,9 @@ var form_elements = new function() { await form_elements.field_ready(field); const old_value = form_elements.get_cache_value(field); - if(old_value !== value) { + if (old_value !== value) { form_elements.logger.trace("loaded from cache", field, value); - if(typeof $().selectpicker === "function" && + if (typeof $().selectpicker === "function" && $(field).find(".selectpicker").length > 0) { $(field).find(".selectpicker").selectpicker("val", value); } else { @@ -138,16 +138,16 @@ var form_elements = new function() { } } - this.is_set = function(field) { + this.is_set = function (field) { var value = $(field).find(":input").val(); return value && value.length > 0; } - this.load_cached = function(cache, form) { + this.load_cached = function (cache, form) { this.logger.trace("enter load_cached", cache, form); $(form) .find(".caosdb-f-field.caosdb-f-form-field-cached") - .each(function(index, field) { + .each(function (index, field) { var key = form_elements.get_cache_key(form, field); var value = cache[key] || null; if (value !== null) { @@ -164,9 +164,9 @@ var form_elements = new function() { /** * (Re-)set this module's functions to standard implementation. */ - this._init_functions = function() { + this._init_functions = function () { - this.init = function() { + this.init = function () { this.logger.trace("enter init"); } @@ -183,13 +183,13 @@ var form_elements = new function() { * @param {string} [desc] - the description for the entity. * @returns {HTMLElement} OPTION element. */ - this.make_reference_option = function(entity_id, desc) { + this.make_reference_option = function (entity_id, desc) { caosdb_utils.assert_string(entity_id, "param `entity_id`"); - if(typeof desc == "undefined") { + if (typeof desc == "undefined") { desc = entity_id; } - var opt_str = '<option value="' + entity_id + '">' + desc - + "</option>"; + var opt_str = '<option value="' + entity_id + '">' + desc + + "</option>"; return $(opt_str)[0]; } @@ -205,9 +205,9 @@ var form_elements = new function() { * parameter. * @returns {HTMLElement} SELECT element with entity options. */ - this.make_reference_select = async function(entities, make_desc, multiple=false) { + this.make_reference_select = async function (entities, make_desc, multiple = false) { caosdb_utils.assert_array(entities, "param `entities`", false); - if ( typeof make_desc !== "undefined" ) { + if (typeof make_desc !== "undefined") { caosdb_utils.assert_type(make_desc, "function", "param `make_desc`"); } @@ -217,7 +217,7 @@ var form_elements = new function() { } else { ret.append('<option style="display: none" selected="selected" value="" disabled="disabled"></option>'); } - for ( let entity of entities ) { + for (let entity of entities) { this.logger.trace("add option", entity); let entity_id = getEntityID(entity); let desc = typeof make_desc == "function" ? await make_desc(entity) : @@ -238,15 +238,17 @@ var form_elements = new function() { * * @param {object} config * @returns {HTMLElement} SELECT element. + * + * TODO make syncronous */ - this.make_reference_drop_down = async function(config) { + this.make_reference_drop_down = async function (config) { let ret = $(this._make_field_wrapper(config.name)); let label = this._make_input_label_str(config); let loading = $('<div class="caosdb-f-field-not-ready">loading...</div>'); let input_col = $('<div class="col-sm-9"/>'); input_col.append(loading); - this._query(config.query).then(async function(entities){ + this._query(config.query).then(async function (entities) { let select = $(await form_elements.make_reference_select( entities, config.make_desc, config.multiple)); select.attr("name", config.name); @@ -254,7 +256,7 @@ var form_elements = new function() { input_col.append(select); form_elements.init_select_picker(ret[0]); ret[0].dispatchEvent(form_elements.field_ready_event); - select.change(function() { + select.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); }).catch(err => { @@ -268,7 +270,7 @@ var form_elements = new function() { } - this.init_select_picker = function(field) { + this.init_select_picker = function (field) { caosdb_utils.assert_html_element(field, "parameter `field`"); const select = $(field).find("select")[0]; const select_picker_options = {}; @@ -285,12 +287,12 @@ var form_elements = new function() { } - this.init_actions_box = function(field) { + this.init_actions_box = function (field) { this.logger.trace("enter init_actions_box", field); caosdb_utils.assert_html_element(field, "parameter `field`"); const select = $(field).find("select"); var actions_box = select.siblings().find(".bs-actionsbox"); - if(actions_box.length === 0) { + if (actions_box.length === 0) { actions_box = $(`<div class="bs-actionsbox"> <div class="btn-group btn-group-sm btn-block"> <button type="button" class="actions-btn btn-default bs-deselect-all btn btn-light">None</button> @@ -305,7 +307,7 @@ var form_elements = new function() { field.addEventListener( form_elements.field_changed_event.type, (e) => { - if(form_elements.is_set(field)) { + if (form_elements.is_set(field)) { actions_box.show(); } else { actions_box.hide(); @@ -314,7 +316,7 @@ var form_elements = new function() { actions_box .find(".bs-deselect-all") - .click((e)=>{ + .click((e) => { select.val(null) .selectpicker("render") .parent().toggleClass("open", false); @@ -333,13 +335,13 @@ var form_elements = new function() { * @param {HTMLElement} field * @return {Promise} the field-ready promise */ - this.field_ready = function(field) { + this.field_ready = function (field) { // TODO add support for field name (string) as field parameter // TODO check type of param field (not an array!) caosdb_utils.assert_html_element(field, "parameter `field`"); - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { try { - if(!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { + if (!$(field).hasClass("caosdb-f-field-not-ready") && $(field).find(".caosdb-f-field-not-ready").length === 0) { resolve(field); } else { field.addEventListener(form_elements.field_ready_event.type, @@ -359,13 +361,20 @@ var form_elements = new function() { this._run_script = async function (script, form) { const json_str = JSON.stringify(form_elements.form_to_object(form[0])); - const params = {"-p0": {"filename": "form.json", "blob": new Blob([json_str], {type: "application/json"})}}; + const params = { + "-p0": { + "filename": "form.json", + "blob": new Blob([json_str], { + type: "application/json" + }) + } + }; const result = await connection.runScript(script, params); this.logger.debug("server-side script returned", result); return this.parse_script_result(result); } - this.parse_script_result = function(result) { + this.parse_script_result = function (result) { console.log(result); const scriptNode = result.evaluate("/Response/script", result, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; const code = result.evaluate("@code", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; @@ -374,13 +383,18 @@ var form_elements = new function() { const stderr = result.evaluate("stderr", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; const stdout = result.evaluate("stdout", scriptNode, null, XPathResult.STRING_TYPE, null).stringValue; - return { "code": code, "call": call, "stdout": stdout, "stderr": stderr }; + return { + "code": code, + "call": call, + "stdout": stdout, + "stderr": stderr + }; } /** * generate a java script object representation of a form */ - this.form_to_object = function(form) { + this.form_to_object = function (form) { this.logger.trace("entity form_to_json", form); caosdb_utils.assert_html_element(form, "parameter `form`"); @@ -389,7 +403,7 @@ var form_elements = new function() { for (const child of element.children) { // ignore disabled fields and subforms - if ($(child).hasClass("caosdb-f-field-disabled")){ + if ($(child).hasClass("caosdb-f-field-disabled")) { continue; } const name = $(child).attr("name"); @@ -400,7 +414,7 @@ var form_elements = new function() { var subform_obj = _to_json(child, {}); if (typeof data[subform] === "undefined") { data[subform] = subform_obj; - } else if (Array.isArray(data[subform])){ + } else if (Array.isArray(data[subform])) { data[subform].push(subform_obj); } else { data[subform] = [data[subform], subform_obj] @@ -408,12 +422,12 @@ var form_elements = new function() { } else if (name && name !== "") { // input elements const not_checkbox = !$(child).is(":checkbox"); - if ( not_checkbox || $(child).is(":checked")) { + if (not_checkbox || $(child).is(":checked")) { // checked or not a checkbox var value = $(child).val(); if (typeof data[name] === "undefined") { data[name] = value; - } else if (Array.isArray(data[name])){ + } else if (Array.isArray(data[name])) { data[name].push(value); } else { data[name] = [data[name], value] @@ -436,12 +450,12 @@ var form_elements = new function() { return ret; } - this.make_submit_button = function() { + this.make_submit_button = function () { var ret = $('<button class="caosdb-f-form-elements-submit-button btn btn-primary" type="submit">Submit</button>'); return ret[0]; } - this.make_cancel_button = function(form) { + this.make_cancel_button = function (form) { var ret = $('<button class="caosdb-f-form-elements-cancel-button btn btn-primary" type="button">Cancel</button>'); ret.on("click", e => { this.logger.debug("cancel form", e, form); @@ -450,22 +464,25 @@ var form_elements = new function() { return ret[0]; } - this.make_form_field = async function(config) { + /** + * TODO make syncronous + */ + this.make_form_field = async function (config) { caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.type, "`config.type` of param `config`"); var field = undefined; const type = config.type; if (type === "date") { - field = await this.make_date_input(config); + field = this.make_date_input(config); } else if (type === "checkbox") { - field = await this.make_checkbox_input(config); + field = this.make_checkbox_input(config); } else if (type === "text") { - field = await this.make_text_input(config); + field = this.make_text_input(config); } else if (type === "double") { - field = await this.make_double_input(config); + field = this.make_double_input(config); } else if (type === "integer") { - field = await this.make_integer_input(config); + field = this.make_integer_input(config); } else if (type === "range") { field = await this.make_range_input(config); } else if (type === "reference_drop_down") { @@ -491,9 +508,11 @@ var form_elements = new function() { } - this.add_help = function(field, config) { + this.add_help = function (field, config) { var help_button = $('<span data-trigger="click focus" data-toggle="popover" class="caosdb-f-form-help pull-right glyphicon glyphicon-info-sign"/>') - .css({"cursor": "pointer"}); + .css({ + "cursor": "pointer" + }); if (typeof config === "string" || config instanceof String) { help_button.attr("data-content", config); @@ -504,16 +523,18 @@ var form_elements = new function() { var label = $(field).children("label"); - if(label.length > 0) { - help_button.css({"margin-left": "4px"}); + if (label.length > 0) { + help_button.css({ + "margin-left": "4px" + }); label.first().append(help_button); } else { $(field).append(help_button); } } - this.make_heading = function(config) { - if(typeof config.header === "undefined") { + this.make_heading = function (config) { + if (typeof config.header === "undefined") { return; } else if (typeof config.header === "string" || config.header instanceof String) { return $('<div class="caosdb-f-form-header h3">' + config.header + '</div>')[0]; @@ -522,7 +543,7 @@ var form_elements = new function() { return config.header; } - this.make_form_wrapper = function(form, config) { + this.make_form_wrapper = function (form, config) { var wrapper = $('<div class="caosdb-f-form-wrapper"/>'); var header = this.make_heading(config); @@ -554,10 +575,10 @@ var form_elements = new function() { return wrapper[0]; } - this.make_form = function(config) { + this.make_form = function (config) { var form = undefined; - if(config.script) { + if (config.script) { form = this.make_script_form(config, config.script); } else { form = this.make_generic_form(config); @@ -566,7 +587,10 @@ var form_elements = new function() { return wrapper; } - this.make_subform = async function(config) { + /** + * TODO make syncronous + */ + this.make_subform = async function (config) { this.logger.trace("enter make_subform"); caosdb_utils.assert_type(config, "object", "param `config`"); caosdb_utils.assert_string(config.name, "`config.name` of param `config`"); @@ -575,7 +599,7 @@ var form_elements = new function() { const name = config.name; var form = $('<div data-subform-name="' + name + '" class="caosdb-f-form-elements-subform"/>'); - for ( let field of config.fields ) { + for (let field of config.fields) { this.logger.trace("add subform field", field); let elem = await this.make_form_field(field); form.append(elem); @@ -585,26 +609,26 @@ var form_elements = new function() { return form[0]; } - this.dismiss_form = function(form) { - if(form.tagName === "FORM") { + this.dismiss_form = function (form) { + if (form.tagName === "FORM") { form.dispatchEvent(this.cancel_form_event); } var _form = $(form).find("form"); - if (_form.length > 0){ + if (_form.length > 0) { _form[0].dispatchEvent(this.cancel_form_event); } } - this.enable_group = function(form, group) { + this.enable_group = function (form, group) { this.enable_fields(this.get_group_fields(form, group)); } - this.disable_group = function(form, group) { + this.disable_group = function (form, group) { this.disable_fields(this.get_group_fields(form, group)); } - this.get_group_fields = function(form, group) { - return $(form).find(".caosdb-f-field[data-groups*='("+group+")']").toArray(); + this.get_group_fields = function (form, group) { + return $(form).find(".caosdb-f-field[data-groups*='(" + group + ")']").toArray(); } /** @@ -613,44 +637,44 @@ var form_elements = new function() { * @param {string} name - the field name * @return {HTMLElement[]} array of fields */ - this.get_fields = function(form, name) { + this.get_fields = function (form, name) { caosdb_utils.assert_html_element(form, "parameter `form`"); caosdb_utils.assert_string(name, "parameter `name`"); return $(form).find(".caosdb-f-field[data-field-name='" + name + "']").toArray(); } - this.add_field_to_group = function(field, group) { + this.add_field_to_group = function (field, group) { this.logger.trace("enter add_field_to_group", field, group); - var groups = ($(field).attr("data-groups")?$(field).attr("data-groups"):"") + "(" + group + ")"; + var groups = ($(field).attr("data-groups") ? $(field).attr("data-groups") : "") + "(" + group + ")"; $(field).attr("data-groups", groups); } - this.disable_fields = function(fields) { + this.disable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", true).hide(); for (const field of $(fields)) { field.dispatchEvent(this.field_disabled_event); } } - this.enable_fields = function(fields) { + this.enable_fields = function (fields) { $(fields).toggleClass("caosdb-f-field-disabled", false).show(); for (const field of $(fields)) { field.dispatchEvent(this.field_enabled_event); } } - this.enable_name = function(form, name) { + this.enable_name = function (form, name) { this.enable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.disable_name = function(form, name) { + this.disable_name = function (form, name) { this.disable_fields(form.find(".caosdb-f-field[data-field-name='" + name + "']").toArray()); } - this.make_script_form = async function(config, script) { + this.make_script_form = async function (config, script) { this.logger.trace("enter make_script_form"); - const submit_callback = async function(form) { + const submit_callback = async function (form) { form = $(form); @@ -672,7 +696,10 @@ var form_elements = new function() { }; this.logger.trace("leave make_script_form"); - const new_config = $.extend({}, {name: script, submit:submit_callback}, config); + const new_config = $.extend({}, { + name: script, + submit: submit_callback + }, config); return await this.make_generic_form(new_config); } @@ -685,7 +712,7 @@ var form_elements = new function() { * * TODO */ - this.make_generic_form = async function(config) { + this.make_generic_form = async function (config) { this.logger.trace("enter make_generic_form"); caosdb_utils.assert_type(config, "object", "param `config`"); @@ -695,14 +722,14 @@ var form_elements = new function() { const form = $('<form class="form-horizontal" action="#" method="post" />'); // set name - if(config.name) { + if (config.name) { form.attr("name", config.name); } // add fields - for ( let field of config.fields ) { + for (let field of config.fields) { this.logger.trace("add field", field); - if ( field instanceof HTMLElement ) { + if (field instanceof HTMLElement) { form.append(field); } else { let elem = await this.make_form_field(field); @@ -712,16 +739,16 @@ var form_elements = new function() { // set groups if (config.groups) { - for ( let group of config.groups ) { + for (let group of config.groups) { this.logger.trace("add group", group); - for ( let fieldname of group.fields ) { + for (let fieldname of group.fields) { let field = form.find(".caosdb-f-field[data-field-name='" + fieldname + "']"); this.logger.trace("set group", field, group); this.add_field_to_group(field, group.name) } // disable if necessary - if (typeof group.enabled === "undefined" || group.enabled ) { + if (typeof group.enabled === "undefined" || group.enabled) { this.enable_group(form, group.name); } else { this.disable_group(form, group.name); @@ -732,14 +759,14 @@ var form_elements = new function() { const footer = this.make_footer(); form.append(footer); - if(!(typeof config.submit === 'boolean' && config.submit === false)) { + if (!(typeof config.submit === 'boolean' && config.submit === false)) { // add submit button unless config.submit is false footer.append(this.make_submit_button()); } form[0].addEventListener("submit", (e) => { e.preventDefault(); e.stopPropagation(); - if(form.find(".caosdb-f-form-submitting").length > 0) { + if (form.find(".caosdb-f-form-submitting").length > 0) { // do not submit twice return; } @@ -766,9 +793,9 @@ var form_elements = new function() { const success_handler = config.success; const submit_callback = config.submit; form.find(".caosdb-f-form-elements-message").remove(); - if(typeof config.submit === "function") { + if (typeof config.submit === "function") { // wrap callback in async function - const _wrap_callback = async function() { + const _wrap_callback = async function () { try { var results = await submit_callback(form[0]); @@ -805,12 +832,12 @@ var form_elements = new function() { }, true); - form[0].addEventListener(this.form_success_event.type, function(e) { + form[0].addEventListener(this.form_success_event.type, function (e) { // remove submit button, show ok button form.find("button[type='submit']").remove(); form.find("button:contains('Cancel')").text("Ok").prop("disabled", false); }, true); - form[0].addEventListener(this.form_error_event.type, function(e) { + form[0].addEventListener(this.form_error_event.type, function (e) { // reenable inputs form.find(":input").prop("disabled", false); }, true); @@ -828,7 +855,7 @@ var form_elements = new function() { return form[0]; } - this.init_form_caching = function(config, form) { + this.init_form_caching = function (config, form) { var default_config = { "cache_event": form_elements.submit_form_event.type, "cache_storage": localStorage @@ -843,50 +870,65 @@ var form_elements = new function() { form_elements.load_cached(lconfig.cache_storage, form); } - this.show_results = function(form, results) { + this.show_results = function (form, results) { $(form).append(results); } - this.show_errors = function(form, errors) { + this.show_errors = function (form, errors) { $(form).append(errors); } - this.make_footer = function() { - return $('<div class="text-right caosdb-f-form-elements-footer"/>')[0]; + this.make_footer = function () { + return $('<div class="text-right caosdb-f-form-elements-footer"/>') + .css({ + "margin": "20px", + }).append(this.make_required_marker()) + .append('<span style="margin-right: 4px; font-size: 11px">required field</span>')[0]; } - this.make_error_message = function(message) { + this.make_error_message = function (message) { return this.make_message(message, "error"); } - this.make_success_message = function(message) { + this.make_success_message = function (message) { return this.make_message(message, "success"); } - this.make_submitting_info = function() { + this.make_submitting_info = function () { // TODO styling return $(this.make_message("Submitting... please wait. This might take some time.", "info")) - .toggleClass("h3",true) - .toggleClass("caosdb-f-form-submitting",true) - .toggleClass("text-right",true)[0]; + .toggleClass("h3", true) + .toggleClass("caosdb-f-form-submitting", true) + .toggleClass("text-right", true)[0]; } - this.make_message = function(message, type) { + this.make_message = function (message, type) { var ret = $('<div class="caosdb-f-form-elements-message"/>'); - if(type) { + if (type) { ret.addClass("caosdb-f-form-elements-message-" + type); } return ret.append(markdown.textToHtml(message))[0]; } - this.make_range_input = async function(config) { + /** + * TODO make syncronous + */ + this.make_range_input = async function (config) { // TODO // 1. wrapp both inputs to separate it from the label into a container // 2. make two rows for each input // 3. make inline-block for all included elements - const from_config = $.extend({}, {cached: config.cached, required: config.required, type: "double"}, config.from); - const to_config = $.extend({}, {cached: config.cached, required: config.required, type: "double"}, config.to); + const from_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.from); + const to_config = $.extend({}, { + cached: config.cached, + required: config.required, + type: "double" + }, config.to); const from_input = await this.make_form_field(from_config); const to_input = await this.make_form_field(to_config); @@ -919,16 +961,17 @@ var form_elements = new function() { * @param {string} name - the name of the field. * @returns {HTMLElement} a DIV. */ - this._make_field_wrapper = function(name) { + this._make_field_wrapper = function (name) { caosdb_utils.assert_string(name, "param `name`"); - return $('<div class="form-group caosdb-f-field caosdb-property-row" data-field-name="'+name+'" />')[0]; + return $('<div class="form-group caosdb-f-field caosdb-property-row" data-field-name="' + name + '" />') + .css({"padding": "0"})[0]; } - this.make_date_input = function(config) { + this.make_date_input = function (config) { return this._make_input(config); } - this.make_text_input = function(config) { + this.make_text_input = function (config) { return this._make_input(config); } @@ -941,8 +984,10 @@ var form_elements = new function() { * @param {form_elements.input_config} config. * @returns {HTMLElement} a double form field. */ - this.make_double_input = function(config) { - var clone = $.extend({}, config, {type: "number"}); + this.make_double_input = function (config) { + var clone = $.extend({}, config, { + type: "number" + }); var ret = $(this._make_input(clone)) ret.find("input").attr("step", "any"); return ret[0]; @@ -957,7 +1002,7 @@ var form_elements = new function() { * @param {form_elements.input_config} config. * @returns {HTMLElement} an integer form field. */ - this.make_integer_input = function(config) { + this.make_integer_input = function (config) { var ret = $(this.make_double_input(config)); ret.find("input").attr("step", "1"); return ret[0]; @@ -970,16 +1015,18 @@ var form_elements = new function() { * @param {form_elements.checkbox_config} config. * @returns {HTMLElement} a checkbox form field. */ - this.make_checkbox_input = function(config) { - var clone = $.extend({}, config, {type: "checkbox"}); + this.make_checkbox_input = function (config) { + var clone = $.extend({}, config, { + type: "checkbox" + }); var ret = $(this._make_input(clone)); ret.find("input:checkbox").prop("checked", false); ret.find("input:checkbox").toggleClass("form-control", false); - if(config.checked ) { + if (config.checked) { ret.find("input:checkbox").prop("checked", true); ret.find("input:checkbox").attr("checked", "checked"); } - if(config.value) { + if (config.value) { ret.find("input:checkbox").attr("value", config.value); } return ret[0]; @@ -991,22 +1038,41 @@ var form_elements = new function() { * * @param {HTMLElement} field - the required form field. */ - this.set_required = function(field) { + this.set_required = function (field) { $(field).toggleClass("caosdb-f-form-field-required", true); + $(field).find(":input").prop("required", true); + $(field).find("label").prepend(this.make_required_marker()); + } + + /** + * Return a span which is to be inserted before a field's label text + * and which marks that field as required. + * + * @returns {HTMLElement} span element. + */ + this.make_required_marker = function () { + // TODO create class and move to css file + return $('<span>*</span>') + .css({ + "font-size": "10px", + "color": "red", + "margin-right": "4px", + "font-weight": "100", + })[0]; } - this.get_enabled_required_fields = function(form) { + this.get_enabled_required_fields = function (form) { return $(this.get_enabled_fields(form)) .filter(".caosdb-f-form-field-required") .toArray(); } - this.get_enabled_fields = function(form) { + this.get_enabled_fields = function (form) { return $(form) .find(".caosdb-f-field") - .filter(function(idx){ + .filter(function (idx) { // remove disabled fields from results return !$(this).hasClass("caosdb-f-field-disabled"); }) @@ -1014,9 +1080,9 @@ var form_elements = new function() { } - this.all_required_fields_set = function(form) { + this.all_required_fields_set = function (form) { const req = form_elements.get_enabled_required_fields(form); - for ( const field of req ) { + for (const field of req) { if (!form_elements.is_set(field)) { return false; } @@ -1027,16 +1093,16 @@ var form_elements = new function() { /** * @param {HTMLElement} form - the form be validated. */ - this.is_valid = function(form) { + this.is_valid = function (form) { return form_elements.all_required_fields_set(form); } - this.toggle_submit_button_form_valid = function(form, submit) { + this.toggle_submit_button_form_valid = function (form, submit) { // TODO do not change the submit button directly. change the // `submittable` state of the form and handle the case where a form // is submitting when this function is called. - if(form_elements.is_valid(form)){ + if (form_elements.is_valid(form)) { $(submit).prop("disabled", false); } else { $(submit).prop("disabled", true); @@ -1044,9 +1110,9 @@ var form_elements = new function() { } - this.init_validator = function(form) { + this.init_validator = function (form) { const submit = $(form).find(":input[type='submit']")[0]; - if(submit) { + if (submit) { form.addEventListener("caosdb.field.changed", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); form.addEventListener("caosdb.field.enabled", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); form.addEventListener("input", (e) => form_elements.toggle_submit_button_form_valid(form, submit), true); @@ -1062,17 +1128,17 @@ var form_elements = new function() { * optional `label` * @returns {HTMLElement} a form field. */ - this._make_input = function(config) { + this._make_input = function (config) { caosdb_utils.assert_string(config.name, "the name of a form field"); let ret = $(this._make_field_wrapper(config.name)); let name = config.name; let label = this._make_input_label_str(config); let type = config.type; let value = config.value; - let input = $('<input class="form-control caosdb-property-text-value" type="' + type - + '" name="' + name - + '" />'); - input.change(function() { + let input = $('<input class="form-control caosdb-property-text-value" type="' + type + + '" name="' + name + + '" />'); + input.change(function () { ret[0].dispatchEvent(form_elements.field_changed_event); }); let input_col = $('<div class="caosdb-property-value col-sm-9"/>'); @@ -1094,19 +1160,19 @@ var form_elements = new function() { * @param {object} config - a config object with `name` and `label`. * @returns {string} a html string for a LABEL element. */ - this._make_input_label_str = function(config) { + this._make_input_label_str = function (config) { let name = config.name; let label = config.label; - return label ? '<label for="' + name - + '" data-property-name="' + name - + '" class="control-label col-sm-3">' + label - + '</label>' : ""; + return label ? '<label for="' + name + + '" data-property-name="' + name + + '" class="control-label col-sm-3">' + label + + '</label>' : ""; } } this._init_functions(); } -$(document).ready(function() { +$(document).ready(function () { caosdb_modules.register(form_elements); }); diff --git a/src/core/js/query_shortcuts.js b/src/core/js/query_shortcuts.js index 1b4765cebfd3d95faf2beb9b5e948fba4456de2b..c12e2b47b7c8b970b2a8177ebdb529dd5cb47efa 100644 --- a/src/core/js/query_shortcuts.js +++ b/src/core/js/query_shortcuts.js @@ -38,7 +38,7 @@ * [ * { * "description": "Find all geological data sets with temperature above {temp} K.", - * "query": "FIND Record Geodata with temperature > \"$1\"" + * "query": "FIND Record Geodata with temperature > \"{temp}\"" * }, { * ... * } diff --git a/src/core/js/tour.js b/src/core/js/tour.js index 9e4be98a80ea72fb2dd49d124b4b8536c5ecc8d6..d798eb558266179483c7be7ebf37f5f083e78a1c 100644 --- a/src/core/js/tour.js +++ b/src/core/js/tour.js @@ -242,7 +242,7 @@ var tour = new function() { if(this.config.deactivate_other) { for (const element of this.elements) { if(element instanceof tour.PageSet && element !== trigger) { - element._deactivate(); + element.deactivate(); } } } @@ -565,17 +565,18 @@ var tour = new function() { return; } const ids = Object.keys(highlighters) + var highlightable = {} for (const id of ids) { - var highlightable = $(highlighters[id]); + highlightable[id] = $(highlighters[id]); if (id == "button") { // special case, don't look for the highlighter in the content - this._apply_highlighter(button, highlightable); + this._apply_highlighter(button, highlightable[id]); } else { $(button).on('shown.bs.popover', (e) => { console.log("Highlighting:") console.log(highlighters[id]); - console.log(highlightable); - this._apply_highlighter("#" + id, highlightable); + console.log(highlightable[id]); + this._apply_highlighter("#" + id, highlightable[id]); }); } } @@ -900,7 +901,7 @@ var tour = new function() { if(this.config.deactivate_other) { for (const element of this.elements) { if(element instanceof tour.PageSet && element !== trigger) { - element._deactivate(); + element.deactivate(); } } } diff --git a/src/core/js/webcaosdb.js b/src/core/js/webcaosdb.js index a66e699605844d6b1160b25bb68913de9722354c..c213a0ebcd766a1d1c2502cc8aaae2fc014ff840 100644 --- a/src/core/js/webcaosdb.js +++ b/src/core/js/webcaosdb.js @@ -151,7 +151,7 @@ this.connection = new function() { this.get = async function _get(uri) { try { return await $.ajax({ - url: window.sessionStorage.caosdbBasePath + uri, + url: connection.getBasePath() + uri, dataType: "xml", }); } catch (error) { @@ -173,7 +173,7 @@ this.connection = new function() { this.put = async function _put(uri, data) { try { return await $.ajax({ - url: window.sessionStorage.caosdbBasePath + uri, + url: connection.getBasePath() + uri, method: 'PUT', dataType: "xml", processData: false, @@ -208,7 +208,7 @@ this.connection = new function() { } try { return await $.ajax({ - url: window.sessionStorage.caosdbBasePath + "scripting", + url: connection.getBasePath() + "scripting", method: 'POST', dataType: "xml", contentType: false, @@ -234,7 +234,7 @@ this.connection = new function() { this.post = async function _post(uri, data) { try { return await $.ajax({ - url: window.sessionStorage.caosdbBasePath + uri, + url: connection.getBasePath() + uri, method: 'POST', dataType: "xml", processData: false, @@ -259,7 +259,7 @@ this.connection = new function() { this.deleteEntities = async function _deleteEntities(idline) { try { return await $.ajax({ - url: window.sessionStorage.caosdbBasePath + "Entity/" + idline, + url: connection.getBasePath() + "Entity/" + idline, method: 'DELETE', dataType: "xml", processData: false, @@ -281,9 +281,11 @@ this.connection = new function() { * Return the base path of the server. */ this.getBasePath = function() { - let a = document.createElement('a'); - a.href = window.sessionStorage.caosdbBasePath; - return a.href; + var base = window.location.origin + "/"; + if (typeof window.sessionStorage.caosdbBasePath !== "undefined") { + base = window.sessionStorage.caosdbBasePath; + } + return base; } /** @@ -344,7 +346,7 @@ this.transformation = new function() { */ this.transformParent = async function _tME(xml) { var xsl = await transformation.retrieveXsltScript("parent.xsl"); - insertParam(xsl, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + insertParam(xsl, "entitypath", connection.getBasePath() + "Entity/"); // TODO the following line should not have any effect. nevertheless: it // does not work without xsl = str2xml(xml2str(xsl)); @@ -358,8 +360,8 @@ this.transformation = new function() { */ this.transformProperty = async function _tME(xml) { var xsl = await transformation.retrieveXsltScript("property.xsl"); - insertParam(xsl, "filesystempath", window.sessionStorage.caosdbBasePath + "FileSystem/"); - insertParam(xsl, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + insertParam(xsl, "filesystempath", connection.getBasePath() + "FileSystem/"); + insertParam(xsl, "entitypath", connection.getBasePath() + "Entity/"); insertParam(xsl, "close-char", '×'); var entityXsl = await transformation.retrieveXsltScript('entity.xsl'); var messageXsl = await transformation.retrieveXsltScript('messages.xsl'); @@ -389,8 +391,8 @@ this.transformation = new function() { var commonXsl = await transformation.retrieveXsltScript("common.xsl"); var errorXsl = await transformation.retrieveXsltScript('messages.xsl'); var xslt = transformation.mergeXsltScripts(entityXsl, [errorXsl, commonXsl]); - insertParam(xslt, "filesystempath", window.sessionStorage.caosdbBasePath + "FileSystem/"); - insertParam(xslt, "entitypath", window.sessionStorage.caosdbBasePath + "Entity/"); + insertParam(xslt, "filesystempath", connection.getBasePath() + "FileSystem/"); + insertParam(xslt, "entitypath", connection.getBasePath() + "Entity/"); insertParam(xslt, "close-char", '×'); xslt = injectTemplate(xslt, '<xsl:template match="/"><div class="root"><xsl:apply-templates select="Response/*" mode="entities"/></div></xsl:template>'); return xslt; @@ -474,7 +476,9 @@ this.transaction = new function() { /** * Generate the URI for the retrieval of a list of entities. - * + * + * TODO merge this with connection.getEntityUri + * * @param {String[]} entityIds - An array of entity ids.. * @return {String} The uri. */ @@ -956,7 +960,7 @@ var queryForm = new function() { if(paging && paging.length > 0) { pagingparam = "P=" + paging + "&"; } - location.href = window.sessionStorage.caosdbBasePath + "Entity/?" + pagingparam + "query=" + query; + location.href = connection.getBasePath() + "Entity/?" + pagingparam + "query=" + query; } this.bindOnClick = function(form, setter) { @@ -1202,10 +1206,10 @@ function xml2str(xml) { * @return object containing the configuration */ async function load_config(filename) { - var uri = connection.getBasePath() + "webinterface/conf/" + filename; + const uri = connection.getBasePath() + "webinterface/${BUILD_NUMBER}/conf/" + filename; try { var data = await $.ajax({ - url: connection.getBasePath() + "webinterface/${BUILD_NUMBER}/conf/" + filename, + url: uri, dataType: "json", }); } catch (error) { diff --git a/src/core/pics/map_tile_caosdb_logo.png b/src/core/pics/map_tile_caosdb_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..10d930f630c4bd3b863be98d0c13e860686145d4 Binary files /dev/null and b/src/core/pics/map_tile_caosdb_logo.png differ diff --git a/src/core/webcaosdb.xsl b/src/core/webcaosdb.xsl index de4bdc9a5728da61799b05b23f5030ed25dfbc4a..ff12c424743d5b7cb9294f3284b22b147fb94942 100644 --- a/src/core/webcaosdb.xsl +++ b/src/core/webcaosdb.xsl @@ -46,9 +46,7 @@ </head> <body> <xsl:call-template name="caosdb-top-navbar" /> - <xsl:call-template name="paging-panel"/> <xsl:call-template name="caosdb-data-container" /> - <xsl:call-template name="paging-panel"/> <footer> <xsl:call-template name="caosdb-footer"/> </footer> diff --git a/src/core/xsl/main.xsl b/src/core/xsl/main.xsl index cd0ff0d770802793779a7cc836e144a7122ca111..038b9765bcf263dca7aeafa91a6f2f28c11401f5 100644 --- a/src/core/xsl/main.xsl +++ b/src/core/xsl/main.xsl @@ -70,7 +70,7 @@ </xsl:element> <xsl:element name="link"> <xsl:attribute name="rel">stylesheet</xsl:attribute> - <xsl:attribute name="href">n + <xsl:attribute name="href"> <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/css/leaflet.css')"/> </xsl:attribute> </xsl:element> @@ -208,12 +208,12 @@ </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/tour.js')"/> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_map.js')"/> </xsl:attribute> </xsl:element> <xsl:element name="script"> <xsl:attribute name="src"> - <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/ext_map.js')"/> + <xsl:value-of select="concat($basepath,'webinterface/${BUILD_NUMBER}/js/tour.js')"/> </xsl:attribute> </xsl:element> <!--JS_EXTENSIONS--> @@ -222,6 +222,7 @@ <div class="container caosdb-f-main"> <div class="row caosdb-v-main-col"> <div class="panel-group caosdb-f-main-entities"> + <xsl:call-template name="paging-panel"/> <xsl:apply-templates select="/Response/UserInfo"/> <xsl:apply-templates mode="top-level-data" select="/Response/*"/> <xsl:apply-templates mode="query-results" select="/Response/Query"/> @@ -231,6 +232,7 @@ <xsl:if test="count(/Response/*)<2 and not(/Response/Error|/Response/Info|/Response/Warning)"> <xsl:call-template name="welcome"/> </xsl:if> + <xsl:call-template name="paging-panel"/> </div> </div> <div class="panel panel-warning caosdb-f-edit caosdb-v-edit-panel caosdb-v-edit-panel hidden"> diff --git a/test/core/index.html b/test/core/index.html index f964f8f8388c2c4ff8c16f29dbd6f77185a58dc0..c91a1261e0b7c305fc135666e2d5fff4addfd6c0 100644 --- a/test/core/index.html +++ b/test/core/index.html @@ -58,6 +58,9 @@ <script src="js/leaflet.js"></script> <script src="js/leaflet-graticule.js"></script> <script src="js/leaflet-latlng-graticule.js"></script> + <script src="js/leaflet-coordinates.js"></script> + <script src="js/proj4.js"></script> + <script src="js/proj4leaflet.js"></script> <script src="js/ext_map.js"></script> <!--EXTENSIONS--> <script src="js/modules/webcaosdb.js.js"></script> diff --git a/test/core/js/modules/edit_mode.js.js b/test/core/js/modules/edit_mode.js.js index 20ad9e0edecbae7c23883d2d1609764c22691879..98fd3b2f9aac4f23f52edc6a57d6740d41600914 100644 --- a/test/core/js/modules/edit_mode.js.js +++ b/test/core/js/modules/edit_mode.js.js @@ -38,6 +38,9 @@ QUnit.module("edit_mode.js", { globalError(err); done(); }); + }, + after: function(assert) { + $('.modal.fade').has(".dropzone").remove(); } }); @@ -60,10 +63,6 @@ QUnit.test("init", function(assert){ assert.ok(edit_mode.init); }); -QUnit.test("scroll_edit_panel", function(assert){ - assert.ok(edit_mode.scroll_edit_panel); -}); - QUnit.test("dragstart", function(assert){ assert.ok(edit_mode.dragstart); }); diff --git a/test/core/js/modules/ext_map.js.js b/test/core/js/modules/ext_map.js.js index 74573b522926cc4093411989821c00771095eea7..88a66a1c11c6691d7f4db53a502a5734ab4cbb3f 100644 --- a/test/core/js/modules/ext_map.js.js +++ b/test/core/js/modules/ext_map.js.js @@ -22,10 +22,33 @@ 'use strict'; -QUnit.module("ext_map.js"); +QUnit.module("ext_map.js", { + before: function(assert) { + var lat = "latitude"; + var lng = "longitude"; + this.datamodel = { lat: lat, lng: lng }; + this.test_map_entity = ` +<div class="caosdb-entity-panel caosdb-properties"> + <div class="caosdb-id">1234</div> + <div class="list-group-item caosdb-property-row"> + <div class="caosdb-property-name">` + + lat + `</div> + <div class="caosdb-property-value">1.23</div> + </div> + <div class="list-group-item caosdb-property-row"> + <div class="caosdb-property-name">` + + lng + `</div> + <div class="caosdb-property-value">5.23</div> + </div> +</div>`; + }, + beforeEach: function(assert) { + sessionStorage.removeItem("caosdb_map.view"); + } +}); QUnit.test("availability", function(assert) { - assert.equal(caosdb_map.version, "0.2", "test version"); + assert.equal(caosdb_map.version, "0.3", "test version"); assert.ok(caosdb_map.init, "init available"); }); @@ -36,7 +59,10 @@ QUnit.test("default config", function(assert) { QUnit.test("load_config", async function(assert) { assert.ok(caosdb_map.load_config, "available"); - assert.ok(await caosdb_map.load_config(), "returns something"); + var config = await caosdb_map.load_config("non_existing.json"); + assert.ok(config, "returns something"); + assert.equal(config.views.length, 1, "one view in default"); + assert.equal(config.views[0].id, "UNCONFIGURED", "view has id 'UNCONFIGURED'."); }); QUnit.test("check_config", function(assert) { @@ -92,4 +118,118 @@ QUnit.test("create_map_panel", function(assert) { assert.ok($(panel).hasClass("container"), "has class container"); }); +QUnit.test("create_map_view", function(assert) { + var view_config = $.extend(true, {}, caosdb_map._unconfigured_views[0], + {"select": true, "view_change": true}); + var map_panel = $("<div/>"); + + var map = caosdb_map.create_map_view(map_panel[0], view_config); + + console.log(map_panel[0]); + assert.ok(map instanceof L.Map, "map instance created"); + assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + + assert.notOk(map._crs, "no special crs"); + map.remove(); + + // test with pre-defined crs + view_config["crs"] = "Simple"; + map = caosdb_map.create_map_view(map_panel[0], view_config); + + console.log(map_panel[0]); + assert.ok(map instanceof L.Map, "map instance created"); + assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + assert.equal(map._crs, L.CRS.Simple, "map has SIMPLE crs"); + + map.remove(); + + // test with special crs: + view_config["crs"] = { + "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"] + } + }; + + map = caosdb_map.create_map_view(map_panel[0], view_config); + + console.log(map_panel[0]); + assert.ok(map instanceof L.Map, "map instance created"); + assert.ok(map_panel.hasClass("leaflet-container"), "map_panel has .leaflet-container child"); + assert.ok(map._crs instanceof L.Proj.CRS, "map has special crs"); + + map.remove(); + +}); + +QUnit.test("get_map_entities", function(assert) { + var datamodel = this.datamodel; + var container = $('<div/>').append(this.test_map_entity); + var map_objects = caosdb_map.get_map_entities(container[0], datamodel); + assert.equal(map_objects.length, 1, "has one map object"); +}); + + +QUnit.test("create_entitiy_markers", function(assert) { + var datamodel = this.datamodel; + var entities = $(this.test_map_entity).toArray(); + + // w/o popup + var markers = caosdb_map.create_entitiy_markers(entities, datamodel); + assert.equal(markers.length, 1, "has one marker"); + assert.ok(markers[0] instanceof L.Marker, "is marker"); + var latlng = markers[0]._latlng; + assert.equal(latlng.lat, "1.23", "latitude set"); + assert.equal(latlng.lng, "5.23", "longitude set"); + assert.notOk(markers[0].getPopup(), "no popup"); + + // with popup + var markers = caosdb_map.create_entitiy_markers(entities, datamodel, ()=>"popup"); + assert.ok(markers[0].getPopup(), "has popup"); +}); + + +QUnit.test("_add_current_page_entities", function(assert) { + var datamodel = this.datamodel; + var layerGroup = L.layerGroup(); + var container = $('<div class="caosdb-f-main-entities"/>').append(this.test_map_entity); + $("body").append(container); + + assert.equal(layerGroup.getLayers().length, 0, "no layer"); + var cpe = caosdb_map._get_current_page_entities(datamodel, undefined, undefined, undefined, undefined); + assert.equal(cpe.length, 1, "has one entity"); + container.remove(); +}); + + +QUnit.test("make_layer_chooser_html", function(assert) { + var test_conf = { "id": "test_id", + "name": "test name", + "description": "test description", + "icon": { "html": "<span>ICON</span>", + }, + }; + + var layer_chooser = caosdb_map.make_layer_chooser_html(test_conf); + assert.ok(layer_chooser, "available"); + assert.equal($(layer_chooser).attr("title"), "test description", "description set as title"); +}); + +QUnit.test("init_entity_layer", function(assert) { + var done = assert.async(); + var test_conf = { "id": "test_id", + "name": "test name", + "description": "test description", + "get_entities": async function() {done(); return []}, + "icon": { "html": "<span>ICON</span>", + }, + } + + var entityLayer= caosdb_map.init_entity_layer(test_conf); + assert.equal(entityLayer.id, test_conf.id, "id"); + assert.equal(entityLayer.active, true, "is active"); + assert.ok(entityLayer.chooser_html instanceof HTMLElement, "chooser_html is HTMLElement"); + assert.equal(entityLayer.layer_group.getLayers().length, 0 , "empty layergroup"); +}); diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js index a82af8e2b7da861404abd03e4f7bfd131ff3a32e..1789f31676590ff9293bdaa26cc3bd133f89824e 100644 --- a/test/core/js/modules/ext_references.js.js +++ b/test/core/js/modules/ext_references.js.js @@ -44,11 +44,6 @@ QUnit.test("update_single_resolvable_reference", function(assert){ assert.ok(resolve_references.update_single_resolvable_reference); }); -QUnit.test("references", function(assert){ - assert.ok(resolve_references.references); -}); - - /* * This test checks whether all required diff --git a/test/core/js/modules/ext_xsl_download.js.js b/test/core/js/modules/ext_xsl_download.js.js index 360b389725eec2bc6a4e4115e9cc46cd91e33d69..12ba18b9da3bf393d8249cb5456c7016f13456d6 100644 --- a/test/core/js/modules/ext_xsl_download.js.js +++ b/test/core/js/modules/ext_xsl_download.js.js @@ -37,7 +37,7 @@ QUnit.module("ext_xls_download"); return str2xml('<response><script code="0" /><stdout>bla</stdout></response>'); } - _go_to_script_results = function(xls_link, filename) { + var _go_to_script_results = function(xls_link, filename) { xls_link.setAttribute( "href", location.protocol + "//" +location.host + "/Shared/" + filename); diff --git a/test/core/js/modules/navbar.xsl.js b/test/core/js/modules/navbar.xsl.js index 3649c1908939400d0b7a77b4de90cf29dfde638b..0b43cae70cbd4694290885560c69594a5d4a555e 100644 --- a/test/core/js/modules/navbar.xsl.js +++ b/test/core/js/modules/navbar.xsl.js @@ -35,7 +35,7 @@ QUnit.module("navbar.xsl", { }).done(function(data, textStatus, jdXHR) { insertParam(data, "entitypath", "/entitypath/"); insertParam(data, "filesystempath", "/filesystempath/"); - insertParam(data, "basepath", "/basepath/"); + insertParam(data, "basepath", window.location.origin); qunit_obj.navbarXsl = injectTemplate(injectTemplate(data, '<xsl:template name="make-filesystem-link"><filesystemlink/></xsl:template>'), '<xsl:template name="caosdb-query-panel"><query/></xsl:template>'); }).always(function() { done(); diff --git a/test/core/js/modules/webcaosdb.js.js b/test/core/js/modules/webcaosdb.js.js index aba5d845d4032514f17bcddac0916cd73d421018..7e02d1d98270f0c5f696bcbe39651fe0b3064c03 100644 --- a/test/core/js/modules/webcaosdb.js.js +++ b/test/core/js/modules/webcaosdb.js.js @@ -146,7 +146,6 @@ QUnit.test("createErrorNotification", function(assert) { /* MODULE connection */ QUnit.module("webcaosdb.js - connection", { before: function(assert) { - window.sessionStorage.caosdbBasePath = "../../"; assert.ok(connection, "connection module is defined"); } }); @@ -460,7 +459,6 @@ QUnit.module("webcaosdb.js - preview", { done(); }); - window.sessionStorage.caosdbBasePath = "../../" assert.ok(preview, "preview module is defined"); }, afterEach: function(assert) { @@ -1444,7 +1442,6 @@ QUnit.module("webcaosdb.js - annotation", { annotation.postCommentXml = function(xml) { return new Promise(resolve => setTimeout(resolve, 1000, str2xml("<Response/>"))); } - window.sessionStorage.caosdbBasePath = "../../"; } }); diff --git a/test/core/js/setup.js b/test/core/js/setup.js index 701195ce7cef08190a1dbf066f240a9ed52a5dce..f2bca4e928b8d5ef01b19ef6cd2d4425bae0ee24 100644 --- a/test/core/js/setup.js +++ b/test/core/js/setup.js @@ -33,19 +33,19 @@ function getQueryValue(key) { if (loggerPort) { console.log("logging QUnit results to http://127.0.0.1:" + loggerPort); + QUnit.done(function( details ) { + console.log("done"); + let report = (details.failed === 0 ? "SUCCESS\n" : "FAILURE\n") + "Total: " + details.total + "\nFailed: " + details.failed + "\nPassed: " + details.passed + "\nRuntime: " + details.runtime + "\n"; + console.log(report); + return $.post("http://127.0.0.1:" + loggerPort + "/done", report); + }); + + QUnit.config.testTimeout = 30000; QUnit.log(function(obj) { - // TODO if(!obj.result) { - var failed_assertion = { - "Actual": obj.actual, - "Expected": obj.expected, - "Message": obj.message, - "Source": obj.source, - "Module": obj.module, - "Name": obj.name, - } - $.post("http://127.0.0.1:" + loggerPort + "/log", JSON.stringify(failed_assertion, null, 2)); + var failed_assertion = JSON.stringify(obj, null, 2); + $.post("http://127.0.0.1:" + loggerPort + "/log", "FAILURE\n" + failed_assertion); } }); @@ -63,15 +63,8 @@ function getQueryValue(key) { "Runtime": details.runtime }; - $.post("http://127.0.0.1:" + loggerPort + "/log", JSON.stringify( result, null, 2 )); + return $.post("http://127.0.0.1:" + loggerPort + "/log", JSON.stringify( result, null, 2 )); - } ); - - QUnit.done(function( details ) { - console.log("done"); - let report = (details.failed === 0 ? "SUCCESS\n" : "") + "Total: " + details.total + "\nFailed: " + details.failed + "\nPassed: " + details.passed + "\nRuntime: " + details.runtime + "\n"; - console.log(report); - $.post("http://127.0.0.1:" + loggerPort + "/done", report); });