From 9b7b0fec85b9887714e8f0e0d289b59b10358f95 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Mon, 20 Apr 2020 17:20:09 +0100 Subject: [PATCH 01/21] Configurable ES Leaflet map base layers Includes support for Google and WMS base layers. --- .../jquery.idc.leafletMap.js | 74 ++++++++++++++++--- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/js/indicia.datacomponents/jquery.idc.leafletMap.js b/js/indicia.datacomponents/jquery.idc.leafletMap.js index 67e4260f..773ddc34 100644 --- a/js/indicia.datacomponents/jquery.idc.leafletMap.js +++ b/js/indicia.datacomponents/jquery.idc.leafletMap.js @@ -43,6 +43,16 @@ initialLng: -2.89479, initialZoom: 5, baseLayer: 'OpenStreetMap', + baseLayerConfig: { + OpenStreetMap: { + title: 'Open Street Map', + type: 'OpenStreetMap' + }, + OpenTopoMap: { + title: 'Open Topo Map', + type: 'OpenTopoMap' + } + }, cookies: true }; @@ -405,6 +415,48 @@ } } + /** + * Build the list of base map layers. + */ + function getBaseMaps(el) { + var baseLayers = {}; + var subType; + var wmsOptions; + $.each(el.settings.baseLayerConfig, function eachLayer(title) { + if (this.type === 'OpenStreetMap') { + baseLayers[title] = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }); + } else if (this.type === 'OpenTopoMap') { + baseLayers[title] = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + maxZoom: 17, + attribution: 'Map data: © OpenStreetMap contributors, ' + + 'SRTM | Map style: © OpenTopoMap ' + + '(CC BY-SA)' + }); + } else if (this.type === 'Google') { + subType = this.config && this.config.subType; + if ($.inArray(subType, ['roadmap', 'satellite', 'terrain', 'hybrid']) === -1) { + indiciaFns.controlFail(el, 'Unknown Google layer subtype ' + subType); + } + baseLayers[title] = L.gridLayer.googleMutant({ + type: subType + }); + } else if (this.type === 'WMS') { + wmsOptions = { + format: 'image/png' + }; + if (typeof this.config.wmsOptions !== 'undefined') { + $.extend(wmsOptions, this.config.wmsOptions); + } + baseLayers[title] = L.tileLayer.wms(this.config.sourceUrl, wmsOptions); + } else { + indiciaFns.controlFail(el, 'Unknown baseLayerConfig type ' + this.type); + } + }); + return baseLayers; + } + /** * Declare public methods. */ @@ -470,19 +522,17 @@ } justClickedOnFeature = false; }); - baseMaps = { - OpenStreetMap: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }), - OpenTopoMap: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { - maxZoom: 17, - attribution: 'Map data: © OpenStreetMap contributors, ' + - 'SRTM | Map style: © OpenTopoMap ' + - '(CC BY-SA)' - }) - }; + baseMaps = getBaseMaps(el); + if (baseMaps.length === 0) { + indiciaFns.controlFail(el, 'No base maps configured for map'); + } // Add the active base layer to the map. - baseMaps[el.settings.baseLayer].addTo(el.map); + if (baseMaps[el.settings.baseLayer]) { + baseMaps[el.settings.baseLayer].addTo(el.map); + } else { + // Fallback if layer missing, e.g. due to out of date cookie. + baseMaps[Object.keys(baseMaps)[0]].addTo(el.map); + } $.each(el.settings.layerConfig, function eachLayer(id, layer) { var group; var wmsOptions; From feafb05ba7ba11345acfeb75a07719c2c4422b20 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Tue, 21 Apr 2020 10:03:46 +0100 Subject: [PATCH 02/21] Standardise the way that sort settings are provided autoAggregationTable sorts now just have a key/value pair (field + asc|desc), removing the order sub-key which was inconsistent with how sorts are specified on normal data sources. --- js/indicia.datacomponents/idc.esDataSource.js | 21 +++++++------------ .../jquery.idc.dataGrid.js | 4 +--- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/js/indicia.datacomponents/idc.esDataSource.js b/js/indicia.datacomponents/idc.esDataSource.js index b35d0d6c..e6422e0f 100644 --- a/js/indicia.datacomponents/idc.esDataSource.js +++ b/js/indicia.datacomponents/idc.esDataSource.js @@ -32,27 +32,20 @@ var IdcEsDataSource; * For autoAggregationTable mode. Sets default to the unique field if not * set. */ - function getSortInfo(source) { + function getAutoAggSortInfo(source) { var sortField; var sortDir; var settings = source.settings; // Default source config to sort by the unique field unless otherwise specified. if (!settings.sort || settings.sort.length === 0) { settings.sort = {}; - settings.sort[settings.autoAggregationTable.unique_field] = { - order: 'asc' - }; + settings.sort[settings.autoAggregationTable.unique_field] = 'asc'; } - // Find the sort field and direction from the source config. - $.each(settings.sort, function eachSortField(field) { - sortField = field; - sortDir = this.order; - // Only support a single sort field. - return false; - }); + // Find the sort field and direction from the source config. Only single + // supported in autoAggregationTable mode at present. return { - field: sortField, - dir: sortDir + field: Object.keys(settings.sort)[0], + dir: settings.sort[Object.keys(settings.sort)[0]] }; } @@ -66,7 +59,7 @@ var IdcEsDataSource; var settings = source.settings; if (settings.autoAggregationTable) { settings.size = 0; - sort = getSortInfo(source); + sort = getAutoAggSortInfo(source); // List of sub-aggregations within the outer terms agg for the unique field must // always contain a top_hits agg to retrieve field values. subAggs = { diff --git a/js/indicia.datacomponents/jquery.idc.dataGrid.js b/js/indicia.datacomponents/jquery.idc.dataGrid.js index 00c9f63a..b7bf417e 100644 --- a/js/indicia.datacomponents/jquery.idc.dataGrid.js +++ b/js/indicia.datacomponents/jquery.idc.dataGrid.js @@ -316,9 +316,7 @@ } else { sortFields = [fieldName]; } - source.settings.sort[sortFields[0]] = { - order: sortDesc ? 'desc' : 'asc' - }; + source.settings.sort[sortFields[0]] = sortDesc ? 'desc' : 'asc'; } /** From ab4aa1b981c22a1c60a49a209748dbf5c2eac7de Mon Sep 17 00:00:00 2001 From: John van Breda Date: Tue, 21 Apr 2020 10:33:17 +0100 Subject: [PATCH 03/21] Allow doc_count use in autoAggregationTable --- js/indicia.datacomponents/idc.esDataSource.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/indicia.datacomponents/idc.esDataSource.js b/js/indicia.datacomponents/idc.esDataSource.js index e6422e0f..ea5aa3ec 100644 --- a/js/indicia.datacomponents/idc.esDataSource.js +++ b/js/indicia.datacomponents/idc.esDataSource.js @@ -33,8 +33,6 @@ var IdcEsDataSource; * set. */ function getAutoAggSortInfo(source) { - var sortField; - var sortDir; var settings = source.settings; // Default source config to sort by the unique field unless otherwise specified. if (!settings.sort || settings.sort.length === 0) { @@ -42,9 +40,10 @@ var IdcEsDataSource; settings.sort[settings.autoAggregationTable.unique_field] = 'asc'; } // Find the sort field and direction from the source config. Only single - // supported in autoAggregationTable mode at present. + // supported in autoAggregationTable mode at present. Doc_count is a + // special value that sorts by _count. return { - field: Object.keys(settings.sort)[0], + field: Object.keys(settings.sort)[0] === 'doc_count' ? '_count' : Object.keys(settings.sort)[0], dir: settings.sort[Object.keys(settings.sort)[0]] }; } From a9f357f6be176b5f05065c5865f8d020ae69ccde Mon Sep 17 00:00:00 2001 From: John van Breda Date: Tue, 21 Apr 2020 14:56:57 +0100 Subject: [PATCH 04/21] Option to set esLeafletMap selectedfeaturestyle --- .../jquery.idc.leafletMap.js | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/js/indicia.datacomponents/jquery.idc.leafletMap.js b/js/indicia.datacomponents/jquery.idc.leafletMap.js index 773ddc34..2328ed0a 100644 --- a/js/indicia.datacomponents/jquery.idc.leafletMap.js +++ b/js/indicia.datacomponents/jquery.idc.leafletMap.js @@ -53,7 +53,11 @@ type: 'OpenTopoMap' } }, - cookies: true + cookies: true, + selectedFeatureStyle: { + color: '#FF0000', + fillColor: '#FF0000', + } }; /** @@ -186,7 +190,6 @@ circle.removeFrom(el.map); break; case 'geom': - wkt = new Wkt.Wkt(); wkt.read(geom); obj = wkt.toObject(config.options); @@ -203,13 +206,16 @@ * Thicken the borders of selected features when zoomed out to aid visibility. */ function ensureFeatureClear(el, feature) { - var weight = Math.min(20, Math.max(1, 20 - (el.map.getZoom()))); - var opacity = Math.min(1, Math.max(0.6, el.map.getZoom() / 18)); + var style; if (typeof feature.setStyle !== 'undefined') { - feature.setStyle({ - weight: weight, - opacity: opacity - }); + style = $.extend({}, el.settings.selectedFeatureStyle); + if (!style.weight) { + style.weight = Math.min(20, Math.max(1, 20 - (el.map.getZoom()))); + } + if (!style.opacity) { + style.opacity = Math.min(1, Math.max(0.6, el.map.getZoom() / 18)); + } + feature.setStyle(style); } } @@ -654,7 +660,7 @@ } }); // Are there document hits to map? - $.each(response.hits.hits, function eachHit() { + $.each(response.hits.hits, function eachHit(i) { var latlon = this._source.location.point.split(','); addFeature(el, sourceSettings.id, latlon, this._source.location.geom, this._source.location.coordinate_uncertainty_in_meters, '_id', this._id); From 79e3dd4c376feafb6d0b30eaf202947f1c7edb0c Mon Sep 17 00:00:00 2001 From: John van Breda Date: Mon, 27 Apr 2020 14:28:31 +0100 Subject: [PATCH 05/21] Add EPSG:3109 (Jersey TM) --- js/proj4defs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/proj4defs.js b/js/proj4defs.js index ef5cc26e..8e1065f4 100644 --- a/js/proj4defs.js +++ b/js/proj4defs.js @@ -12,4 +12,5 @@ Proj4js.defs["EPSG:31464"] = "+proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 + Proj4js.defs['EPSG:23030'] = "+proj=utm +zone=30 +ellps=intl +units=m +no_defs +towgs84=-87,-98,-121"; Proj4js.defs["EPSG:31466"] = "+proj=tmerc +lat_0=0 +lon_0=6 +k=1 +x_0=2500000 +y_0=0 +ellps=bessel +datum=potsdam +units=m +no_defs"; Proj4js.defs["EPSG:31467"] = "+proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +datum=potsdam +units=m +no_defs"; -Proj4js.defs["EPSG:31468"] = "+proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +datum=potsdam +units=m +no_defs"; \ No newline at end of file +Proj4js.defs["EPSG:31468"] = "+proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +datum=potsdam +units=m +no_defs"; +Proj4js.defs["EPSG:3109"] = "+proj=tmerc +lat_0=49.225 +lon_0=-2.135 +k=0.9999999000000001 +x_0=40000 +y_0=70000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs"; \ No newline at end of file From e89804f979bdaa1b62ed41149743c6374156aaba Mon Sep 17 00:00:00 2001 From: John van Breda Date: Mon, 27 Apr 2020 14:37:11 +0100 Subject: [PATCH 06/21] Some fixes relating to autoAggregationTables and keyword mappings. The request source filtering must refer to the fieldname without the .keyword suffix. --- js/indicia.datacomponents/idc.esDataSource.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/js/indicia.datacomponents/idc.esDataSource.js b/js/indicia.datacomponents/idc.esDataSource.js index ea5aa3ec..e8ae680f 100644 --- a/js/indicia.datacomponents/idc.esDataSource.js +++ b/js/indicia.datacomponents/idc.esDataSource.js @@ -56,6 +56,8 @@ var IdcEsDataSource; var sort; var orderBy; var settings = source.settings; + var uniqueFieldWithoutSuffix; + var sortFieldWithoutSuffix; if (settings.autoAggregationTable) { settings.size = 0; sort = getAutoAggSortInfo(source); @@ -83,12 +85,15 @@ var IdcEsDataSource; } }); // Include the unique field in the list of fields request even if not specified. - if ($.inArray(settings.autoAggregationTable.unique_field, settings.autoAggregationTable.fields) === -1) { - settings.autoAggregationTable.fields.push(settings.autoAggregationTable.unique_field); + // Don't include .keyword suffix for source filtering. + uniqueFieldWithoutSuffix = settings.autoAggregationTable.unique_field.replace(/\.keyword$/, ''); + if ($.inArray(uniqueFieldWithoutSuffix, settings.autoAggregationTable.fields) === -1) { + settings.autoAggregationTable.fields.push(uniqueFieldWithoutSuffix); } - if ($.inArray(sort.field, settings.autoAggregationTable.fields) > -1) { + sortFieldWithoutSuffix = sort.field.replace(/\.keyword$/, ''); + if ($.inArray(sortFieldWithoutSuffix, settings.autoAggregationTable.fields) > -1) { // Sorting by a standard field. - if (sort.field === settings.autoAggregationTable.unique_field) { + if (sortFieldWithoutSuffix === uniqueFieldWithoutSuffix) { // Using the outer agg to sort, so simple use of _key. orderBy = '_key'; } else { From 7861a789d121e48395dfe3640285d370b929fcf6 Mon Sep 17 00:00:00 2001 From: John van Breda Date: Tue, 5 May 2020 09:36:59 +0100 Subject: [PATCH 07/21] Significant rewrite to simplify config Still needs further testing and refactoring --- js/indicia.datacomponents/idc.core.js | 249 ++++++- js/indicia.datacomponents/idc.esDataSource.js | 646 +++++++++--------- .../jquery.idc.dataGrid.js | 258 ++++--- .../jquery.idc.esDownload.js | 74 +- .../jquery.idc.leafletMap.js | 7 +- 5 files changed, 749 insertions(+), 485 deletions(-) diff --git a/js/indicia.datacomponents/idc.core.js b/js/indicia.datacomponents/idc.core.js index f0f91d40..983c94dc 100644 --- a/js/indicia.datacomponents/idc.core.js +++ b/js/indicia.datacomponents/idc.core.js @@ -110,14 +110,33 @@ }; /** - * Initially populate the data sources. + * Instantiate the data sources. */ - indiciaFns.populateDataSources = function populateDataSources() { + indiciaFns.initDataSources = function initDataSources() { // Build the Elasticsearch source objects and run initial population. $.each(indiciaData.esSources, function eachSource() { var sourceObject = new IdcEsDataSource(this); indiciaData.esSourceObjects[this.id] = sourceObject; - sourceObject.populate(); + }); + }; + + /** + * Hookup datasources to their controls. + */ + indiciaFns.hookupDataSources = function hookupDataSources() { + // Build the Elasticsearch source objects and run initial population. + $.each(indiciaData.esSourceObjects, function eachSource() { + this.hookup(); + }); + }; + + /** + * Initially populate the data sources. + */ + indiciaFns.populateDataSources = function populateDataSources() { + // Build the Elasticsearch source objects and run initial population. + $.each(indiciaData.esSourceObjects, function eachSource() { + this.populate(); }); }; @@ -147,6 +166,74 @@ throw new Error(msg); }; + /** + * Auto-add keyword suffix for aggregating/sorting on fields with keywords. + * + * Allows the configuration to not care about keyword sub-fields. + */ + indiciaFns.esFieldWithKeywordSuffix = function esFieldWithKeywordSuffix(field) { + var keywordFields = [ + 'event.attributes.id', + 'event.attributes.value', + 'event.habitat', + 'event.recorded_by', + 'event.sampling_protocol', + 'identification.auto_checks.output.message', + 'identification.auto_checks.output.rule_type', + 'identification.identified_by', + 'identification.query', + 'identification.recorder_certainty', + 'identification.verification_decision_source', + 'identification.verifier.name', + 'indexed_location_ids', + 'location.name', + 'location.output_sref', + 'location.output_sref_system', + 'location.parent.name', + 'location.verbatim_locality', + 'message', + 'metadata.group.title', + 'metadata.input_form', + 'metadata.licence_code', + 'metadata.sensitivity_blur', + 'metadata.survey.title', + 'metadata.website.title', + 'occurrence.associated_media', + 'occurrence.attributes.id', + 'occurrence.attributes.value', + 'occurrence.life_stage', + 'occurrence.media.caption', + 'occurrence.media.licence', + 'occurrence.media.path', + 'occurrence.media.path2', + 'occurrence.media.type', + 'occurrence.organism_quantity', + 'occurrence.sex', + 'output_sref', + 'tags', + 'taxon.accepted_name', + 'taxon.accepted_name_authorship', + 'taxon.class', + 'taxon.family', + 'taxon.genus', + 'taxon.group', + 'taxon.kingdom', + 'taxon.order', + 'taxon.phylum', + 'taxon.species', + 'taxon.subfamily', + 'taxon.taxon_name', + 'taxon.taxon_name_authorship', + 'taxon.taxon_rank', + 'taxon.vernacular_name', + 'warehouse' + ]; + if ($.inArray(field, keywordFields) > -1) { + return field + '.keyword'; + } + return field; + }; + /** * Convert an ES (ISO) date to local display format. * @@ -175,6 +262,59 @@ .replace('Y', date.getFullYear()); }; + /** + * Convert an ES media file to thumbnail HTML. + * + * @param integer id + * Document ID. + * @param object file + * Nested file object from ES document. + * @param string sizeClass + * Class to attach to , either single or multi depending on number of + * thumbnails. + */ + indiciaFns.drawMediaFile = function drawMediaFile(id, file, sizeClass) { + // Check if an extenral URL. + var match = file.path.match(/^http(s)?:\/\/(www\.)?([a-z(\.kr)]+)/); + var captionItems = []; + var captionAttr; + var html = ''; + if (file.caption) { + captionItems.push(file.caption); + } + if (file.licence) { + captionItems.push('Licence is ' + file.licence); + } + captionAttr = captionItems.length ? ' title="' + captionItems.join(' | ').replace('"', '"') + '"' : ''; + if (match !== null) { + // If so, is it iNat? We can work out the image file names if so. + if (file.path.match(/^https:\/\/static\.inaturalist\.org/)) { + html += '' + + ''; + } else { + html += ''; + if (captionItems.length) { + html += '

' + captionItems.join(' | ').replace('"', '"') + '

'; + } + } + } else if ($.inArray(file.path.split('.').pop(), ['mp3', 'wav']) > -1) { + // Audio files can have a player control. + html += '