├── .gitignore ├── postinstall.sh ├── public ├── lib │ ├── leaflet.measurescale │ │ ├── L.Control.MeasureScale.css │ │ └── L.Control.MeasureScale.js │ ├── jquery.minicolors │ │ ├── jquery.minicolors.png │ │ └── minicolors.js │ ├── leaflet.setview │ │ ├── L.Control.SetView.css │ │ └── L.Control.SetView.js │ ├── leaflet.sync │ │ └── L.Map.Sync.js │ └── angular-bootstrap │ │ └── js │ │ └── accordion-tpls.js ├── vis.html ├── vislib │ ├── marker_types │ │ ├── legendSlider.html │ │ ├── geohash_grid.js │ │ ├── shaded_circles.js │ │ ├── scaled_circles.js │ │ ├── heatmap.js │ │ └── base_marker.js │ ├── geo_point.js │ ├── markerIcon.js │ ├── __test__ │ │ └── geo_point.js │ ├── sync_maps.js │ ├── LDrawToolbench.js │ └── geoFilter.js ├── tooltip │ ├── visTooltip.html │ ├── searchTooltip.html │ ├── popupVisualize.less │ ├── visTooltip.js │ ├── searchTooltip.js │ └── popupVisualize.js ├── directives │ ├── wmsOverlay.js │ ├── wmsOverlays.html │ ├── savedSearches.html │ ├── wmsOverlays.js │ ├── savedSearches.js │ ├── bands.html │ ├── bands.js │ ├── savedSearch.html │ ├── tooltipFormatter.html │ ├── tooltipFormatter.js │ ├── savedSearch.js │ └── wmsOverlay.html ├── backwardsCompatible.js ├── vis.less ├── vis.js ├── callbacks.js ├── POIs.js ├── utils.js ├── options.html └── visController.js ├── .bowerrc ├── index.js ├── gulpfile.js ├── package.json ├── bower.json ├── developer.md ├── geoserver.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | public/bower_components 3 | node_modules 4 | -------------------------------------------------------------------------------- /postinstall.sh: -------------------------------------------------------------------------------- 1 | echo "removing unwanted dependencies" 2 | rm -rf ./public/bower_components/angular -------------------------------------------------------------------------------- /public/lib/leaflet.measurescale/L.Control.MeasureScale.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-scale { 2 | cursor: pointer; 3 | } -------------------------------------------------------------------------------- /public/vis.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /public/lib/jquery.minicolors/jquery.minicolors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nreese/enhanced_tilemap/HEAD/public/lib/jquery.minicolors/jquery.minicolors.png -------------------------------------------------------------------------------- /public/vislib/marker_types/legendSlider.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 |
-------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components", 3 | "cwd": "./", 4 | "scripts": { 5 | "preinstall": "", 6 | "postinstall": "./postinstall.sh", 7 | "preuninstall": "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/tooltip/visTooltip.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (kibana) { 4 | 5 | return new kibana.Plugin({ 6 | 7 | uiExports: { 8 | visTypes: ['plugins/enhanced_tilemap/vis.js'] 9 | } 10 | 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const mocha = require('gulp-mocha'); 3 | 4 | gulp.task('test', [], function () { 5 | require('babel-register')({ 6 | presets: ['es2015'] 7 | }); 8 | require('jsdom-global')() 9 | return gulp.src([ 10 | 'public/**/__test__/**/*.js' 11 | ], { read: false }) 12 | .pipe(mocha({ reporter: 'list' })); 13 | }); -------------------------------------------------------------------------------- /public/tooltip/searchTooltip.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enhanced_tilemap", 3 | "version": "kibana", 4 | "scripts": { 5 | "test": "gulp test" 6 | }, 7 | "devDependencies": { 8 | "babel-core": "6.22.1", 9 | "babel-preset-es2015": "6.22.0", 10 | "babel-register": "6.22.0", 11 | "chai": "3.5.0", 12 | "gulp": "3.9.1", 13 | "gulp-mocha": "3.0.1", 14 | "jsdom": "9.9.1", 15 | "jsdom-global": "2.1.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/lib/leaflet.setview/L.Control.SetView.css: -------------------------------------------------------------------------------- 1 | .leaflet-draw select { 2 | height: 27px; 3 | padding: 0px 5px 0px 5px; 4 | border: 1px solid #AAA; 5 | border-right-style: hidden; 6 | border-top-color: #dddddd; 7 | border-bottom-color: #dddddd; 8 | -webkit-appearance: none; 9 | -webkit-border-radius: 0px; 10 | } 11 | 12 | .leaflet-draw input { 13 | width:8em; 14 | height: 27px; 15 | padding: 0px 5px 0px 5px; 16 | border: 1px solid #AAA; 17 | border-right-style: hidden; 18 | border-top-color: #dddddd; 19 | border-bottom-color: #dddddd; 20 | text-align: center; 21 | } 22 | 23 | .small-screen { 24 | display: block !important; 25 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enhanced-tilemap", 3 | "description": "Kibana plugin", 4 | "version": "kibana", 5 | "dependencies": { 6 | "formatcoords": "https://github.com/nerik/formatcoords.git#1.1.2", 7 | "mgrs": "https://github.com/proj4js/mgrs.git#1.0.0", 8 | "Leaflet.MousePosition": "https://github.com/nreese/Leaflet.MousePosition.git", 9 | "angularjs-slider": "https://github.com/angular-slider/angularjs-slider.git#6.1.0", 10 | "angularjs-dropdown-multiselect": "https://github.com/dotansimha/angularjs-dropdown-multiselect.git#1.11.8", 11 | "Leaflet.NonTiledLayer": "https://github.com/ptv-logistics/Leaflet.NonTiledLayer.git#f9775506389e461e59d2f15ad2b4786e67eb4ed5" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/directives/wmsOverlay.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const module = require('ui/modules').get('kibana'); 3 | 4 | define(function (require) { 5 | module.directive('wmsOverlay', function (indexPatterns, Private) { 6 | 7 | return { 8 | restrict: 'E', 9 | replace: true, 10 | scope: { 11 | layer: '=' 12 | }, 13 | template: require('./wmsOverlay.html'), 14 | link: function (scope, element, attrs) { 15 | scope.zoomLevels = []; 16 | for (var i=0; i<=18; i++) { 17 | scope.zoomLevels.push(i); 18 | } 19 | indexPatterns.getIds().then(function(list) { 20 | scope.indexPatternList = list; 21 | }); 22 | } 23 | }; 24 | 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /public/vislib/geo_point.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const L = require('leaflet'); 3 | 4 | export const toLatLng = function (geo) { 5 | let lat = 0; 6 | let lon = 0; 7 | if(_.isArray(geo)) { 8 | lat = geo[1]; 9 | lon = geo[0]; 10 | } else if (isString(geo)) { 11 | const split = geo.split(','); 12 | if (split[0] && split[1]) { 13 | lat = split[0]; 14 | lon = split[1]; 15 | } 16 | } else if (_.has(geo, 'lat') && _.has(geo, 'lon')) { 17 | lat = geo.lat; 18 | lon = geo.lon; 19 | } 20 | return L.latLng(lat, lon); 21 | } 22 | 23 | function isString(myVar) { 24 | let isString = false; 25 | if (typeof myVar === 'string' || myVar instanceof String) { 26 | isString = true; 27 | } 28 | return isString; 29 | } -------------------------------------------------------------------------------- /public/directives/wmsOverlays.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 16 | 19 |
-------------------------------------------------------------------------------- /public/directives/savedSearches.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 17 | 20 |
-------------------------------------------------------------------------------- /public/directives/wmsOverlays.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const module = require('ui/modules').get('kibana'); 3 | 4 | define(function (require) { 5 | require('plugins/enhanced_tilemap/directives/wmsOverlay'); 6 | 7 | module.directive('wmsOverlays', function (Private) { 8 | return { 9 | restrict: 'E', 10 | replace: true, 11 | scope: { 12 | layers: '=' 13 | }, 14 | template: require('./wmsOverlays.html'), 15 | link: function (scope, element, attrs) { 16 | scope.addLayer = function() { 17 | if (!scope.layers) scope.layers = []; 18 | scope.layers.push({ 19 | minZoom: 13 20 | }); 21 | } 22 | scope.removeLayer = function(layerIndex) { 23 | scope.layers.splice(layerIndex, 1); 24 | } 25 | } 26 | }; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /public/directives/savedSearches.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const module = require('ui/modules').get('kibana'); 3 | 4 | define(function (require) { 5 | require('plugins/enhanced_tilemap/directives/savedSearch'); 6 | 7 | module.directive('savedSearches', function (Private, indexPatterns) { 8 | return { 9 | restrict: 'E', 10 | replace: true, 11 | scope: { 12 | layers: '=' 13 | }, 14 | template: require('./savedSearches.html'), 15 | link: function (scope, element, attrs) { 16 | scope.addLayer = function() { 17 | if (!scope.layers) scope.layers = []; 18 | scope.layers.push({ 19 | color: '#008000', 20 | popupFields: [], 21 | markerSize: 'm', 22 | syncFilters: true 23 | }); 24 | } 25 | scope.removeLayer = function(layerIndex) { 26 | scope.layers.splice(layerIndex, 1); 27 | } 28 | } 29 | }; 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /public/backwardsCompatible.js: -------------------------------------------------------------------------------- 1 | import _ from'lodash'; 2 | 3 | /** 4 | * As new features are added, sometimes the visualization parameters needs to change 5 | * to accomidate better suited data structures. 6 | * 7 | * The exported object contains a set of functions to migrate existing persisted 8 | * state to the new data structures so users can easily upgraded. 9 | */ 10 | export const backwardsCompatible = { 11 | updateParams: function(params) { 12 | params.overlays.savedSearches.forEach(layerParams => { 13 | this.updateSavedSearch(layerParams); 14 | }); 15 | }, 16 | updateSavedSearch: function(layerParams) { 17 | if (_.has(layerParams, 'labelField')) { 18 | if (layerParams.labelField) { 19 | layerParams.popupFields = [{ 20 | name: layerParams.labelField 21 | }]; 22 | } 23 | delete layerParams.labelField; 24 | } 25 | if (!_.has(layerParams, 'popupFields')) { 26 | layerParams.popupFields = []; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /public/directives/bands.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 11 | to 12 | 22 | 30 | 31 |
32 | 33 |
34 | -------------------------------------------------------------------------------- /public/lib/leaflet.measurescale/L.Control.MeasureScale.js: -------------------------------------------------------------------------------- 1 | L.Control.MeasureScale = L.Control.Scale.extend({ 2 | _addScales: function(options, className, container) { 3 | L.Control.Scale.prototype._addScales.call(this, options, className, container); 4 | 5 | this._container = container; 6 | const self = this; 7 | L.DomEvent.on(this._container, 'click', function(e) { 8 | self.startMeasure(); 9 | }); 10 | }, 11 | onRemove: function (map) { 12 | L.Control.Scale.prototype.onRemove.call(this, map); 13 | L.DomEvent.off(this._container, 'click'); 14 | }, 15 | initMeasure: function() { 16 | const options = { 17 | error: 'Error: shape edges cannot cross!', 18 | tooltip: { 19 | start: 'Click to start drawing line.', 20 | cont: 'Click to continue drawing line.', 21 | end: 'Click last point to finish line.' 22 | } 23 | } 24 | this.polyline = new L.Draw.Polyline(this._map, options); 25 | }, 26 | startMeasure: function() { 27 | if(!this.polyline) this.initMeasure(); 28 | this.polyline.enable(); 29 | } 30 | }); 31 | 32 | L.control.measureScale = function (options) { 33 | return new L.Control.MeasureScale(options); 34 | }; -------------------------------------------------------------------------------- /public/vislib/markerIcon.js: -------------------------------------------------------------------------------- 1 | const L = require('leaflet'); 2 | 3 | export const markerIcon = function (color, size) { 4 | const path = 'M16,1 C7.7146,1 1,7.65636364 1,15.8648485 C1,24.0760606 16,51 16,51 C16,51 31,24.0760606 31,15.8648485 C31,7.65636364 24.2815,1 16,1 L16,1 Z'; 5 | const markerSvg = ''; 6 | const markerUrl = "data:image/svg+xml;base64," + btoa(markerSvg); 7 | let iconSize = [30, 50]; 8 | switch (size) { 9 | case 'xs': 10 | iconSize = [12, 20]; 11 | break; 12 | case 's': 13 | iconSize = [18, 30]; 14 | break; 15 | case 'm': 16 | iconSize = [24, 40]; 17 | break; 18 | case 'l': 19 | iconSize = [30, 50]; 20 | break; 21 | case 'xl': 22 | iconSize = [36, 60]; 23 | break; 24 | } 25 | return L.icon({ 26 | iconUrl: markerUrl, 27 | iconSize: iconSize, 28 | iconAnchor: [iconSize[0]/2, iconSize[1]], 29 | className: "vector-marker", 30 | popupAnchor: [0, -10] 31 | }); 32 | } -------------------------------------------------------------------------------- /public/vislib/__test__/geo_point.js: -------------------------------------------------------------------------------- 1 | const filename = require('path').basename(__filename); 2 | const expect = require('chai').expect; 3 | 4 | const toLatLon = require(`../${filename}`).toLatLng; 5 | 6 | describe(filename, () => { 7 | 8 | describe('toLatLon', () => { 9 | it('is a function', () => { 10 | expect(toLatLon).to.be.a('function'); 11 | }); 12 | 13 | it('converts Elasticsearch geo-point string to L.LatLon', () => { 14 | const latLng = toLatLon("41.12,-71.34"); 15 | expect(latLng.lat).to.equal(41.12); 16 | expect(latLng.lng).to.equal(-71.34); 17 | }); 18 | 19 | it('converts malformed Elasticsearch geo-point string to L.LatLon centered at 0,0', () => { 20 | const latLng = toLatLon(","); 21 | expect(latLng.lat).to.equal(0); 22 | expect(latLng.lng).to.equal(0); 23 | }); 24 | 25 | it('converts Elasticsearch geo-point array to L.LatLon', () => { 26 | const latLng = toLatLon([ -71.34, 41.12 ]); 27 | expect(latLng.lat).to.equal(41.12); 28 | expect(latLng.lng).to.equal(-71.34); 29 | }); 30 | 31 | it('converts Elasticsearch geo-point object to L.LatLon', () => { 32 | const latLng = toLatLon({ 33 | "lat": 41.12, 34 | "lon": -71.34 35 | }); 36 | expect(latLng.lat).to.equal(41.12); 37 | expect(latLng.lng).to.equal(-71.34); 38 | }); 39 | }); 40 | 41 | }); -------------------------------------------------------------------------------- /public/vislib/marker_types/geohash_grid.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return function GeohashGridMarkerFactory(Private) { 3 | let _ = require('lodash'); 4 | let L = require('leaflet'); 5 | 6 | let BaseMarker = Private(require('./base_marker')); 7 | 8 | /** 9 | * Map overlay: rectangles that show the geohash grid bounds 10 | * 11 | * @param map {Leaflet Object} 12 | * @param geoJson {geoJson Object} 13 | * @param params {Object} 14 | */ 15 | _.class(GeohashGridMarker).inherits(BaseMarker); 16 | function GeohashGridMarker(map, geoJson, params) { 17 | let self = this; 18 | GeohashGridMarker.Super.apply(this, arguments); 19 | 20 | // super min and max from all chart data 21 | let min = this.geoJson.properties.allmin; 22 | let max = this.geoJson.properties.allmax; 23 | 24 | this._createMarkerGroup({ 25 | pointToLayer: function (feature, latlng) { 26 | let geohashRect = feature.properties.rectangle; 27 | // get bounds from northEast[3] and southWest[1] 28 | // corners in geohash rectangle 29 | let corners = [ 30 | [geohashRect[3][0], geohashRect[3][1]], 31 | [geohashRect[1][0], geohashRect[1][1]] 32 | ]; 33 | return L.rectangle(corners); 34 | } 35 | }); 36 | } 37 | 38 | return GeohashGridMarker; 39 | }; 40 | }); -------------------------------------------------------------------------------- /public/tooltip/popupVisualize.less: -------------------------------------------------------------------------------- 1 | @import (reference) "~ui/styles/variables"; 2 | 3 | popup-visualize { 4 | display: flex; 5 | flex-direction: column; 6 | height: 100%; 7 | width: 100%; 8 | overflow: auto; 9 | position: relative; 10 | 11 | .k4tip { 12 | white-space: pre-line; 13 | } 14 | 15 | .vis-container { 16 | display: flex; 17 | flex-direction: row; 18 | overflow: auto; 19 | -webkit-transition: opacity 0.01s; 20 | transition: opacity 0.01s; 21 | flex: 1 1 0; 22 | 23 | // IE11 Hack 24 | // 25 | // Normally we would just set flex: 1 1 0%, which works as expected in IE11. 26 | // Unfortunately, a recent bug in Firefox causes this rule to be ignored, so we 27 | // have to use an IE-specific hack instead. 28 | _:-ms-fullscreen, :root & { 29 | flex: 1 0; 30 | } 31 | 32 | &.vis-container--legend-left { 33 | flex-direction: row-reverse; 34 | } 35 | &.vis-container--legend-right { 36 | flex-direction: row; 37 | } 38 | &.vis-container--legend-top { 39 | flex-direction: column-reverse; 40 | } 41 | &.vis-container--legend-bottom { 42 | flex-direction: column; 43 | } 44 | 45 | &.spy-only { 46 | display: none; 47 | } 48 | 49 | } 50 | 51 | .loading { 52 | opacity: @loading-opacity; 53 | } 54 | 55 | .spinner { 56 | position: absolute; 57 | top: 40%; 58 | left: 0; 59 | right: 0; 60 | z-index: 20; 61 | opacity: @loading-opacity; 62 | } 63 | } -------------------------------------------------------------------------------- /public/vislib/sync_maps.js: -------------------------------------------------------------------------------- 1 | require('leaflet'); 2 | require('./../lib/leaflet.sync/L.Map.Sync'); 3 | 4 | var singleton = (function() { 5 | var maps = []; 6 | var sync = true; 7 | var syncOptions = { 8 | syncCursor: false 9 | }; 10 | 11 | function syncMaps(mapA, mapB) { 12 | mapA.sync(mapB, syncOptions); 13 | mapB.sync(mapA, syncOptions); 14 | } 15 | 16 | function unsyncMaps(mapA, mapB) { 17 | mapA.unsync(mapB); 18 | mapB.unsync(mapA); 19 | } 20 | 21 | return { 22 | add: function(newmap) { 23 | if(sync) { 24 | maps.forEach(function(map) { 25 | syncMaps(newmap, map); 26 | }); 27 | } 28 | maps.push(newmap); 29 | }, 30 | remove: function(oldmap) { 31 | maps.forEach(function(map) { 32 | if(oldmap != map) unsyncMaps(oldmap, map); 33 | }); 34 | for(var i=0; i 0) { 10 | const lastBand = scope.bands.slice(-1)[0]; 11 | if(!isNaN(lastBand.high) && !isNaN(lastBand.low)) { 12 | low = lastBand.high; 13 | high = low + (lastBand.high - lastBand.low) 14 | } 15 | } 16 | let colorIndex = scope.bands.length; 17 | if(colorIndex > defaultColors.length-1) colorIndex = defaultColors.length-1; 18 | scope.bands.push({ 19 | low: low, 20 | high: high, 21 | color: defaultColors[colorIndex] 22 | }); 23 | } 24 | 25 | scope.removeBand = function() { 26 | if(scope.bands.length > 0) scope.bands.pop(); 27 | } 28 | 29 | //The end of one band marks the beginning of the next 30 | //Automatically update the next band's low value to reflect the change in the current band. 31 | scope.updateOlderSibling = function(index) { 32 | if(index !== scope.bands.length - 1) { 33 | scope.bands[index + 1].low = scope.bands[index].high; 34 | } 35 | } 36 | } 37 | 38 | return { 39 | restrict: 'E', 40 | scope: { 41 | bands: '=' 42 | }, 43 | template: require('./bands.html'), 44 | link: link 45 | }; 46 | }); -------------------------------------------------------------------------------- /public/directives/savedSearch.html: -------------------------------------------------------------------------------- 1 | 83 | -------------------------------------------------------------------------------- /public/directives/tooltipFormatter.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 11 |
12 |
13 |

Saved search tooltip no longer supported. Kibana 5.5 changes broke this feature

14 | 23 |
24 |
25 | 30 | 34 |
35 |
36 | 43 | 50 |
51 |
52 | 57 |
58 |
59 | -------------------------------------------------------------------------------- /public/vislib/marker_types/shaded_circles.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return function ShadedCircleMarkerFactory(Private) { 3 | let _ = require('lodash'); 4 | let L = require('leaflet'); 5 | 6 | let BaseMarker = Private(require('./base_marker')); 7 | 8 | /** 9 | * Map overlay: circle markers that are shaded to illustrate values 10 | * 11 | * @param map {Leaflet Object} 12 | * @param mapData {geoJson Object} 13 | * @return {Leaflet object} featureLayer 14 | */ 15 | _.class(ShadedCircleMarker).inherits(BaseMarker); 16 | function ShadedCircleMarker(map, geoJson, params) { 17 | let self = this; 18 | ShadedCircleMarker.Super.apply(this, arguments); 19 | 20 | // super min and max from all chart data 21 | let min = this.geoJson.properties.allmin; 22 | let max = this.geoJson.properties.allmax; 23 | 24 | // multiplier to reduce size of all circles 25 | let scaleFactor = 0.8; 26 | 27 | this._createMarkerGroup({ 28 | pointToLayer: function (feature, latlng) { 29 | let radius = self._geohashMinDistance(feature) * scaleFactor; 30 | return L.circle(latlng, radius); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * _geohashMinDistance returns a min distance in meters for sizing 37 | * circle markers to fit within geohash grid rectangle 38 | * 39 | * @method _geohashMinDistance 40 | * @param feature {Object} 41 | * @return {Number} 42 | */ 43 | ShadedCircleMarker.prototype._geohashMinDistance = function (feature) { 44 | let centerPoint = _.get(feature, 'properties.center'); 45 | let geohashRect = _.get(feature, 'properties.rectangle'); 46 | 47 | // centerPoint is an array of [lat, lng] 48 | // geohashRect is the 4 corners of the geoHash rectangle 49 | // an array that starts at the southwest corner and proceeds 50 | // clockwise, each value being an array of [lat, lng] 51 | 52 | // center lat and southeast lng 53 | let east = L.latLng([centerPoint[0], geohashRect[2][1]]); 54 | // southwest lat and center lng 55 | let north = L.latLng([geohashRect[3][0], centerPoint[1]]); 56 | 57 | // get latLng of geohash center point 58 | let center = L.latLng([centerPoint[0], centerPoint[1]]); 59 | 60 | // get smallest radius at center of geohash grid rectangle 61 | let eastRadius = Math.floor(center.distanceTo(east)); 62 | let northRadius = Math.floor(center.distanceTo(north)); 63 | return _.min([eastRadius, northRadius]); 64 | }; 65 | 66 | return ShadedCircleMarker; 67 | }; 68 | }); -------------------------------------------------------------------------------- /public/directives/tooltipFormatter.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; 3 | import { uiModules } from 'ui/modules'; 4 | 5 | const module = uiModules.get('kibana'); 6 | 7 | define(function (require) { 8 | module.directive('tooltipFormatter', function (Private, indexPatterns) { 9 | const visService = Private(SavedObjectRegistryProvider).byLoaderPropertiesName.visualizations; 10 | const searchService = Private(SavedObjectRegistryProvider).byLoaderPropertiesName.searches; 11 | 12 | return { 13 | restrict: 'E', 14 | replace: true, 15 | scope: { 16 | tooltip: '=' 17 | }, 18 | template: require('./tooltipFormatter.html'), 19 | link: function(scope, element, attrs) { 20 | if (!scope.tooltip) { 21 | scope.tooltip = { 22 | closeOnMouseout: true, 23 | type: 'metric', 24 | options: { 25 | 26 | } 27 | } 28 | } 29 | scope.popupDimensionOptions = []; 30 | for (let i=10; i<100; i+=10) { 31 | scope.popupDimensionOptions.push({ 32 | text: i + '%', 33 | value: i / 100 34 | }); 35 | } 36 | fetchVisList(); 37 | fetchSearchList(); 38 | 39 | scope.filterVisList = function() { 40 | scope.tooltip.options.visFilter = this.tooltip.options.visFilter; 41 | fetchVisList(); 42 | } 43 | 44 | scope.filterSearchList = function() { 45 | scope.tooltip.options.searchFilter = this.tooltip.options.searchFilter; 46 | fetchSearchList(); 47 | } 48 | 49 | function fetchSearchList() { 50 | searchService.find(scope.tooltip.options.searchFilter) 51 | .then(hits => { 52 | scope.searchList = _.map(hits.hits, hit => { 53 | return { 54 | label: hit.title, 55 | id: hit.id 56 | }; 57 | }); 58 | }); 59 | } 60 | 61 | function fetchVisList() { 62 | visService.find(scope.tooltip.options.visFilter) 63 | .then(hits => { 64 | scope.visList = _.chain(hits.hits) 65 | .filter(hit => { 66 | const visState = JSON.parse(hit.visState); 67 | return !_.includes(['enhanced_tilemap', 'tilemap', 'timelion'], visState.type); 68 | }) 69 | .map(hit => { 70 | return { 71 | label: hit.title, 72 | id: hit.id 73 | } 74 | }) 75 | .value(); 76 | }); 77 | } 78 | } 79 | } 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /public/tooltip/visTooltip.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import $ from 'jquery'; 3 | import utils from 'plugins/enhanced_tilemap/utils'; 4 | import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; 5 | 6 | define(function (require) { 7 | return function VisTooltipFactory( 8 | $compile, $rootScope, $timeout, 9 | getAppState, Private, savedVisualizations) { 10 | 11 | const geoFilter = Private(require('plugins/enhanced_tilemap/vislib/geoFilter')); 12 | const SearchSource = Private(SearchSourceProvider); 13 | const $state = getAppState(); 14 | const UI_STATE_ID = 'popupVis'; 15 | 16 | class VisTooltip { 17 | constructor(visId, fieldname, geotype, options) { 18 | this.visId = visId; 19 | this.fieldname = fieldname; 20 | this.geotype = geotype; 21 | this.options = options; 22 | this.$tooltipScope = $rootScope.$new(); 23 | this.$visEl = null; 24 | this.parentUiState = $state.makeStateful('uiState'); 25 | } 26 | 27 | destroy() { 28 | this.parentUiState.removeChild(UI_STATE_ID); 29 | this.$tooltipScope.$destroy(); 30 | if (this.$visEl) { 31 | this.$visEl.remove(); 32 | } 33 | } 34 | 35 | getFormatter() { 36 | const linkFn = $compile(require('./visTooltip.html')); 37 | let renderbot = null; 38 | let fetchTimestamp; 39 | 40 | const self = this; 41 | savedVisualizations.get(this.visId).then(function (savedVis) { 42 | self.$tooltipScope.savedObj = savedVis; 43 | const uiState = savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {}; 44 | self.$tooltipScope.uiState = self.parentUiState.createChild(UI_STATE_ID, uiState, true); 45 | self.$visEl = linkFn(self.$tooltipScope); 46 | $timeout(function() { 47 | renderbot = self.$visEl[0].getScope().renderbot; 48 | }); 49 | }); 50 | 51 | function createFilter(rect) { 52 | const bounds = utils.getRectBounds(rect); 53 | return geoFilter.rectFilter(self.fieldname, self.geotype, bounds.top_left, bounds.bottom_right); 54 | } 55 | 56 | return function(feature, map) { 57 | if (!feature) return ''; 58 | if (!self.$visEl) return 'initializing'; 59 | 60 | const width = Math.round(map.getSize().x * _.get(self.options, 'xRatio', 0.6)); 61 | const height = Math.round(map.getSize().y * _.get(self.options, 'yRatio', 0.6)); 62 | const style = 'style="height: ' + height + 'px; width: ' + width + 'px;"'; 63 | const loadHtml = '
Loading Visualization Data
'; 64 | 65 | const localFetchTimestamp = Date.now(); 66 | fetchTimestamp = localFetchTimestamp; 67 | const searchSource = new SearchSource(); 68 | searchSource.inherits(self.$tooltipScope.savedObj.searchSource); 69 | searchSource.filter([createFilter(feature.properties.rectangle)]); 70 | searchSource.fetch().then(esResp => { 71 | self.$visEl.css({ 72 | width: width, 73 | height: height 74 | }); 75 | 76 | const $popup = $(map.getContainer()).find('.leaflet-popup-content'); 77 | 78 | //A lot can happed between calling fetch and getting a response 79 | //Only update popup content if the popup context is still for this fetch 80 | if ($popup 81 | && $popup.html() === loadHtml 82 | && localFetchTimestamp === fetchTimestamp) { 83 | $popup.empty(); 84 | $popup.append(self.$visEl); 85 | renderbot.render(esResp); 86 | } 87 | }); 88 | 89 | return loadHtml; 90 | } 91 | } 92 | } 93 | 94 | return VisTooltip; 95 | }; 96 | }); 97 | -------------------------------------------------------------------------------- /public/vis.less: -------------------------------------------------------------------------------- 1 | .etm-vis { 2 | padding: 1em; 3 | display: flex; 4 | -webkit-box-flex: 1; 5 | flex: 1 1 100%; 6 | -webkit-box-orient: horizontal; 7 | -webkit-box-direction: normal; 8 | flex-direction: row; 9 | min-height: 0; 10 | min-width: 0; 11 | overflow: hidden; 12 | } 13 | 14 | .etm-options-section { 15 | display: block; 16 | border-bottom: 2px solid #eee; 17 | margin-bottom: 3px !important; 18 | } 19 | 20 | .etm-well-section { 21 | display: block; 22 | border-bottom: 2px solid #ccc; 23 | margin-bottom: 3px !important; 24 | } 25 | 26 | .btn-input { 27 | padding: 5px 15px; 28 | font-size: 14px; 29 | color: #2D2D2D; 30 | height: 32px; 31 | width: 100%; 32 | background-color: #ffffff; 33 | border: 1px solid #ecf0f1; 34 | border-radius: 4px; 35 | } 36 | 37 | .btn-input:hover { 38 | color: #2D2D2D; 39 | } 40 | 41 | .side-by-side { 42 | float: left; 43 | width: 50%; 44 | } 45 | 46 | .form-control-inline { 47 | display: inline; 48 | width: auto !important; 49 | } 50 | 51 | ul.etm-options-list { 52 | list-style:none; 53 | padding-left:0; 54 | } 55 | 56 | li.etm-option-item { 57 | background-color: #ecf0f1; 58 | margin: 0px 5px 2px 5px; 59 | padding: 2px; 60 | position: relative; 61 | } 62 | 63 | div.rightCorner { 64 | position: absolute; 65 | right: 0px; 66 | top: 0px; 67 | } 68 | 69 | .etm-vis .leaflet-popup-content-wrapper { 70 | box-shadow: none; 71 | } 72 | 73 | .interactive-popup { 74 | .leaflet-popup { 75 | pointer-events: auto; 76 | } 77 | 78 | .leaflet-popup-content { 79 | pointer-events: auto; 80 | } 81 | } 82 | 83 | .leaflet-spin-control { 84 | margin-right: 14px !important; 85 | } 86 | 87 | .leaflet-spin-control a { 88 | color: #000; 89 | } 90 | 91 | .leaflet-draw-tooltip { 92 | display: inline !important; 93 | } 94 | 95 | .leaflet-draw-toolbar .leaflet-toolbench-tool { 96 | background-position: -100px -100px; 97 | } 98 | 99 | .leaflet-popup-content-wrapper .panel-content { 100 | height:100%; 101 | color: rgb(68, 68, 68); 102 | } 103 | 104 | .leaflet-popup-content-wrapper doc-table.panel-content { 105 | display: block; 106 | color: rgb(206, 206, 206); 107 | } 108 | 109 | .tab-dashboard.theme-dark { 110 | .leaflet-popup-content-wrapper .panel-content { 111 | color: rgb(206, 206, 206); 112 | } 113 | } 114 | 115 | div.leaflet-layer.no-filter > div.leaflet-tile-container > img.leaflet-tile { 116 | filter: none !important; 117 | } 118 | 119 | .vector-marker { 120 | position: absolute; 121 | left: 0; 122 | top: 0; 123 | display: block; 124 | text-align: center; 125 | } 126 | 127 | .bands .minicolors-theme-bootstrap .minicolors-swatch { 128 | left: inherit; 129 | right: 8px; 130 | } 131 | 132 | .bands .minicolors-theme-bootstrap .minicolors-input { 133 | padding-left: 4px; 134 | padding-right: 44px; 135 | } 136 | 137 | .bands .band-add { 138 | margin-top: 5px; 139 | } 140 | 141 | .band { 142 | position: relative; 143 | } 144 | 145 | .band .minicolors { 146 | float: left; 147 | padding-right: 5px; 148 | } 149 | 150 | div.minicolors .minicolors-swatch { 151 | width: 24px; 152 | height: 24px; 153 | } 154 | 155 | .band .band-remove { 156 | position: absolute; 157 | top: 6px; 158 | left: -24px; 159 | } 160 | 161 | .saved-search label { 162 | display: block; 163 | } 164 | 165 | .saved-search .minicolors { 166 | width: 74%; 167 | display: inline-block; 168 | } 169 | 170 | .saved-search .marker-size { 171 | width: 26%; 172 | display: inline; 173 | float: right; 174 | } 175 | 176 | .tilemap-legend { 177 | .rzslider { 178 | margin: 15px 0 5px 0; 179 | } 180 | 181 | .rzslider .rz-bubble { 182 | bottom: 5px; 183 | } 184 | 185 | .rzslider .rz-pointer { 186 | top: -3px; 187 | width: 10px; 188 | height: 10px; 189 | border-radius: 5px; 190 | background-color: #fed976; 191 | } 192 | 193 | .rz-selection { 194 | background: #fed976; 195 | } 196 | 197 | .rzslider .rz-pointer:after { 198 | display: none; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /public/vis.js: -------------------------------------------------------------------------------- 1 | import 'plugins/enhanced_tilemap/lib/angular-bootstrap/js/accordion-tpls.js'; 2 | import 'plugins/enhanced_tilemap/bower_components/angularjs-slider/dist/rzslider.css'; 3 | import 'plugins/enhanced_tilemap/bower_components/angularjs-slider/dist/rzslider.js'; 4 | import 'plugins/enhanced_tilemap/bower_components/angularjs-dropdown-multiselect/dist/angularjs-dropdown-multiselect.min'; 5 | import _ from 'lodash'; 6 | import { supports } from 'ui/utils/supports'; 7 | import { AggResponseGeoJsonProvider } from 'ui/agg_response/geo_json/geo_json'; 8 | import { VisVisTypeProvider } from 'ui/vis/vis_type'; 9 | import { TemplateVisTypeProvider } from 'ui/template_vis_type/template_vis_type'; 10 | import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; 11 | import { VisSchemasProvider } from 'ui/vis/schemas'; 12 | 13 | define(function (require) { 14 | VisTypesRegistryProvider.register(EnhancedTileMapVisProvider); 15 | require('plugins/enhanced_tilemap/vis.less'); 16 | require('plugins/enhanced_tilemap/lib/jquery.minicolors/minicolors'); 17 | require('plugins/enhanced_tilemap/directives/bands'); 18 | require('plugins/enhanced_tilemap/directives/savedSearches'); 19 | require('plugins/enhanced_tilemap/directives/tooltipFormatter'); 20 | require('plugins/enhanced_tilemap/directives/wmsOverlays'); 21 | require('plugins/enhanced_tilemap/tooltip/popupVisualize'); 22 | require('plugins/enhanced_tilemap/tooltip/popupVisualize.less'); 23 | require('plugins/enhanced_tilemap/visController'); 24 | 25 | function EnhancedTileMapVisProvider(Private, getAppState, courier, config) { 26 | const VisType = Private(VisVisTypeProvider); 27 | const TemplateVisType = Private(TemplateVisTypeProvider); 28 | const Schemas = Private(VisSchemasProvider); 29 | const geoJsonConverter = Private(AggResponseGeoJsonProvider); 30 | 31 | return new TemplateVisType({ 32 | name: 'enhanced_tilemap', 33 | title: 'Enhanced Coordinate Map', 34 | icon: 'fa-map-marker', 35 | description: 'Coordinate map plugin that provides better performance, complete geospatial query support, and more features than the built in Coordinate map.', 36 | category: VisType.CATEGORY.MAP, 37 | template: require('plugins/enhanced_tilemap/vis.html'), 38 | params: { 39 | defaults: { 40 | mapType: 'Scaled Circle Markers', 41 | collarScale: 1.5, 42 | scaleType: 'Dynamic - Linear', 43 | scaleBands: [{ 44 | low: 0, 45 | high: 10, 46 | color: "#ffffcc" 47 | }], 48 | scrollWheelZoom: true, 49 | isDesaturated: true, 50 | addTooltip: true, 51 | heatMaxZoom: 16, 52 | heatMinOpacity: 0.1, 53 | heatRadius: 25, 54 | heatBlur: 15, 55 | heatNormalizeData: true, 56 | mapZoom: 2, 57 | mapCenter: [15, 5], 58 | markers: [], 59 | overlays: { 60 | savedSearches: [], 61 | wmsOverlays: [] 62 | }, 63 | wms: config.get('visualization:tileMap:WMSdefaults') 64 | }, 65 | mapTypes: ['Scaled Circle Markers', 'Shaded Circle Markers', 'Shaded Geohash Grid', 'Heatmap'], 66 | scaleTypes: ['Dynamic - Linear', 'Dynamic - Uneven', 'Static'], 67 | canDesaturate: !!supports.cssFilters, 68 | editor: require('plugins/enhanced_tilemap/options.html') 69 | }, 70 | hierarchicalData: function (vis) { 71 | return false; 72 | }, 73 | responseConverter: geoJsonConverter, 74 | schemas: new Schemas([ 75 | { 76 | group: 'metrics', 77 | name: 'metric', 78 | title: 'Value', 79 | min: 1, 80 | max: 1, 81 | aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality'], 82 | defaults: [ 83 | { schema: 'metric', type: 'count' } 84 | ] 85 | }, 86 | { 87 | group: 'buckets', 88 | name: 'segment', 89 | title: 'Geo Coordinates', 90 | aggFilter: 'geohash_grid', 91 | min: 1, 92 | max: 1 93 | } 94 | ]) 95 | }); 96 | } 97 | 98 | return EnhancedTileMapVisProvider; 99 | }); 100 | -------------------------------------------------------------------------------- /public/tooltip/searchTooltip.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import $ from 'jquery'; 3 | import utils from 'plugins/enhanced_tilemap/utils'; 4 | import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; 5 | 6 | define(function (require) { 7 | return function SearchTooltipFactory( 8 | $compile, $rootScope, $timeout, 9 | Private, savedSearches) { 10 | 11 | const geoFilter = Private(require('plugins/enhanced_tilemap/vislib/geoFilter')); 12 | const SearchSource = Private(SearchSourceProvider); 13 | 14 | class SearchTooltip { 15 | constructor(searchId, fieldname, geotype, options) { 16 | this.searchId = searchId; 17 | this.fieldname = fieldname; 18 | this.geotype = geotype; 19 | this.options = options; 20 | this.$tooltipScope = $rootScope.$new(); 21 | this.$visEl = null; 22 | } 23 | 24 | destroy() { 25 | this.$tooltipScope.$destroy(); 26 | if (this.$visEl) { 27 | this.$visEl.remove(); 28 | } 29 | } 30 | 31 | getFormatter() { 32 | const linkFn = $compile(require('./searchTooltip.html')); 33 | let origSearchSource = null; 34 | let fetchTimestamp; 35 | 36 | const self = this; 37 | savedSearches.get(this.searchId).then(function (savedSearch) { 38 | origSearchSource = savedSearch.searchSource; 39 | self.$tooltipScope.searchSource = savedSearch.searchSource; 40 | self.$tooltipScope.columns = savedSearch.columns; 41 | self.$tooltipScope.sort = savedSearch.sort; 42 | self.$tooltipScope.title = savedSearch.title; 43 | self.$tooltipScope.description = savedSearch.description; 44 | self.$tooltipScope.setSortOrder = () => { 45 | console.log('setSortOrder no supported'); 46 | }; 47 | self.$tooltipScope.addColumn = () => { 48 | console.log('addColumn no supported'); 49 | }; 50 | self.$tooltipScope.removeColumn = () => { 51 | console.log('removeColumn no supported'); 52 | }; 53 | self.$tooltipScope.moveColumn = () => { 54 | console.log('moveColumn no supported'); 55 | }; 56 | self.$visEl = linkFn(self.$tooltipScope); 57 | }); 58 | 59 | function createFilter(rect) { 60 | const bounds = utils.getRectBounds(rect); 61 | return geoFilter.rectFilter(self.fieldname, self.geotype, bounds.top_left, bounds.bottom_right); 62 | } 63 | 64 | return function(feature, map) { 65 | if (!feature) return ''; 66 | if (!self.$visEl) return 'initializing'; 67 | 68 | const width = Math.round(map.getSize().x * _.get(self.options, 'xRatio', 0.6)); 69 | const height = Math.round(map.getSize().y * _.get(self.options, 'yRatio', 0.6)); 70 | const style = 'style="height: ' + height + 'px; width: ' + width + 'px;"'; 71 | const loadHtml = '
Loading Data
'; 72 | 73 | const localFetchTimestamp = Date.now(); 74 | fetchTimestamp = localFetchTimestamp; 75 | const searchSource = new SearchSource(); 76 | searchSource.inherits(origSearchSource); 77 | searchSource.filter([createFilter(feature.properties.rectangle)]); 78 | searchSource.fetch().then(esResp => { 79 | self.$visEl.css({ 80 | width: width, 81 | height: height 82 | }); 83 | 84 | const $popup = $(map.getContainer()).find('.leaflet-popup-content'); 85 | 86 | //A lot can happed between calling fetch and getting a response 87 | //Only update popup content if the popup context is still for this fetch 88 | if ($popup 89 | && $popup.html() === loadHtml 90 | && localFetchTimestamp === fetchTimestamp) { 91 | self.$tooltipScope.hits = esResp.hits.hits; 92 | $timeout(function() { 93 | $popup.empty(); 94 | $popup.append(self.$visEl); 95 | }); 96 | } 97 | }); 98 | 99 | return loadHtml; 100 | } 101 | } 102 | } 103 | 104 | return SearchTooltip; 105 | }; 106 | }); 107 | -------------------------------------------------------------------------------- /public/lib/jquery.minicolors/minicolors.js: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2013 Kai Henzler 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | // this software and associated documentation files (the "Software"), to deal in 7 | // the Software without restriction, including without limitation the rights to 8 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | // the Software, and to permit persons to whom the Software is furnished to do so, 10 | // subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | 21 | require('plugins/enhanced_tilemap/lib/jquery.minicolors/jquery.minicolors'); 22 | require('plugins/enhanced_tilemap/lib/jquery.minicolors/jquery.minicolors.css'); 23 | 24 | var module = require('ui/modules').get('kibana/enhanced_tilemap'); 25 | module.provider('minicolors', function() { 26 | this.defaults = { 27 | theme: 'bootstrap', 28 | position: 'top left', 29 | defaultValue: '', 30 | animationSpeed: 50, 31 | animationEasing: 'swing', 32 | change: null, 33 | changeDelay: 0, 34 | control: 'hue', 35 | hide: null, 36 | hideSpeed: 100, 37 | inline: false, 38 | letterCase: 'lowercase', 39 | opacity: false, 40 | show: null, 41 | showSpeed: 100 42 | }; 43 | 44 | this.$get = function() { 45 | return this; 46 | }; 47 | }); 48 | module.directive('minicolors', ["minicolors", "$timeout", function (minicolors, $timeout) { 49 | return { 50 | require: '?ngModel', 51 | restrict: 'A', 52 | priority: 1, //since we bind on an input element, we have to set a higher priority than angular-default input 53 | link: function(scope, element, attrs, ngModel) { 54 | 55 | var inititalized = false; 56 | 57 | //gets the settings object 58 | var getSettings = function() { 59 | var config = angular.extend({}, minicolors.defaults, scope.$eval(attrs.minicolors)); 60 | return config; 61 | }; 62 | 63 | //what to do if the value changed 64 | ngModel.$render = function() { 65 | 66 | //we are in digest or apply, and therefore call a timeout function 67 | $timeout(function() { 68 | var color = ngModel.$viewValue; 69 | element.minicolors('value', color); 70 | }, 0, false); 71 | }; 72 | 73 | //init method 74 | var initMinicolors = function() { 75 | 76 | if (!ngModel) { 77 | return; 78 | } 79 | var settings = getSettings(); 80 | settings.change = function(hex) { 81 | scope.$apply(function() { 82 | ngModel.$setViewValue(hex); 83 | }); 84 | }; 85 | 86 | //destroy the old colorpicker if one already exists 87 | if (element.hasClass('minicolors-input')) { 88 | element.minicolors('destroy'); 89 | } 90 | 91 | // Create the new minicolors widget 92 | element.minicolors(settings); 93 | 94 | // are we inititalized yet ? 95 | //needs to be wrapped in $timeout, to prevent $apply / $digest errors 96 | //$scope.$apply will be called by $timeout, so we don't have to handle that case 97 | if (!inititalized) { 98 | $timeout(function() { 99 | var color = ngModel.$viewValue; 100 | element.minicolors('value', color); 101 | }, 0); 102 | inititalized = true; 103 | return; 104 | } 105 | }; 106 | 107 | initMinicolors(); 108 | //initital call 109 | 110 | // Watch for changes to the directives options and then call init method again 111 | scope.$watch(getSettings, initMinicolors, true); 112 | } 113 | }; 114 | }]); 115 | -------------------------------------------------------------------------------- /public/vislib/marker_types/scaled_circles.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return function ScaledCircleMarkerFactory(Private) { 3 | let _ = require('lodash'); 4 | let L = require('leaflet'); 5 | 6 | let BaseMarker = Private(require('./base_marker')); 7 | 8 | /** 9 | * Map overlay: circle markers that are scaled to illustrate values 10 | * 11 | * @param map {Leaflet Object} 12 | * @param mapData {geoJson Object} 13 | * @param params {Object} 14 | */ 15 | _.class(ScaledCircleMarker).inherits(BaseMarker); 16 | function ScaledCircleMarker(map, geoJson, params) { 17 | let self = this; 18 | ScaledCircleMarker.Super.apply(this, arguments); 19 | 20 | // Earth circumference in meters 21 | const earthCircumference = 40075017; 22 | const mapZoom = map.getZoom(); 23 | const latitudeRadians = map.getCenter().lat * (Math.PI/180); 24 | this._metersPerPixel = earthCircumference * Math.cos(latitudeRadians) / Math.pow(2, mapZoom + 8); 25 | 26 | this._createMarkerGroup({ 27 | pointToLayer: function (feature, latlng) { 28 | let scaledRadius = self._radiusScale(feature); 29 | return L.circleMarker(latlng).setRadius(scaledRadius); 30 | } 31 | }); 32 | } 33 | 34 | /** 35 | * _geohashMinDistance returns a min distance in meters for sizing 36 | * circle markers to fit within geohash grid rectangle 37 | * 38 | * @method _geohashMinDistance 39 | * @param feature {Object} 40 | * @return {Number} 41 | */ 42 | ScaledCircleMarker.prototype._geohashMinDistance = function (feature) { 43 | let centerPoint = _.get(feature, 'properties.center'); 44 | let geohashRect = _.get(feature, 'properties.rectangle'); 45 | 46 | // centerPoint is an array of [lat, lng] 47 | // geohashRect is the 4 corners of the geoHash rectangle 48 | // an array that starts at the southwest corner and proceeds 49 | // clockwise, each value being an array of [lat, lng] 50 | 51 | // center lat and southeast lng 52 | let east = L.latLng([centerPoint[0], geohashRect[2][1]]); 53 | // southwest lat and center lng 54 | let north = L.latLng([geohashRect[3][0], centerPoint[1]]); 55 | 56 | // get latLng of geohash center point 57 | let center = L.latLng([centerPoint[0], centerPoint[1]]); 58 | 59 | // get smallest radius at center of geohash grid rectangle 60 | let eastRadius = Math.floor(center.distanceTo(east)); 61 | let northRadius = Math.floor(center.distanceTo(north)); 62 | return _.min([eastRadius, northRadius]); 63 | }; 64 | 65 | /** 66 | * _radiusScale returns the radius (in pixels) of the feature based on its 67 | * value. The radius fits within the geohash bounds of the feature to 68 | * avoid overlapping. 69 | * 70 | * @method _scaleValueBetween 71 | * @param feature {Object} - The feature 72 | * @return {Number} 73 | */ 74 | ScaledCircleMarker.prototype._radiusScale = function(feature) { 75 | let radius = this._geohashMinDistance(feature); 76 | let orgMin = this.geoJson.properties.allmin; 77 | let orgMax = this.geoJson.properties.allmax; 78 | // Don't let the circle size get any smaller than one-third the max size 79 | let min = orgMax / 3; 80 | let max = orgMax; 81 | let value = this._scaleValueBetween(feature.properties.value, min, max, orgMin, orgMax); 82 | return radius * (value / max) / this._metersPerPixel; 83 | } 84 | 85 | /** 86 | * _scaleValueBetween returns the given value between the new min and max based 87 | * on the original scale 88 | * 89 | * @method _scaleValueBetween 90 | * @param value {Number} - The value to scale 91 | * @param min {Number} - The new minimum 92 | * @param max {Number} - The new maximum 93 | * @param orgMin {Number} - The original minimum 94 | * @param orgMax {Number} - The original maximum 95 | * @return {Number} 96 | */ 97 | ScaledCircleMarker.prototype._scaleValueBetween = function(value, min, max, orgMin, orgMax) { 98 | return (orgMin != orgMax) ? ((max-min)*(value-orgMin))/(orgMax-orgMin) + min : value; 99 | } 100 | 101 | return ScaledCircleMarker; 102 | }; 103 | }); 104 | -------------------------------------------------------------------------------- /public/directives/savedSearch.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | import { backwardsCompatible } from 'plugins/enhanced_tilemap/backwardsCompatible'; 3 | import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; 4 | import { uiModules } from 'ui/modules'; 5 | 6 | const module = uiModules.get('kibana'); 7 | 8 | define(function (require) { 9 | module.directive('savedSearch', function (Private, indexPatterns) { 10 | const service = Private(SavedObjectRegistryProvider).byLoaderPropertiesName.searches; 11 | 12 | return { 13 | restrict: 'E', 14 | replace: true, 15 | scope: { 16 | layer: '=' 17 | }, 18 | template: require('./savedSearch.html'), 19 | link: function (scope, element, attrs) { 20 | backwardsCompatible.updateSavedSearch(scope.layer); 21 | scope.multiSelectSettings = { 22 | buttonClasses: 'btn-input', 23 | displayProp: 'name', 24 | externalIdProp: 'name', 25 | idProp: 'name', 26 | showCheckAll: false, 27 | scrollable: true 28 | }; 29 | 30 | fetchSavedSearches(); 31 | 32 | scope.updateIndex = function() { 33 | scope.warn = ""; 34 | scope.layer.savedSearchId = scope.savedSearch.value; 35 | scope.layer.geoField = null; 36 | scope.layer.popupFields = []; 37 | 38 | refreshIndexFields(scope.savedSearch.indexId, function(geoFields, labelFields) { 39 | scope.geoFields = geoFields; 40 | scope.labelFields = labelFields; 41 | 42 | if (scope.geoFields.length === 0) { 43 | scope.warn = "Unable to use selected saved search, index does not contain any geospatial fields." 44 | } else if (scope.geoFields.length === 1) { 45 | scope.layer.geoField = scope.geoFields[0]; 46 | } 47 | }) 48 | } 49 | 50 | scope.filterSavedSearches = function() { 51 | scope.layer.filter = this.layer.filter; 52 | fetchSavedSearches(); 53 | } 54 | 55 | function fetchSavedSearches() { 56 | //TODO add filter to find to reduce results 57 | service.find(scope.layer.filter) 58 | .then(function (hits) { 59 | scope.items = _.map(hits.hits, function(hit) { 60 | return { 61 | indexId: getIndexId(hit), 62 | label: hit.title, 63 | value: hit.id 64 | }; 65 | }); 66 | 67 | const selected = _.filter(scope.items, function(item) { 68 | if (item.value === scope.layer.savedSearchId) { 69 | return true; 70 | } 71 | }); 72 | if (selected.length > 0) { 73 | scope.savedSearch = selected[0]; 74 | refreshIndexFields(selected[0].indexId, function(geoFields, labelFields) { 75 | scope.geoFields = geoFields; 76 | scope.labelFields = labelFields; 77 | }); 78 | } 79 | }); 80 | } 81 | } 82 | }; 83 | 84 | function refreshIndexFields(indexId, callback) { 85 | indexPatterns.get(indexId).then(function (index) { 86 | const geoFields = index.fields.filter(function (field) { 87 | return field.type === 'geo_point' || field.type === 'geo_shape'; 88 | }).map(function (field) { 89 | return field.name; 90 | }); 91 | 92 | const labelFields = index.fields.filter(function (field) { 93 | let keep = true; 94 | if (field.type === 'boolean' || field.type === 'geo_point' || field.type === 'geo_shape') { 95 | keep = false; 96 | } else if (!field.name || field.name.substring(0,1) === '_') { 97 | keep = false; 98 | } 99 | return keep; 100 | }).sort(function (a, b) { 101 | if(a.name < b.name) return -1; 102 | if(a.name > b.name) return 1; 103 | return 0; 104 | }).map(function (field) { 105 | return { 106 | type: field.type, 107 | name: field.name 108 | }; 109 | }); 110 | 111 | callback(geoFields, labelFields); 112 | }); 113 | } 114 | 115 | function getIndexId(hit) { 116 | const state = JSON.parse(hit.kibanaSavedObjectMeta.searchSourceJSON); 117 | return state.index; 118 | } 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /public/directives/wmsOverlay.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 9 |
10 |
11 | 14 | 17 |
18 |
19 | 22 | 25 |
26 |
27 | 30 | 33 |
34 |
35 | 38 | 40 |
41 |
42 | 45 | 49 |
50 |
51 | 54 | 57 |
58 |
59 | 62 | 65 |
66 |
67 | 76 |
77 |
78 | 81 |
82 |
83 | 86 | 89 |
90 |
91 | 100 |
101 |
102 | 108 | 110 |
111 |
-------------------------------------------------------------------------------- /geoserver.md: -------------------------------------------------------------------------------- 1 | # Geoserver + ElasticGeo + Kibana 2 | 3 | ## Overview 4 | Web Map Service (WMS) is a standard protocol for serving georeferenced map tiles. 5 | Your web browser map requests numerous map tiles from a WMS server and stiches them together to provide a seamless user expericence. 6 | As the map is zoomed and panned, new tiles are requested and displayed. 7 | 8 | [Geoserver](http://geoserver.org/) is an open source server that implements OGC compliant standards such as WMS. 9 | Geoserver provides the tools needed to turn geospatial data into map tiles. 10 | Out of the box, geoserver supports data stores such as PostGis and static files - but not Elasticsearch. 11 | [ElasticGeo](https://github.com/ngageoint/elasticgeo) provides the plumbing needed to hook up geoserver to Elasticsearch. 12 | 13 | A servlet is a Java program that implements the Java Servlet API - a standard for Java classes that respond to requests. 14 | Geoserver is a servlet. 15 | Servlets are deployed in web containers. A web container is an application that manages one to many servlets. 16 | [Apache Tomcat](http://tomcat.apache.org/) is an easy to use web container. 17 | 18 | ### Passing Elasticsearch queries over WMS 19 | ElasticGeo exposes native Elasticsearch query functionality with the WMS parameter [viewparams](https://github.com/ngageoint/elasticgeo/blob/master/gs-web-elasticsearch/doc/index.rst#custom-q-and-f-parameters). 20 | The enhanced tilemap plugin uses this mechanism to pass the identical query Kibana used for aggregations to the WMS server. 21 | 22 | ## Installation 23 | 24 | ### Apache Tomcat 25 | * [Download](http://tomcat.apache.org/download-70.cgi) latest tomcat 7 release. 26 | * Unzip the download and run the script TOMCAT_HOME/bin/start.sh. 27 | * Verify that tomcat is running by opening a browser and viewing http://localhost:8080/ 28 | 29 | ### Geoserver 30 | * [Download](http://geoserver.org/release/stable/) latest geoserver 2.9.x Web Archive. 31 | * Unzip the download and copy the file geoserver.war into the directory TOMCAT_HOME/webapps 32 | * Verify that geoserver is running by opening a browser and viewing http://localhost:8080/geoserver 33 | 34 | ### ElasticGeo 35 | * [Download](https://github.com/ngageoint/elasticgeo/releases) latest ElasticGeo. 36 | * Unzip and copy the files elasticgeoXXX.jar and guava-18.0.jar into the directory TOMCAT_HOME/webapps/geoserver/WEB-INF/lib 37 | * Remove the jar file TOMCAT_HOME/webapps/geoserver/WEB-INF/lib/guava-17.0.jar. 38 | * Restart tomcat. 39 | * Verify that ElasticGeo is properly installed. [Login to geoserver](#login-as-admin). 40 | View the page http://localhost:8080/geoserver/web/wicket/bookmarkable/org.geoserver.web.data.store.NewDataPage and ensure that Elasticsearch is an option under Vector Data Source. 41 | 42 | ## Setting up a WMS layer 43 | Must have elasticsearch 2.2 instance running on the standard ports with an index containing a top level field with either a geo_point or geo_shape type. 44 | 45 | ### Login as admin 46 | username: admin 47 | password: geoserver 48 | 49 | ### Create workspace 50 | A workspace is a namespace. The value will be in the WMS layer URL. Use them to organize your data stores. 51 | * Workspaces -> Add new workspace 52 | * Set Name and Namespace URI to 'elastic', check 'Default Workspace', and click Submit 53 | 54 | ### Create an elasticsearch data store + layer 55 | * Stores -> Add new Store -> Elasticsearch 56 | * Fill in the following fields 57 | ``` 58 | Workspace: elastic 59 | Data Source Name: your_datasource_name //value is just used in geoserver GUIs to identify the data store 60 | elasticsearch_host: localhost 61 | elasticsearch_port: 9300 62 | index_name: name_of_your_index 63 | cluster_name: name_of_your_cluster 64 | ``` 65 | * Click Save 66 | * The screen 'New Layer' will appear. Click the publish link in the table. 67 | * The window 'Elasticsearch fields configuration' will be displayed. You should see your data source mappings and your geospatial index. Click Apply. 68 | * The screen 'Edit Layer' will appear. Fill out the 'Bounding Boxes' section. Just use -90 and 90 for lat constraints and -180 and 180 for lon constraints. Click Save. 69 | * Test out the layer. Go to Layer Preview and select OpenLayers in your layer's row. 70 | 71 | ## View WMS layer in kibana - with kibana filters 72 | Must have kibana instance with enhanced tilemap plugin installed 73 | 74 | ### Create enhanced tilemap visualization 75 | 76 | ### Add WMS overlay 77 | * Under options, check 'WMS Overlays' 78 | * Set WMS URL to http://localhost:8080/geoserver/your-workspace-name/wms 79 | * In geoserver, go to the Layers page. The WMS Layer value will be the name column for your layer row. Set WMS Layers to this value 80 | * Check 'Sync kibana filters' 81 | * Click 'Apply changes' 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /public/vislib/LDrawToolbench.js: -------------------------------------------------------------------------------- 1 | //Adds additional tools to the standard leafelt.draw toolbar 2 | define(function (require) { 3 | function LDrawToolbench(map, drawControl) { 4 | this._map = map; 5 | 6 | const container = drawControl.getContainer().firstChild; 7 | this._actionsContainer = container.getElementsByClassName('leaflet-draw-actions')[0]; 8 | this._toolbarContainer = container.getElementsByClassName('leaflet-draw-toolbar')[0]; 9 | } 10 | 11 | LDrawToolbench.prototype.addTool = function () { 12 | const self = this; 13 | _createButton({ 14 | title: "Create geo_distance filter around POIs", 15 | className: 'fa fa-bullseye leaflet-toolbench-tool', 16 | container: this._toolbarContainer, 17 | callback: function() { 18 | self._cancelOldActions(); 19 | self._displayActions(); 20 | }, 21 | context: {} 22 | }); 23 | } 24 | 25 | LDrawToolbench.prototype.removeTools = function () { 26 | const tools = this._toolbarContainer.getElementsByClassName('leaflet-toolbench-tool'); 27 | for (var i = 0; i < tools.length; i++) { 28 | const elem = tools[i]; 29 | elem.parentNode.removeChild(elem); 30 | } 31 | } 32 | 33 | LDrawToolbench.prototype._clearActions = function () { 34 | while (this._actionsContainer.firstChild) { 35 | this._actionsContainer.removeChild(this._actionsContainer.firstChild); 36 | } 37 | } 38 | 39 | /** 40 | * In the event that a users selects a toolbench tool prior to Canceling a Draw tool, 41 | * this function guarantees that the cancel action gets triggered to allow 42 | * Leaflet.Draw to clean up the UI. 43 | * 44 | * @method cancelOldActions 45 | */ 46 | LDrawToolbench.prototype._cancelOldActions = function () { 47 | for (var i=0; i 0); 81 | }, 82 | 83 | // overload methods on originalMap to replay interactions on _syncMaps; 84 | _initSync: function () { 85 | if (this._syncMaps) { 86 | return; 87 | } 88 | var originalMap = this; 89 | 90 | this._syncMaps = []; 91 | this._cursors = []; 92 | 93 | L.extend(originalMap, { 94 | setView: function (center, zoom, options, sync) { 95 | if (!sync) { 96 | originalMap._syncMaps.forEach(function (toSync) { 97 | toSync.setView(center, zoom, options, true); 98 | }); 99 | } 100 | return L.Map.prototype.setView.call(this, center, zoom, options); 101 | }, 102 | 103 | panBy: function (offset, options, sync) { 104 | if (!sync) { 105 | originalMap._syncMaps.forEach(function (toSync) { 106 | toSync.panBy(offset, options, true); 107 | }); 108 | } 109 | return L.Map.prototype.panBy.call(this, offset, options); 110 | }, 111 | 112 | _onResize: function (event, sync) { 113 | if (!sync) { 114 | originalMap._syncMaps.forEach(function (toSync) { 115 | toSync._onResize(event, true); 116 | }); 117 | } 118 | return L.Map.prototype._onResize.call(this, event); 119 | } 120 | }); 121 | 122 | originalMap.on('zoomend', function () { 123 | originalMap._syncMaps.forEach(function (toSync) { 124 | toSync.setView(originalMap.getCenter(), originalMap.getZoom(), NO_ANIMATION); 125 | }); 126 | }, this); 127 | 128 | originalMap.dragging._draggable._updatePosition = function () { 129 | L.Draggable.prototype._updatePosition.call(this); 130 | var self = this; 131 | originalMap._syncMaps.forEach(function (toSync) { 132 | L.DomUtil.setPosition(toSync.dragging._draggable._element, self._newPos); 133 | toSync.eachLayer(function (layer) { 134 | if (layer._google !== undefined) { 135 | layer._google.setCenter(originalMap.getCenter()); 136 | } 137 | }); 138 | toSync.fire('moveend'); 139 | }); 140 | }; 141 | } 142 | }); 143 | })(); 144 | -------------------------------------------------------------------------------- /public/callbacks.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return function CallbacksFactory(Private, courier, config) { 3 | const _ = require('lodash'); 4 | const geoFilter = Private(require('plugins/enhanced_tilemap/vislib/geoFilter')); 5 | const utils = require('plugins/enhanced_tilemap/utils'); 6 | 7 | return { 8 | createMarker: function (event) { 9 | const agg = _.get(event, 'chart.geohashGridAgg'); 10 | if (!agg) return; 11 | const editableVis = agg.vis.getEditableVis(); 12 | if (!editableVis) return; 13 | const newPoint = [_.round(event.latlng.lat, 5), _.round(event.latlng.lng, 5)]; 14 | editableVis.params.markers.push(newPoint); 15 | }, 16 | deleteMarkers: function (event) { 17 | const agg = _.get(event, 'chart.geohashGridAgg'); 18 | if (!agg) return; 19 | const editableVis = agg.vis.getEditableVis(); 20 | if (!editableVis) return; 21 | 22 | event.deletedLayers.eachLayer(function (layer) { 23 | editableVis.params.markers = editableVis.params.markers.filter(function(point) { 24 | if(point[0] === layer._latlng.lat && point[1] === layer._latlng.lng) { 25 | return false; 26 | } else { 27 | return true; 28 | } 29 | }); 30 | }); 31 | }, 32 | mapMoveEnd: function (event) { 33 | const vis = _.get(event, 'chart.geohashGridAgg.vis'); 34 | if (vis && vis.hasUiState()) { 35 | vis.getUiState().set('mapCenter', [ 36 | _.round(event.center.lat, 5), 37 | _.round(event.center.lng, 5) 38 | ]); 39 | vis.getUiState().set('mapZoom', event.zoom); 40 | } 41 | 42 | //Fetch new data if map bounds are outsize of collar 43 | const bounds = utils.scaleBounds(event.mapBounds, 1); 44 | if(_.has(event, 'collar.top_left') && !utils.contains(event.collar, bounds)) { 45 | courier.fetch(); 46 | } 47 | }, 48 | mapZoomEnd: function (event) { 49 | const vis = _.get(event, 'chart.geohashGridAgg.vis'); 50 | if (vis && vis.hasUiState()) { 51 | vis.getUiState().set('mapZoom', event.zoom); 52 | } 53 | 54 | const autoPrecision = _.get(event, 'chart.geohashGridAgg.params.autoPrecision'); 55 | if (autoPrecision) { 56 | courier.fetch(); 57 | } 58 | }, 59 | poiFilter: function (event) { 60 | const agg = _.get(event, 'chart.geohashGridAgg'); 61 | if (!agg) return; 62 | 63 | const field = agg.fieldName(); 64 | const indexPatternName = agg.vis.indexPattern.id; 65 | 66 | const filters = []; 67 | event.poiLayers.forEach(function (poiLayer) { 68 | poiLayer.getLayers().forEach(function (feature) { 69 | if (feature instanceof L.Marker) { 70 | const filter = {geo_distance: {distance: event.radius + "km"}}; 71 | filter.geo_distance[field] = { 72 | "lat" : feature.getLatLng().lat, 73 | "lon" : feature.getLatLng().lng 74 | } 75 | filters.push(filter); 76 | } 77 | }); 78 | }); 79 | geoFilter.add(filters, field, indexPatternName); 80 | }, 81 | polygon: function (event) { 82 | const agg = _.get(event, 'chart.geohashGridAgg'); 83 | if (!agg) return; 84 | const indexPatternName = agg.vis.indexPattern.id; 85 | 86 | let newFilter; 87 | let field; 88 | if (event.params.filterByShape && event.params.shapeField) { 89 | const firstPoint = event.points[0]; 90 | const closed = event.points; 91 | closed.push(firstPoint); 92 | field = event.params.shapeField; 93 | newFilter = {geo_shape: {}}; 94 | newFilter.geo_shape[field] = { 95 | shape: { 96 | type: 'Polygon', 97 | coordinates: [ closed ] 98 | } 99 | }; 100 | } else { 101 | field = agg.fieldName(); 102 | newFilter = {geo_polygon: {}}; 103 | newFilter.geo_polygon[field] = { points: event.points}; 104 | } 105 | 106 | geoFilter.add(newFilter, field, indexPatternName); 107 | }, 108 | rectangle: function (event) { 109 | const agg = _.get(event, 'chart.geohashGridAgg'); 110 | if (!agg) return; 111 | const indexPatternName = agg.vis.indexPattern.id; 112 | 113 | let field = agg.fieldName(); 114 | let geotype = 'geo_point'; 115 | if (event.params.filterByShape && event.params.shapeField) { 116 | field = event.params.shapeField; 117 | geotype = 'geo_shape'; 118 | } 119 | const newFilter = geoFilter.rectFilter( 120 | field, geotype, event.bounds.top_left, event.bounds.bottom_right); 121 | 122 | geoFilter.add(newFilter, field, indexPatternName); 123 | }, 124 | circle: function (event) { 125 | const agg = _.get(event, 'chart.geohashGridAgg'); 126 | if (!agg) return; 127 | const indexPatternName = agg.vis.indexPattern.id; 128 | const center = [event.e.layer._latlng.lat, event.e.layer._latlng.lng]; 129 | const radius = event.e.layer._mRadius; 130 | let field = agg.fieldName(); 131 | if (event.params.filterByShape && event.params.shapeField) { 132 | field = event.params.shapeField; 133 | } 134 | 135 | const newFilter = geoFilter.circleFilter( 136 | field, center[0], center[1], radius); 137 | 138 | geoFilter.add(newFilter, field, indexPatternName); 139 | } 140 | } 141 | } 142 | }); -------------------------------------------------------------------------------- /public/tooltip/popupVisualize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy of src/ui/public/visualize/visualize.js that exposes scope thru DOM 3 | */ 4 | import 'ui/visualize/spy'; 5 | import 'ui/visualize/visualize.less'; 6 | import 'ui/visualize/visualize_legend'; 7 | import $ from 'jquery'; 8 | import _ from 'lodash'; 9 | import { uiModules } from 'ui/modules'; 10 | import visualizeTemplate from 'ui/visualize/visualize.html'; 11 | import 'angular-sanitize'; 12 | 13 | import { 14 | isTermSizeZeroError, 15 | } from 'ui/elasticsearch_errors'; 16 | 17 | uiModules 18 | .get('kibana/directive', ['ngSanitize']) 19 | .directive('popupVisualize', function (Notifier, SavedVis, indexPatterns, Private, config, $timeout) { 20 | return { 21 | restrict: 'E', 22 | require: '?renderCounter', 23 | scope : { 24 | showSpyPanel: '=?', 25 | vis: '=', 26 | uiState: '=?', 27 | searchSource: '=?', 28 | editableVis: '=?', 29 | esResp: '=?', 30 | }, 31 | template: visualizeTemplate, 32 | link: function ($scope, $el, attr, renderCounter) { 33 | $el[0].getScope = function() { 34 | return $scope; 35 | }; 36 | 37 | const minVisChartHeight = 180; 38 | 39 | if (_.isUndefined($scope.showSpyPanel)) { 40 | $scope.showSpyPanel = true; 41 | } 42 | 43 | function getter(selector) { 44 | return function () { 45 | let $sel = $el.find(selector); 46 | if ($sel.size()) return $sel; 47 | }; 48 | } 49 | 50 | let getVisEl = getter('.visualize-chart'); 51 | let getVisContainer = getter('.vis-container'); 52 | let getSpyContainer = getter('.visualize-spy-container'); 53 | 54 | // Show no results message when isZeroHits is true and it requires search 55 | $scope.showNoResultsMessage = function () { 56 | let requiresSearch = _.get($scope, 'vis.type.requiresSearch'); 57 | let isZeroHits = _.get($scope,'esResp.hits.total') === 0; 58 | let shouldShowMessage = !_.get($scope, 'vis.params.handleNoResults'); 59 | 60 | return Boolean(requiresSearch && isZeroHits && shouldShowMessage); 61 | }; 62 | 63 | const legendPositionToVisContainerClassMap = { 64 | top: 'vis-container--legend-top', 65 | bottom: 'vis-container--legend-bottom', 66 | left: 'vis-container--legend-left', 67 | right: 'vis-container--legend-right', 68 | }; 69 | 70 | $scope.getVisContainerClasses = function () { 71 | return legendPositionToVisContainerClassMap[$scope.vis.params.legendPosition]; 72 | }; 73 | 74 | if (renderCounter && !$scope.vis.implementsRenderComplete()) { 75 | renderCounter.disable(); 76 | } 77 | 78 | $scope.spy = {}; 79 | $scope.spy.mode = ($scope.uiState) ? $scope.uiState.get('spy.mode', {}) : {}; 80 | 81 | let applyClassNames = function () { 82 | const $visEl = getVisContainer(); 83 | const $spyEl = getSpyContainer(); 84 | if (!$spyEl) return; 85 | 86 | let fullSpy = ($scope.spy.mode && ($scope.spy.mode.fill || $scope.fullScreenSpy)); 87 | 88 | $visEl.toggleClass('spy-only', Boolean(fullSpy)); 89 | $spyEl.toggleClass('only', Boolean(fullSpy)); 90 | 91 | $timeout(function () { 92 | if (shouldHaveFullSpy()) { 93 | $visEl.addClass('spy-only'); 94 | $spyEl.addClass('only'); 95 | }; 96 | }, 0); 97 | }; 98 | 99 | // we need to wait for some watchers to fire at least once 100 | // before we are "ready", this manages that 101 | let prereq = (function () { 102 | let fns = []; 103 | 104 | return function register(fn) { 105 | fns.push(fn); 106 | 107 | return function () { 108 | fn.apply(this, arguments); 109 | 110 | if (fns.length) { 111 | _.pull(fns, fn); 112 | if (!fns.length) { 113 | $scope.$root.$broadcast('ready:vis'); 114 | } 115 | } 116 | }; 117 | }; 118 | }()); 119 | 120 | let loadingDelay = config.get('visualization:loadingDelay'); 121 | $scope.loadingStyle = { 122 | '-webkit-transition-delay': loadingDelay, 123 | 'transition-delay': loadingDelay 124 | }; 125 | 126 | function shouldHaveFullSpy() { 127 | let $visEl = getVisEl(); 128 | if (!$visEl) return; 129 | 130 | return ($visEl.height() < minVisChartHeight) 131 | && _.get($scope.spy, 'mode.fill') 132 | && _.get($scope.spy, 'mode.name'); 133 | } 134 | 135 | // spy watchers 136 | $scope.$watch('fullScreenSpy', applyClassNames); 137 | 138 | $scope.$watchCollection('spy.mode', function () { 139 | $scope.fullScreenSpy = shouldHaveFullSpy(); 140 | applyClassNames(); 141 | }); 142 | 143 | $scope.$watch('vis', prereq(function (vis, oldVis) { 144 | let $visEl = getVisEl(); 145 | if (!$visEl) return; 146 | 147 | if (!attr.editableVis) { 148 | $scope.editableVis = vis; 149 | } 150 | 151 | if (oldVis) $scope.renderbot = null; 152 | if (vis) { 153 | $scope.renderbot = vis.type.createRenderbot(vis, $visEl, $scope.uiState); 154 | } 155 | })); 156 | 157 | $scope.$watchCollection('vis.params', prereq(function () { 158 | if ($scope.renderbot) $scope.renderbot.updateParams(); 159 | })); 160 | 161 | $scope.$watch('esResp', prereq(function (resp, prevResp) { 162 | if (!resp) return; 163 | $scope.renderbot.render(resp); 164 | })); 165 | 166 | $scope.$watch('renderbot', function (newRenderbot, oldRenderbot) { 167 | if (oldRenderbot && newRenderbot !== oldRenderbot) { 168 | oldRenderbot.destroy(); 169 | } 170 | }); 171 | 172 | $scope.$on('$destroy', function () { 173 | if ($scope.renderbot) { 174 | $el.off('renderComplete'); 175 | $scope.renderbot.destroy(); 176 | } 177 | }); 178 | } 179 | }; 180 | }); 181 | -------------------------------------------------------------------------------- /public/POIs.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const L = require('leaflet'); 3 | import { markerIcon } from 'plugins/enhanced_tilemap/vislib/markerIcon'; 4 | import { toLatLng } from 'plugins/enhanced_tilemap/vislib/geo_point'; 5 | import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; 6 | import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; 7 | import utils from 'plugins/enhanced_tilemap/utils'; 8 | 9 | define(function (require) { 10 | return function POIsFactory(Private, savedSearches) { 11 | 12 | const SearchSource = Private(SearchSourceProvider); 13 | const queryFilter = Private(FilterBarQueryFilterProvider); 14 | 15 | /** 16 | * Points of Interest 17 | * 18 | * Turns saved search results into easily consumible data for leaflet. 19 | */ 20 | function POIs(params) { 21 | this.savedSearchId = params.savedSearchId; 22 | this.geoField = params.geoField; 23 | //remain backwards compatible 24 | if (!params.geoField && params.geoPointField) { 25 | this.geoField = params.geoPointField 26 | } 27 | this.popupFields = _.get(params, 'popupFields', []).map(function(obj) { 28 | return obj.name; 29 | }); 30 | this.limit = _.get(params, 'limit', 100); 31 | this.syncFilters = _.get(params, 'syncFilters', false); 32 | } 33 | 34 | /** 35 | * @param {options} options: styling options 36 | * @param {Function} callback(layer) 37 | layer {ILayer}: Leaflet ILayer containing the results of the saved search 38 | */ 39 | POIs.prototype.getLayer = function (options, callback) { 40 | const self = this; 41 | savedSearches.get(this.savedSearchId).then(savedSearch => { 42 | const geoType = savedSearch.searchSource._state.index.fields.byName[self.geoField].type; 43 | const searchSource = new SearchSource(); 44 | if (this.syncFilters) { 45 | searchSource.inherits(savedSearch.searchSource); 46 | searchSource.filter(queryFilter.getFilters()); 47 | } else { 48 | //Do not filter POIs by time so can not inherit from rootSearchSource 49 | searchSource.inherits(false); 50 | searchSource.index(savedSearch.searchSource._state.index); 51 | searchSource.query(savedSearch.searchSource.get('query')); 52 | searchSource.filter(savedSearch.searchSource.get('filter')); 53 | } 54 | searchSource.size(this.limit); 55 | searchSource.source({ 56 | includes: _.compact(_.flatten([this.geoField, this.popupFields])), 57 | excludes: [] 58 | }); 59 | searchSource.fetch() 60 | .then(searchResp => { 61 | callback(self._createLayer(searchResp.hits.hits, geoType, options)); 62 | }); 63 | }); 64 | }; 65 | 66 | POIs.prototype._createLayer = function (hits, geoType, options) { 67 | let layer = null; 68 | if ('geo_point' === geoType) { 69 | const markers = _.map(hits, hit => { 70 | return this._createMarker(hit, options); 71 | }); 72 | layer = new L.FeatureGroup(markers); 73 | } else if ('geo_shape' === geoType) { 74 | const shapes = _.map(hits, hit => { 75 | const geometry = _.get(hit, `_source[${this.geoField}]`); 76 | geometry.type = capitalizeFirstLetter(geometry.type); 77 | let popupContent = false; 78 | if (this.popupFields.length > 0) { 79 | popupContent = this._popupContent(hit); 80 | } 81 | return { 82 | type: 'Feature', 83 | properties: { 84 | label: popupContent 85 | }, 86 | geometry: geometry 87 | } 88 | }); 89 | layer = L.geoJson( 90 | shapes, 91 | { 92 | onEachFeature: function (feature, thisLayer) { 93 | if (feature.properties.label) { 94 | thisLayer.bindPopup(feature.properties.label); 95 | thisLayer.on('mouseover', function(e) { 96 | this.openPopup(); 97 | }); 98 | thisLayer.on('mouseout', function(e) { 99 | this.closePopup(); 100 | }); 101 | } 102 | 103 | if (_.get(feature, 'geometry.type') === 'Polygon') { 104 | thisLayer.on('click', function(e) { 105 | thisLayer._map.fire('etm:select-feature', { 106 | geojson: thisLayer.toGeoJSON() 107 | }); 108 | }); 109 | } 110 | }, 111 | pointToLayer: function (feature, latlng) { 112 | return L.circleMarker( 113 | latlng, 114 | { 115 | radius: 6 116 | }); 117 | }, 118 | style: { 119 | color: options.color, 120 | weight: 1.5, 121 | opacity: 0.65 122 | } 123 | }); 124 | } else { 125 | console.warn('Unexpected feature geo type: ' + geoType); 126 | } 127 | return layer; 128 | }; 129 | 130 | POIs.prototype._createMarker = function (hit, options) { 131 | const feature = L.marker( 132 | toLatLng(_.get(hit, `_source[${this.geoField}]`)), 133 | { 134 | icon: markerIcon(options.color, options.size) 135 | }); 136 | if (this.popupFields.length > 0) { 137 | const content = this._popupContent(hit); 138 | feature.on('mouseover', function(e) { 139 | const popup = L.popup({ 140 | autoPan: false, 141 | maxHeight: 'auto', 142 | maxWidth: 'auto', 143 | offset: utils.popupOffset(this._map, content, e.latlng) 144 | }) 145 | .setLatLng(e.latlng) 146 | .setContent(content) 147 | .openOn(this._map); 148 | }); 149 | feature.on('mouseout', function(e) { 150 | this._map.closePopup(); 151 | }); 152 | } 153 | return feature; 154 | }; 155 | 156 | POIs.prototype._popupContent = function (hit) { 157 | let dlContent = ''; 158 | this.popupFields.forEach(function(field) { 159 | dlContent += `
${field}
${hit._source[field]}
` 160 | }); 161 | return `
${dlContent}
`; 162 | } 163 | 164 | function capitalizeFirstLetter(string) { 165 | return string.charAt(0).toUpperCase() + string.slice(1); 166 | } 167 | 168 | return POIs; 169 | } 170 | }); 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # enhanced_tilemap 2 | Kibana ships with a functional tilemap visualization. This plugin provides an additional tilemap visualization containing the enhancments listed below. 3 | 4 | ## Better Performance 5 | 6 | #### Geohash aggregation filtered by geo_bounding_box collar 7 | Resolves issue [Filter geohash_grid aggregation to map view box with collar](https://github.com/elastic/kibana/issues/8087) 8 | 9 | #### Load geohash grids without blocking user interface 10 | The existing tilemap loads all of the geohash grids at a single time. This can result in adding hundreds or even thousands of DOM elements at a single time. The browser is locked up while this process occurs. 11 | 12 | The enhanced tilemap plugin phases-in geohash grids, loading 100 every 200 milliseconds, so that the browser never locks up. A control with a spinning icon is added to the map while grids are being phased-in. The control is removed once all grids are processed. 13 | 14 | ## Richer Popups 15 | Show more than just the aggregation metric when mousing over a geohash grid marker. The enhanced tilemap allows for the display of any kibana visualization or saved search inside the popup. The visualization or saved search are filtered by the geogrid coordinates - showing just the results for the moused-over cell. 16 | ![alt text](https://github.com/nreese/enhanced_tilemap/blob/gh-pages/images/popup.gif) 17 | 18 | ## Contextual Overlays 19 | 20 | #### Overlay Saved Search results as map markers 21 | 22 | #### WMS Overlay 23 | Add a WMS overlay to the tilemap. 24 | 25 | View aggregated results and document features in the same map. 26 | When **Sync kibana filters** is checked, kibana filters are sent to the WMS server resulting in tiles that reflect the time range, search query, and filters of your kibana application. 27 | Requires WMS to be served from an elasticsearch Store. 28 | Follow the [geoserver guide](geoserver.md) for instructions on setting up a WMS layer pulling data from your elasticsearch cluster. 29 | 30 | ## Complete Geospatial Query Support 31 | ![alt text](https://github.com/nreese/enhanced_tilemap/blob/gh-pages/images/geo_query.gif) 32 | #### Geo polygon query support 33 | Click the polygon icon and draw a polygon on the map. The enhanced tilemap plugin will create a geo_polygon filter. 34 | 35 | #### geo_shape datatype query support 36 | The geohash_grid aggregation only supports the geo_point datatype. 37 | Geospatial queries created by the existing tilemap plugin can only be applied to the geo_point field selected for the aggregation. When your data represents large geospatial shapes, this limitation can provide misleading results as documents that intersect the query my be omitted if their point representation is not accurately reflected in a geo_point field. 38 | 39 | While the enhanced tilemap plugin cannot provide geohash_grid aggregation support for the geo_shape datatype, it does provide the ability to create geospatial queries on a geo_shape datatype. That way, queries accurately represent the results for geospatial shape intersection. **Note:** The index still requires a geo_point field for the aggregation (storing the center of the shape as a geo_point field works well). 40 | 41 | #### OR geospatial queries 42 | Kibana's tilemap visualization has a neat feature where you can draw a rectangle on the map and create a geo_bounding_box filter. The limitation arises when multiple bounding boxes are needed. Each drawn rectangle creates a new geo_bounding_box filter that are ANDed together resulting in "No results found" messages across all visualizations. 43 | 44 | The enhanced tilemap visualization allows for the creation of multiple geospatial filters that will be ORed together. Each drawn rectangle or polygon will append a geo filter to an ORed array. 45 | 46 | ## And More 47 | 48 | ![alt text](https://github.com/nreese/enhanced_tilemap/blob/gh-pages/images/andMore.gif) 49 | 50 | * Set view Leaflet control. 51 | * mouse latitude and longitude display control. Click display to toggle decimal degrees, degrees minutes seconds, and MGRS. 52 | * Map scale control. Click for measurement tool. 53 | * Scroll map on mouse zoom. Feature can be turned off in options. 54 | * Slider control in legend to hide aggregation grids outside of selected range. 55 | 56 | #### Static quantized range bands 57 | The existing tilemap generates quantized range bands dynamically. The enhanced_tilemap provides the ability to set static quantized range bands. 58 | 59 | #### Sync maps 60 | Sync map movements when dashboard contains multiple map visualizations. Map syncing implemented with [Leaflet.Sync](https://github.com/turban/Leaflet.Sync) 61 | 62 | **Performance tip** Store enhanced_tilemaps belonging to the same dashboard at identical zoom levels. When enhanced_tilemaps are stored with different zoom levels, the browser will have to make 2 requests to elasticsearch for data. The first will get all data at different zoom levels. Then the next, will fetch all data at identical zoom levels. The second request can be avoided if all maps are stored at identical zoom levels. Check the map zoom level by clicking the set view control (eye icon) in the upper left corner of the map display. 63 | 64 | # Install 65 | ## Kibana 5.x 66 | ```bash 67 | ./bin/kibana-plugin install https://github.com/nreese/enhanced_tilemap/releases/download/v2017-04-19/enhanced-tilemap-v2017-04-19-5.0.2.zip 68 | ``` 69 | 70 | ```bash 71 | ./bin/kibana-plugin install https://github.com/nreese/enhanced_tilemap/releases/download/v2017-04-19/enhanced-tilemap-v2017-04-19-5.1.2.zip 72 | ``` 73 | 74 | ```bash 75 | ./bin/kibana-plugin install https://github.com/nreese/enhanced_tilemap/releases/download/v2017-04-19/enhanced-tilemap-v2017-04-19-5.2.2.zip 76 | ``` 77 | 78 | ```bash 79 | ./bin/kibana-plugin install https://github.com/nreese/enhanced_tilemap/releases/download/v2017-04-19/enhanced-tilemap-v2017-04-19-5.3.0.zip 80 | ``` 81 | 82 | ## Kibana 4.x 83 | ```bash 84 | ./bin/kibana plugin -i enhanced_tilemap -u https://github.com/nreese/enhanced_tilemap/archive/4.x.zip 85 | ``` 86 | 87 | # Uninstall 88 | ## Kibana 5.x 89 | ```bash 90 | ./bin/kibana-plugin remove enhanced_tilemap 91 | ``` 92 | 93 | # Development 94 | 95 | ## Build plugin 96 | * clone git repo in `kibana_home/plugins` 97 | * `cd kibana_home/plugins/enhanced_tilemap` 98 | * `bower install` 99 | 100 | ## Run unit tests 101 | Use npm@2 (as root `npm install -g 'npm@<3'`). npm@3 installs dependencies as [maximally flat](https://github.com/npm/npm/issues/9809). As a result, `kibana_home/plugins/enhanced_tilemap/node_modules` contains the folder lodash. During the kibana build process, require.js finds this version of lodash instead of the version under `kibana_home/node_moduldes`. If you don't want to install npm@3, then manually delete the folder `kibana_home/plugins/enhanced_tilemap/node_modules` before kibana builds `kibana_home/optimze/bundles/kibana.bundle.js`. 102 | 103 | * clone git repo in `kibana_home/plugins` 104 | * `cd kibana_home/plugins/enhanced_tilemap` 105 | * `npm install` 106 | * `npm test` 107 | -------------------------------------------------------------------------------- /public/vislib/marker_types/heatmap.js: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return function HeatmapMarkerFactory(Private) { 3 | let d3 = require('d3'); 4 | let _ = require('lodash'); 5 | let L = require('leaflet'); 6 | 7 | let BaseMarker = Private(require('./base_marker')); 8 | 9 | /** 10 | * Map overlay: canvas layer with leaflet.heat plugin 11 | * 12 | * @param map {Leaflet Object} 13 | * @param geoJson {geoJson Object} 14 | * @param params {Object} 15 | */ 16 | _.class(HeatmapMarker).inherits(BaseMarker); 17 | function HeatmapMarker(map, geoJson, params) { 18 | let self = this; 19 | this._disableTooltips = false; 20 | HeatmapMarker.Super.apply(this, arguments); 21 | 22 | this._createMarkerGroup({ 23 | radius: +this._attr.heatRadius, 24 | blur: +this._attr.heatBlur, 25 | maxZoom: +this._attr.heatMaxZoom, 26 | minOpacity: +this._attr.heatMinOpacity 27 | }); 28 | } 29 | 30 | /** 31 | * Does nothing, heatmaps don't have a legend 32 | * 33 | * @method addLegend 34 | * @return {undefined} 35 | */ 36 | HeatmapMarker.prototype.addLegend = _.noop; 37 | 38 | HeatmapMarker.prototype._createMarkerGroup = function (options) { 39 | let max = _.get(this.geoJson, 'properties.allmax'); 40 | let points = this._dataToHeatArray(max); 41 | 42 | this._markerGroup = L.heatLayer(points, options); 43 | this._fixTooltips(); 44 | this._addToMap(); 45 | }; 46 | 47 | HeatmapMarker.prototype._fixTooltips = function () { 48 | let self = this; 49 | let debouncedMouseMoveLocation = _.debounce(mouseMoveLocation.bind(this), 15, { 50 | 'leading': true, 51 | 'trailing': false 52 | }); 53 | 54 | if (!this._disableTooltips && this._attr.addTooltip) { 55 | this.map.on('mousemove', debouncedMouseMoveLocation); 56 | this.map.on('mouseout', function () { 57 | self.map.closePopup(); 58 | }); 59 | this.map.on('mousedown', function () { 60 | self._disableTooltips = true; 61 | self.map.closePopup(); 62 | }); 63 | this.map.on('mouseup', function () { 64 | self._disableTooltips = false; 65 | }); 66 | } 67 | 68 | function mouseMoveLocation(e) { 69 | let latlng = e.latlng; 70 | 71 | this.map.closePopup(); 72 | 73 | // unhighlight all svgs 74 | d3.selectAll('path.geohash', this.chartEl).classed('geohash-hover', false); 75 | 76 | if (!this.geoJson.features.length || this._disableTooltips) { 77 | return; 78 | } 79 | 80 | // find nearest feature to event latlng 81 | let feature = this._nearestFeature(latlng); 82 | 83 | // show tooltip if close enough to event latlng 84 | if (this._tooltipProximity(latlng, feature)) { 85 | this._showTooltip(feature, latlng); 86 | } 87 | } 88 | }; 89 | 90 | /** 91 | * returns a memoized Leaflet latLng for given geoJson feature 92 | * 93 | * @method addLatLng 94 | * @param feature {geoJson Object} 95 | * @return {Leaflet latLng Object} 96 | */ 97 | HeatmapMarker.prototype._getLatLng = _.memoize(function (feature) { 98 | return L.latLng( 99 | feature.geometry.coordinates[1], 100 | feature.geometry.coordinates[0] 101 | ); 102 | }, function (feature) { 103 | // turn coords into a string for the memoize cache 104 | return [feature.geometry.coordinates[1], feature.geometry.coordinates[0]].join(','); 105 | }); 106 | 107 | /** 108 | * Finds nearest feature in mapData to event latlng 109 | * 110 | * @method _nearestFeature 111 | * @param latLng {Leaflet latLng} 112 | * @return nearestPoint {Leaflet latLng} 113 | */ 114 | HeatmapMarker.prototype._nearestFeature = function (latLng) { 115 | let self = this; 116 | let nearest; 117 | 118 | if (latLng.lng < -180 || latLng.lng > 180) { 119 | return; 120 | } 121 | 122 | _.reduce(this.geoJson.features, function (distance, feature) { 123 | let featureLatLng = self._getLatLng(feature); 124 | let dist = latLng.distanceTo(featureLatLng); 125 | 126 | if (dist < distance) { 127 | nearest = feature; 128 | return dist; 129 | } 130 | 131 | return distance; 132 | }, Infinity); 133 | 134 | return nearest; 135 | }; 136 | 137 | /** 138 | * display tooltip if feature is close enough to event latlng 139 | * 140 | * @method _tooltipProximity 141 | * @param latlng {Leaflet latLng Object} 142 | * @param feature {geoJson Object} 143 | * @return {Boolean} 144 | */ 145 | HeatmapMarker.prototype._tooltipProximity = function (latlng, feature) { 146 | if (!feature) return; 147 | 148 | let showTip = false; 149 | let featureLatLng = this._getLatLng(feature); 150 | 151 | // zoomScale takes map zoom and returns proximity value for tooltip display 152 | // domain (input values) is map zoom (min 1 and max 18) 153 | // range (output values) is distance in meters 154 | // used to compare proximity of event latlng to feature latlng 155 | let zoomScale = d3.scale.linear() 156 | .domain([1, 4, 7, 10, 13, 16, 18]) 157 | .range([1000000, 300000, 100000, 15000, 2000, 150, 50]); 158 | 159 | let proximity = zoomScale(this.map.getZoom()); 160 | let distance = latlng.distanceTo(featureLatLng); 161 | 162 | // maxLngDif is max difference in longitudes 163 | // to prevent feature tooltip from appearing 360° 164 | // away from event latlng 165 | let maxLngDif = 40; 166 | let lngDif = Math.abs(latlng.lng - featureLatLng.lng); 167 | 168 | if (distance < proximity && lngDif < maxLngDif) { 169 | showTip = true; 170 | } 171 | 172 | let testScale = d3.scale.pow().exponent(0.2) 173 | .domain([1, 18]) 174 | .range([1500000, 50]); 175 | return showTip; 176 | }; 177 | 178 | 179 | /** 180 | * returns data for data for heat map intensity 181 | * if heatNormalizeData attribute is checked/true 182 | • normalizes data for heat map intensity 183 | * 184 | * @method _dataToHeatArray 185 | * @param max {Number} 186 | * @return {Array} 187 | */ 188 | HeatmapMarker.prototype._dataToHeatArray = function (max) { 189 | let self = this; 190 | let mapData = this.geoJson; 191 | 192 | return this.geoJson.features.map(function (feature) { 193 | let lat = feature.geometry.coordinates[1]; 194 | let lng = feature.geometry.coordinates[0]; 195 | let heatIntensity; 196 | 197 | if (!self._attr.heatNormalizeData) { 198 | // show bucket value on heatmap 199 | heatIntensity = feature.properties.value; 200 | } else { 201 | // show bucket value normalized to max value 202 | heatIntensity = feature.properties.value / max; 203 | } 204 | 205 | return [lat, lng, heatIntensity]; 206 | }); 207 | }; 208 | 209 | return HeatmapMarker; 210 | }; 211 | }); 212 | -------------------------------------------------------------------------------- /public/utils.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | define(function (require) { 4 | 5 | return { 6 | getGeoExtents: function(visData) { 7 | return { 8 | min: visData.geoJson.properties.min, 9 | max: visData.geoJson.properties.max 10 | } 11 | }, 12 | /* 13 | * @param bounds {LatLngBounds} 14 | * @param scale {number} 15 | * @return {object} 16 | */ 17 | scaleBounds: function(bounds, scale) { 18 | let safeScale = scale; 19 | if(safeScale < 1) scale = 1; 20 | if(safeScale > 5) scale = 5; 21 | safeScale = safeScale - 1; 22 | 23 | const topLeft = bounds.getNorthWest(); 24 | const bottomRight = bounds.getSouthEast(); 25 | let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); 26 | let lonDiff = _.round(Math.abs(bottomRight.lng - topLeft.lng), 5); 27 | //map height can be zero when vis is first created 28 | if(latDiff === 0) latDiff = lonDiff; 29 | 30 | const latDelta = latDiff * safeScale; 31 | let topLeftLat = _.round(topLeft.lat, 5) + latDelta; 32 | if(topLeftLat > 90) topLeftLat = 90; 33 | let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; 34 | if(bottomRightLat < -90) bottomRightLat = -90; 35 | const lonDelta = lonDiff * safeScale; 36 | let topLeftLon = _.round(topLeft.lng, 5) - lonDelta; 37 | if(topLeftLon < -180) topLeftLon = -180; 38 | let bottomRightLon = _.round(bottomRight.lng, 5) + lonDelta; 39 | if(bottomRightLon > 180) bottomRightLon = 180; 40 | 41 | //console.log("scale:" + safeScale + ", latDelta: " + latDelta + ", lonDelta: " + lonDelta); 42 | //console.log("top left lat " + _.round(topLeft.lat, 5) + " -> " + topLeftLat); 43 | //console.log("bottom right lat " + _.round(bottomRight.lat, 5) + " -> " + bottomRightLat); 44 | //console.log("top left lon " + _.round(topLeft.lng, 5) + " -> " + topLeftLon); 45 | //console.log("bottom right lon " + _.round(bottomRight.lng, 5) + " -> " + bottomRightLon); 46 | 47 | return { 48 | "top_left": {lat: topLeftLat, lon: topLeftLon}, 49 | "bottom_right": {lat: bottomRightLat, lon: bottomRightLon} 50 | }; 51 | }, 52 | contains: function(collar, bounds) { 53 | //test if bounds top_left is inside collar 54 | if(bounds.top_left.lat > collar.top_left.lat 55 | || bounds.top_left.lon < collar.top_left.lon) 56 | return false; 57 | 58 | //test if bounds bottom_right is inside collar 59 | if(bounds.bottom_right.lat < collar.bottom_right.lat 60 | || bounds.bottom_right.lon > collar.bottom_right.lon) 61 | return false; 62 | 63 | //both corners are inside collar so collar contains 64 | return true; 65 | }, 66 | getAggConfig: function (aggs, aggName) { 67 | let aggConfig = null; 68 | index = _.findIndex(aggs, function (agg) { 69 | return agg.schema.name === aggName; 70 | }); 71 | if (index !== -1) { 72 | aggConfig = aggs[index]; 73 | } 74 | return aggConfig; 75 | }, 76 | /* 77 | * @param rect {Array of Array(lat, lon)} grid rectangle 78 | * created from KIBANA_HOME/src/ui/public/agg_response/geo_json/rows_to_features.js 79 | * @return {object} 80 | */ 81 | getRectBounds: function(rect) { 82 | const RECT_LAT_INDEX = 0; 83 | const RECT_LON_INDEX = 1; 84 | let latMin = 90; 85 | let latMax = -90; 86 | let lonMin = 180; 87 | let lonMax = -180; 88 | rect.forEach(function(vertex) { 89 | if (vertex[RECT_LAT_INDEX] < latMin) latMin = vertex[RECT_LAT_INDEX]; 90 | if (vertex[RECT_LAT_INDEX] > latMax) latMax = vertex[RECT_LAT_INDEX]; 91 | if (vertex[RECT_LON_INDEX] < lonMin) lonMin = vertex[RECT_LON_INDEX]; 92 | if (vertex[RECT_LON_INDEX] > lonMax) lonMax = vertex[RECT_LON_INDEX]; 93 | }); 94 | return { 95 | top_left: { 96 | lat: latMax, 97 | lon: lonMin 98 | }, 99 | bottom_right: { 100 | lat: latMin, 101 | lon: lonMax 102 | } 103 | }; 104 | }, 105 | getMapStateFromVis: function(vis) { 106 | const mapState = {}; 107 | //Visualizations created in 5.x will have map state in uiState 108 | if (vis.hasUiState()) { 109 | const uiStateCenter = vis.uiStateVal('mapCenter'); 110 | const uiStateZoom = vis.uiStateVal('mapZoom'); 111 | if(uiStateCenter && uiStateZoom) { 112 | mapState.center = uiStateCenter; 113 | mapState.zoom = uiStateZoom; 114 | } 115 | } 116 | //Visualizations created in 4.x will have map state in segment aggregation 117 | if (!_.has(mapState, 'center') && !_.has(mapState, 'zoom')) { 118 | const agg = this.getAggConfig(vis.aggs, 'segment'); 119 | if (agg) { 120 | mapState.center = _.get(agg, 'params.mapCenter'); 121 | mapState.zoom = _.get(agg, 'params.mapZoom'); 122 | } 123 | } 124 | //Provide defaults if no state found 125 | if (!_.has(mapState, 'center') && !_.has(mapState, 'zoom')) { 126 | mapState.center = [15, 5]; 127 | mapState.zoom = 2; 128 | } 129 | return mapState; 130 | }, 131 | /** 132 | * Avoid map auto panning. Use the offset option to 133 | * anchor popups so content fits inside map bounds. 134 | * 135 | * @method popupOffset 136 | * @param map {L.Map} Leaflet map 137 | * @param content {String} String containing html popup content 138 | * @param latLng {L.LatLng} popup location 139 | * @return {L.Point} offset 140 | */ 141 | popupOffset: function(map, content, latLng) { 142 | const mapWidth = map.getSize().x; 143 | const mapHeight = map.getSize().y; 144 | const popupPoint = map.latLngToContainerPoint(latLng); 145 | //Create popup that is out of view to determine dimensions 146 | const popup = L.popup({ 147 | autoPan: false, 148 | maxHeight: 'auto', 149 | maxWidth: 'auto', 150 | offset: new L.Point(mapWidth * -2, mapHeight * -2) 151 | }) 152 | .setLatLng(latLng) 153 | .setContent(content) 154 | .openOn(map); 155 | const popupHeight = popup._contentNode.clientHeight; 156 | const popupWidth = popup._contentNode.clientWidth / 2; 157 | 158 | let widthOffset = 0; 159 | const distToLeftEdge = popupPoint.x; 160 | const distToRightEdge = mapWidth - popupPoint.x; 161 | if (distToLeftEdge < popupWidth) { 162 | //Move popup right as little as possible 163 | widthOffset = popupWidth - distToLeftEdge; 164 | } else if (distToRightEdge < popupWidth) { 165 | //Move popup left as little as possible 166 | widthOffset = -1 * (popupWidth - distToRightEdge); 167 | } 168 | 169 | let heightOffset = 6; //leaflet default 170 | const distToTopEdge = popupPoint.y; 171 | if (distToTopEdge < popupHeight) { 172 | //Move popup down as little as possible 173 | heightOffset = popupHeight - distToTopEdge + 16; 174 | } 175 | 176 | return new L.Point(widthOffset, heightOffset); 177 | } 178 | } 179 | }); -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 8 | 11 |
12 |
13 | 14 | 21 |
22 | 23 |
24 |
25 | 30 | 37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 | 48 |
49 | 59 |
60 | {{vis.params.heatRadius}} 61 |
62 |
63 |
64 |
65 | 69 |
70 | 80 |
81 | {{vis.params.heatBlur}} 82 |
83 |
84 |
85 |
86 | 90 |
91 | 101 |
102 | {{vis.params.heatMaxZoom}} 103 |
104 |
105 |
106 |
107 | 111 |
112 | 122 |
123 | {{vis.params.heatMinOpacity}} 124 |
125 |
126 |
127 |
128 | 132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 | 148 | 153 |
154 |
155 | 156 | 157 |
158 |
159 | 168 |
169 |
170 | 180 |
181 |
182 | 191 |
192 |
193 |
194 |

195 | WMS maps are 3rd party mapping services that have not been verified to work with Kibana. 196 | These should be considered expert settings. 197 |

198 | 201 | 204 |
205 |
206 | 209 | 213 |
214 |
215 | 218 | 221 |
222 |
223 | 226 | 229 |
230 |
231 | 234 | 237 |
238 |
239 | 242 | 245 |
246 |

* if this parameter is incorrect, maps will fail to load.

247 |
248 |
249 | 250 | 251 |
252 | 253 | 254 | 255 | 256 | 257 |
258 |
-------------------------------------------------------------------------------- /public/vislib/geoFilter.js: -------------------------------------------------------------------------------- 1 | import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; 2 | 3 | define(function (require) { 4 | const LAT_INDEX = 1; 5 | const LON_INDEX = 0; 6 | 7 | return function GeoFilterFactory(Private, confirmModal) { 8 | const _ = require('lodash'); 9 | const queryFilter = Private(FilterBarQueryFilterProvider); 10 | 11 | function filterAlias(field, numBoxes) { 12 | return field + ": " + numBoxes + " geo filters" 13 | } 14 | 15 | function _applyFilter(newFilter, field, indexPatternName) { 16 | let numFilters = 1; 17 | if (_.isArray(newFilter)) { 18 | numFilters = newFilter.length; 19 | newFilter = { 20 | bool: { 21 | should: newFilter 22 | } 23 | }; 24 | } 25 | newFilter.meta = { 26 | alias: filterAlias(field, numFilters), 27 | negate: false, 28 | index: indexPatternName, 29 | key: field 30 | }; 31 | queryFilter.addFilters(newFilter); 32 | } 33 | 34 | function _combineFilters(newFilter, existingFilter, field) { 35 | let geoFilters = _.flatten([newFilter]); 36 | let type = ''; 37 | if (_.has(existingFilter, 'bool.should')) { 38 | geoFilters = geoFilters.concat(existingFilter.bool.should); 39 | type = 'bool'; 40 | } else if (_.has(existingFilter, 'geo_bounding_box')) { 41 | geoFilters.push({ geo_bounding_box: existingFilter.geo_bounding_box }); 42 | type = 'geo_bounding_box'; 43 | } else if (_.has(existingFilter, 'geo_polygon')) { 44 | geoFilters.push({ geo_polygon: existingFilter.geo_polygon }); 45 | type = 'geo_polygon'; 46 | } else if (_.has(existingFilter, 'geo_shape')) { 47 | geoFilters.push({ geo_shape: existingFilter.geo_shape }); 48 | type = 'geo_shape'; 49 | } else if (_.has(existingFilter, 'geo_distance')) { 50 | geoFilters.push({ geo_distance: existingFilter.geo_distance }); 51 | type = 'geo_distance'; 52 | } 53 | 54 | // Update method removed - so just remove old filter and add updated filter 55 | const updatedFilter = { 56 | bool: { 57 | should: geoFilters 58 | }, 59 | meta: existingFilter.meta 60 | }; 61 | updatedFilter.meta.alias = filterAlias(field, geoFilters.length); 62 | queryFilter.removeFilter(existingFilter); 63 | queryFilter.addFilters([updatedFilter]); 64 | } 65 | 66 | function _overwriteFilters(newFilter, existingFilter, field, indexPatternName) { 67 | if (existingFilter) { 68 | queryFilter.removeFilter(existingFilter); 69 | } 70 | 71 | _applyFilter(newFilter, field, indexPatternName); 72 | } 73 | 74 | function addGeoFilter(newFilter, field, indexPatternName) { 75 | let existingFilter = null; 76 | _.flatten([queryFilter.getAppFilters(), queryFilter.getGlobalFilters()]).forEach(function (it) { 77 | if (isGeoFilter(it, field)) { 78 | existingFilter = it; 79 | } 80 | }); 81 | 82 | if (existingFilter) { 83 | const confirmModalOptions = { 84 | confirmButtonText: "Combine with existing filters", 85 | cancelButtonText: "Overwrite existing filter", 86 | onCancel: () => { 87 | _overwriteFilters(newFilter, existingFilter, field, indexPatternName); 88 | }, 89 | onConfirm: () => { 90 | _combineFilters(newFilter, existingFilter, field); 91 | } 92 | }; 93 | 94 | confirmModal("How would you like this filter applied?", confirmModalOptions); 95 | } else { 96 | _applyFilter(newFilter, field, indexPatternName); 97 | } 98 | } 99 | 100 | /** 101 | * Convert elasticsearch geospatial filter to leaflet vectors 102 | * 103 | * @method toVector 104 | * @param filter {Object} elasticsearch geospatial filter 105 | * @param field {String} Index field name for geo_point or geo_shape field 106 | * @return {Array} Array of Leaftet Vector Layers constructed from filter geometries 107 | */ 108 | function toVector(filter, field) { 109 | let features = []; 110 | if (_.has(filter, ['bool', 'should'])) { 111 | _.get(filter, ['bool', 'should'], []).forEach(function(it) { 112 | features = features.concat(toVector(it, field)); 113 | }); 114 | } else if (_.has(filter, ['geo_bounding_box', field])) { 115 | const topLeft = _.get(filter, ['geo_bounding_box', field, 'top_left']); 116 | const bottomRight = _.get(filter, ['geo_bounding_box', field, 'bottom_right']); 117 | if(topLeft && bottomRight) { 118 | const bounds = L.latLngBounds( 119 | [topLeft.lat, topLeft.lon], 120 | [bottomRight.lat, bottomRight.lon]); 121 | features.push(L.rectangle(bounds)); 122 | } 123 | } else if (_.has(filter, ['geo_distance', field])) { 124 | let distance_str = _.get(filter, ['geo_distance', 'distance']); 125 | let distance = 1000; 126 | if (_.includes(distance_str, 'km')) { 127 | distance = parseFloat(distance_str.replace('km', '')) * 1000; 128 | } 129 | const center = _.get(filter, ['geo_distance', field]); 130 | if(center) { 131 | features.push(L.circle([center.lat, center.lon], distance)); 132 | } 133 | } else if (_.has(filter, ['geo_polygon', field])) { 134 | const points = _.get(filter, ['geo_polygon', field, 'points'], []); 135 | const latLngs = []; 136 | points.forEach(function(point) { 137 | const lat = point[LAT_INDEX]; 138 | const lon = point[LON_INDEX]; 139 | latLngs.push(L.latLng(lat, lon)); 140 | }); 141 | if(latLngs.length > 0) 142 | features.push(L.polygon(latLngs)); 143 | } else if (_.has(filter, ['geo_shape', field])) { 144 | const type = _.get(filter, ['geo_shape', field, 'shape', 'type']); 145 | if (type.toLowerCase() === 'envelope') { 146 | const envelope = _.get(filter, ['geo_shape', field, 'shape', 'coordinates']); 147 | const tl = envelope[0]; //topleft 148 | const br = envelope[1]; //bottomright 149 | const bounds = L.latLngBounds( 150 | [tl[LAT_INDEX], tl[LON_INDEX]], 151 | [br[LAT_INDEX], br[LON_INDEX]]); 152 | features.push(L.rectangle(bounds)); 153 | } else if (type.toLowerCase() === 'polygon') { 154 | coords = _.get(filter, ['geo_shape', field, 'shape', 'coordinates'])[0]; 155 | const latLngs = []; 156 | coords.forEach(function(point) { 157 | const lat = point[LAT_INDEX]; 158 | const lon = point[LON_INDEX]; 159 | latLngs.push(L.latLng(lat, lon)); 160 | }); 161 | features.push(L.polygon(latLngs)); 162 | } else { 163 | console.log("Unexpected geo_shape type: " + type); 164 | } 165 | } 166 | return features; 167 | } 168 | 169 | function getGeoFilters(field) { 170 | let filters = []; 171 | _.flatten([queryFilter.getAppFilters(), queryFilter.getGlobalFilters()]).forEach(function (it) { 172 | if (isGeoFilter(it, field) && !_.get(it, 'meta.disabled', false)) { 173 | const features = toVector(it, field); 174 | filters = filters.concat(features); 175 | } 176 | }); 177 | return filters; 178 | } 179 | 180 | function getGeoSpatialModel(filter) { 181 | let geoSpatialModel = null; 182 | if (_.has(filter, 'bool.should')) { 183 | geoSpatialModel = { bool: filter.bool }; 184 | } else if (_.has(filter, 'geo_bounding_box')) { 185 | geoSpatialModel = { geo_bounding_box: filter.geo_bounding_box }; 186 | } else if (_.has(filter, 'geo_polygon')) { 187 | geoSpatialModel = { geo_polygon: filter.geo_polygon }; 188 | } else if (_.has(filter, 'geo_shape')) { 189 | geoSpatialModel = { geo_shape: filter.geo_shape }; 190 | } 191 | 192 | return geoSpatialModel; 193 | } 194 | 195 | function isGeoFilter(filter, field) { 196 | if (filter.meta.key === field 197 | || _.has(filter, ['geo_bounding_box', field]) 198 | || _.has(filter, ['geo_distance', field]) 199 | || _.has(filter, ['geo_polygon', field]) 200 | || _.has(filter, ['geo_shape', field])) { 201 | return true; 202 | } else if (_.has(filter, ['bool', 'should'])) { 203 | let model = getGeoSpatialModel(filter); 204 | let found = false; 205 | for (let i = 0; i < model.bool.should.length; i++) { 206 | if (_.has(model.bool.should[i], ['geo_bounding_box', field]) 207 | || _.has(model.bool.should[i], ['geo_distance', field]) 208 | || _.has(model.bool.should[i], ['geo_polygon', field]) 209 | || _.has(model.bool.should[i], ['geo_shape', field])) { 210 | found = true; 211 | break; 212 | } 213 | } 214 | return found; 215 | } else { 216 | return false; 217 | } 218 | } 219 | 220 | /** 221 | * Create elasticsearch geospatial rectangle filter 222 | * 223 | * @method rectFilter 224 | * @param fieldname {String} name of geospatial field in IndexPattern 225 | * @param geotype {String} geospatial datatype of field, geo_point or geo_shape 226 | * @param top_left {Object} top left lat and lon (decimal degrees) 227 | * @param bottom_right {Object} bottom right at and lon (decimal degrees) 228 | * @return {Object} elasticsearch geospatial rectangle filter 229 | */ 230 | function rectFilter(fieldname, geotype, top_left, bottom_right) { 231 | let geofilter = null; 232 | if ('geo_point' === geotype) { 233 | geofilter = {geo_bounding_box: {}}; 234 | geofilter.geo_bounding_box[fieldname] = { 235 | top_left: top_left, 236 | bottom_right: bottom_right 237 | }; 238 | } else if ('geo_shape' === geotype) { 239 | geofilter = {geo_shape: {}}; 240 | geofilter.geo_shape[fieldname] = { 241 | shape: { 242 | type: 'envelope', 243 | coordinates: [ 244 | [top_left.lon, top_left.lat], 245 | [bottom_right.lon, bottom_right.lat] 246 | ] 247 | } 248 | }; 249 | } else { 250 | console.warn('unexpected geotype: ' + geotype); 251 | } 252 | return geofilter; 253 | } 254 | 255 | /** 256 | * Create elasticsearch geospatial geo_distance filter 257 | * 258 | * @method circleFilter 259 | * @param fieldname {String} name of geospatial field in IndexPattern 260 | * @param lat {Object} latitude of center point for circle (decimal degrees) 261 | * @param lon {Object} longitude of center point for circle (decimal degrees) 262 | * @param radius {Object} radius 263 | * @return {Object} elasticsearch geospatial geo_distance filter 264 | */ 265 | function circleFilter(fieldname, lat, lon, radius) { 266 | let geofilter = null; 267 | geofilter = { geo_distance: { 268 | distance: radius 269 | }}; 270 | geofilter.geo_distance[fieldname] = { 271 | lat: lat, 272 | lon: lon 273 | }; 274 | return geofilter; 275 | } 276 | 277 | return { 278 | add: addGeoFilter, 279 | isGeoFilter: isGeoFilter, 280 | getGeoFilters: getGeoFilters, 281 | rectFilter: rectFilter, 282 | circleFilter: circleFilter 283 | } 284 | } 285 | }); 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /public/lib/leaflet.setview/L.Control.SetView.js: -------------------------------------------------------------------------------- 1 | L.Control.SetView = L.Control.extend({ 2 | options: { 3 | position: 'topleft' 4 | }, 5 | initialize: function (options) { 6 | this._toolbar = new L.SetViewToolbar(options); 7 | }, 8 | onAdd: function (map) { 9 | var container = L.DomUtil.create('div', 'leaflet-draw'); 10 | container.appendChild(this._toolbar.addToolbar(map)); 11 | return container; 12 | }, 13 | onRemove: function (map) { 14 | this._toolbar.removeToolbar(); 15 | } 16 | }); 17 | 18 | L.SetViewToolbar = L.Class.extend({ 19 | initialize: function (options) { 20 | this._decimalDegrees = true; 21 | }, 22 | addToolbar: function (map) { 23 | var container = L.DomUtil.create('div', 'leaflet-draw-section'); 24 | this._toolbarContainer = L.DomUtil.create('div', 'leaflet-bar'); 25 | this._actionsContainer = L.DomUtil.create('ul', 'leaflet-draw-actions'); 26 | container.appendChild(this._toolbarContainer); 27 | container.appendChild(this._actionsContainer); 28 | 29 | var self = this; 30 | this._map = map; 31 | this._tools = []; 32 | 33 | this._tools.push(this._createButton({ 34 | title: "Fit Data Bounds", 35 | className: 'fa fa-crop', 36 | container: this._toolbarContainer, 37 | callback: function() { 38 | self._hideActionsToolbar(); 39 | self._map.fire('setview:fitBounds', {}); 40 | }, 41 | context: {} 42 | })); 43 | this._tools.push(this._createButton({ 44 | title: "Set View Location", 45 | className: 'fa fa-eye', 46 | container: this._toolbarContainer, 47 | callback: function() { 48 | self._showInputs(); 49 | }, 50 | context: {} 51 | })); 52 | 53 | return container; 54 | 55 | }, 56 | removeToolbar: function () { 57 | this._tools.forEach(function (tool) { 58 | this._dispose(tool); 59 | }); 60 | }, 61 | _createButton: function (options) { 62 | var link = L.DomUtil.create('a', options.className || '', options.container); 63 | link.href = '#'; 64 | if (options.text) { 65 | link.innerHTML = options.text; 66 | } 67 | if (options.title) { 68 | link.title = options.title; 69 | } 70 | 71 | L.DomEvent 72 | .on(link, 'click', L.DomEvent.stopPropagation) 73 | .on(link, 'mousedown', L.DomEvent.stopPropagation) 74 | .on(link, 'dblclick', L.DomEvent.stopPropagation) 75 | .on(link, 'click', L.DomEvent.preventDefault) 76 | .on(link, 'click', options.callback, options.context); 77 | 78 | return link; 79 | }, 80 | _createInput: function (options) { 81 | var input = L.DomUtil.create('input', options.className || '', options.container); 82 | input.type = options.inputType; 83 | if (options.placeholder) { 84 | input.placeholder = options.placeholder; 85 | input.title = options.placeholder; 86 | } 87 | if (options.value) { 88 | input.value = options.value; 89 | } 90 | L.DomEvent 91 | .on(input, 'mousedown', L.DomEvent.stopPropagation) 92 | .on(input, 'dblclick', L.DomEvent.stopPropagation) 93 | if (options.callback) { 94 | L.DomEvent 95 | .on(input, 'change', options.callback); 96 | } 97 | return input; 98 | }, 99 | _createSelect: function (options) { 100 | var select = L.DomUtil.create('select', options.className || '', options.container); 101 | if (options.title) { 102 | select.title = options.title; 103 | } 104 | options.choices.forEach(function (choice) { 105 | var option = L.DomUtil.create('option', '', select); 106 | option.innerHTML = choice.display; 107 | option.value = choice.value; 108 | if(options.selectedValue === choice.value) { 109 | option.selected = 'selected'; 110 | } 111 | }); 112 | if (options.callback) { 113 | L.DomEvent 114 | .on(select, 'change', options.callback); 115 | } 116 | return select; 117 | }, 118 | _dispose: function (button, callback) { 119 | L.DomEvent 120 | .off(button, 'click', L.DomEvent.stopPropagation) 121 | .off(button, 'mousedown', L.DomEvent.stopPropagation) 122 | .off(button, 'dblclick', L.DomEvent.stopPropagation) 123 | .off(button, 'click', L.DomEvent.preventDefault) 124 | .off(button, 'click', callback); 125 | }, 126 | _hideActionsToolbar: function () { 127 | this._actionsContainer.style.display = 'none'; 128 | 129 | L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-notop'); 130 | L.DomUtil.removeClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); 131 | L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-top'); 132 | L.DomUtil.removeClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); 133 | }, 134 | _showInputs: function () { 135 | var self = this; 136 | var container = this._actionsContainer; 137 | // Clean up any old stuff 138 | while (container.firstChild) { 139 | container.removeChild(container.firstChild); 140 | } 141 | 142 | var listItemClass = ''; 143 | if (this._map.getSize().x < 375) { 144 | listItemClass = 'small-screen'; 145 | } 146 | 147 | var center = this._map.getCenter(); 148 | this._lat = L.Util.formatNum(center.lat, 5); 149 | this._lon = L.Util.formatNum(center.lng, 5); 150 | this._zoom = this._map.getZoom(); 151 | 152 | let unitValue = 'dd'; 153 | if(!this._decimalDegrees) unitValue = 'dms'; 154 | this._createSelect({ 155 | container: L.DomUtil.create('li', listItemClass, container), 156 | name: 'unit', 157 | title: 'Select coordinate units; decimal degrees (dd) or degrees minutes seconds (dms)', 158 | selectedValue: unitValue, 159 | choices: [{display:'dd', value: 'dd'}, {display:'dms', value: 'dms'}], 160 | callback: function(event) { 161 | self._decimalDegrees = !self._decimalDegrees; 162 | self._hideActionsToolbar(); 163 | self._showInputs(); 164 | } 165 | }); 166 | if (this._decimalDegrees) { 167 | this._createInput({ 168 | container: L.DomUtil.create('li', listItemClass, container), 169 | inputType: 'number', 170 | placeholder: 'lat', 171 | name: 'lat', 172 | value: this._lat, 173 | callback: function(event) { 174 | self._setLat(self._getValue(event)); 175 | } 176 | }); 177 | this._createInput({ 178 | container: L.DomUtil.create('li', listItemClass, container), 179 | inputType: 'number', 180 | name: 'lon', 181 | placeholder: 'lon', 182 | value: this._lon, 183 | callback: function(event) { 184 | self._setLon(self._getValue(event)); 185 | } 186 | }); 187 | } else { 188 | this._latDms = this._ddToDms(this._lat); 189 | this._createInput({ 190 | container: L.DomUtil.create('li', listItemClass, container), 191 | inputType: 'text', 192 | placeholder: 'lat DDMMSS', 193 | name: 'latDms', 194 | value: this._latDms, 195 | callback: function(event) { 196 | self._latDms = self._getValue(event); 197 | } 198 | }); 199 | this._latDirection = 'n'; 200 | if(this._lat < 0) this._latDirection = 's'; 201 | this._createSelect({ 202 | container: L.DomUtil.create('li', listItemClass, container), 203 | name: 'latDirection', 204 | title: 'Latitude: North or South', 205 | selectedValue: this._latDirection, 206 | choices: [{display:'n', value: 'n'}, {display:'s', value: 's'}], 207 | callback: function(event) { 208 | self._latDirection = self._getValue(event); 209 | } 210 | }); 211 | this._lonDms = this._ddToDms(this._lon); 212 | this._createInput({ 213 | container: L.DomUtil.create('li', listItemClass, container), 214 | inputType: 'text', 215 | placeholder: 'lon DDMMSS', 216 | name: 'lonDms', 217 | value: this._lonDms, 218 | callback: function(event) { 219 | self._lonDms = self._getValue(event); 220 | } 221 | }); 222 | this._lonDirection = 'e'; 223 | if(this._lon < 0) this._lonDirection = 'w'; 224 | this._createSelect({ 225 | container: L.DomUtil.create('li', listItemClass, container), 226 | name: 'lonDirection', 227 | title: 'Longitude: East or West', 228 | selectedValue: this._lonDirection, 229 | choices: [{display:'e', value: 'e'}, {display:'w', value: 'w'}], 230 | callback: function(event) { 231 | self._lonDirection = self._getValue(event); 232 | } 233 | }); 234 | 235 | } 236 | var choices = []; 237 | for(var i = this._map.getMinZoom(); i <= this._map.getMaxZoom(); i++) { 238 | choices.push({ 239 | display: i, 240 | value: i 241 | }); 242 | } 243 | this._createSelect({ 244 | container: L.DomUtil.create('li', listItemClass, container), 245 | name: 'zoom', 246 | title: 'zoom level', 247 | selectedValue: this._map.getZoom(), 248 | choices: choices, 249 | callback: function(event) { 250 | self._zoom = self._getValue(event); 251 | } 252 | }); 253 | this._createButton({ 254 | title: "Click to set map view to provided values.", 255 | text: "Set View", 256 | container: L.DomUtil.create('li', listItemClass, container), 257 | callback: function() { 258 | if(!self._decimalDegrees) { 259 | self._setLat(self._dmsToDd(self._latDms, self._latDirection)); 260 | self._setLon(self._dmsToDd(self._lonDms, self._lonDirection)); 261 | } 262 | self._map.setView(L.latLng(self._lat, self._lon), self._zoom); 263 | self._hideActionsToolbar(); 264 | } 265 | }); 266 | this._createButton({ 267 | title: "Click to cancel.", 268 | text: "Cancel", 269 | container: L.DomUtil.create('li', listItemClass, container), 270 | callback: function() { 271 | self._hideActionsToolbar(); 272 | } 273 | }); 274 | L.DomUtil.addClass(this._toolbarContainer, 'leaflet-draw-toolbar-nobottom'); 275 | L.DomUtil.addClass(this._actionsContainer, 'leaflet-draw-actions-bottom'); 276 | this._actionsContainer.style.top = '25px'; 277 | this._actionsContainer.style.display = 'block'; 278 | }, 279 | _getValue: function(event) { 280 | const el = event.target || event.srcElement; 281 | return el.value; 282 | }, 283 | _setLat: function(lat) { 284 | if (lat < -90) lat = -90; 285 | if (lat > 90) lat = 90; 286 | this._lat = lat; 287 | }, 288 | _setLon: function(lon) { 289 | if (lon < -180) lon = -180; 290 | if (lon > 180) lon = 180; 291 | this._lon = lon; 292 | }, 293 | _formatNumber: function(num) { 294 | let sNum = parseInt(num, 10) + ''; 295 | if(num < 10) sNum = '0' + sNum; 296 | return sNum; 297 | }, 298 | _ddToDms: function(dd) { 299 | let deg = parseInt(Math.abs(dd), 10); 300 | let frac = Math.abs(Math.abs(dd) - deg); 301 | let min = parseInt(frac * 60, 10); 302 | let sec = frac * 3600 - min * 60; 303 | if(sec >= 60) sec = 0; 304 | return this._formatNumber(deg) + this._formatNumber(min) + this._formatNumber(sec); 305 | }, 306 | _dmsToDd: function(dms, dir) { 307 | let safeDms = ''; 308 | //remove any non-numerical characters 309 | dms.split('').forEach(function(char) { 310 | if (char >= '0' && char <= '9') safeDms += char; 311 | }); 312 | //Ensure dms is at least 6 characters 313 | while(safeDms.length < 6) { 314 | safeDms += '0'; 315 | } 316 | 317 | let degLength = 2; 318 | if(safeDms.length > 6) { 319 | degLength = 3; 320 | } 321 | let deg = parseInt(safeDms.substring(0, degLength), 10); 322 | let min = parseInt(safeDms.substring(degLength, degLength+2), 10); 323 | let sec = parseInt(safeDms.substring(degLength+2, degLength+4), 10); 324 | let dd = deg + (min / 60.0) + (sec / 3600.0); 325 | if (dir.toLowerCase() === 'w' || dir.toLowerCase() === 's') dd = dd * -1; 326 | return dd; 327 | } 328 | }); -------------------------------------------------------------------------------- /public/lib/angular-bootstrap/js/accordion-tpls.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-ui-bootstrap 3 | * http://angular-ui.github.io/bootstrap/ 4 | 5 | * Version: 2.3.0 - 2016-11-26 6 | * License: MIT 7 | */angular.module("etm-ui.bootstrap", ["etm-ui.bootstrap.tpls","etm-ui.bootstrap.accordion","etm-ui.bootstrap.collapse","etm-ui.bootstrap.tabindex"]); 8 | angular.module("etm-ui.bootstrap.tpls", ["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html"]); 9 | angular.module('etm-ui.bootstrap.accordion', ['etm-ui.bootstrap.collapse', 'etm-ui.bootstrap.tabindex']) 10 | 11 | .constant('uibAccordionConfig', { 12 | closeOthers: true 13 | }) 14 | 15 | .controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) { 16 | // This array keeps track of the accordion groups 17 | this.groups = []; 18 | 19 | // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to 20 | this.closeOthers = function(openGroup) { 21 | var closeOthers = angular.isDefined($attrs.closeOthers) ? 22 | $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; 23 | if (closeOthers) { 24 | angular.forEach(this.groups, function(group) { 25 | if (group !== openGroup) { 26 | group.isOpen = false; 27 | } 28 | }); 29 | } 30 | }; 31 | 32 | // This is called from the accordion-group directive to add itself to the accordion 33 | this.addGroup = function(groupScope) { 34 | var that = this; 35 | this.groups.push(groupScope); 36 | 37 | groupScope.$on('$destroy', function(event) { 38 | that.removeGroup(groupScope); 39 | }); 40 | }; 41 | 42 | // This is called from the accordion-group directive when to remove itself 43 | this.removeGroup = function(group) { 44 | var index = this.groups.indexOf(group); 45 | if (index !== -1) { 46 | this.groups.splice(index, 1); 47 | } 48 | }; 49 | }]) 50 | 51 | // The accordion directive simply sets up the directive controller 52 | // and adds an accordion CSS class to itself element. 53 | .directive('uibAccordion', function() { 54 | return { 55 | controller: 'UibAccordionController', 56 | controllerAs: 'accordion', 57 | transclude: true, 58 | template: "
" 59 | }; 60 | }) 61 | 62 | // The accordion-group directive indicates a block of html that will expand and collapse in an accordion 63 | .directive('uibAccordionGroup', function() { 64 | return { 65 | require: '^uibAccordion', // We need this directive to be inside an accordion 66 | transclude: true, // It transcludes the contents of the directive into the template 67 | restrict: 'A', 68 | template: "
\n" + 69 | "

\n" + 70 | " {{heading}}\n" + 71 | "

\n" + 72 | "
\n" + 73 | "
\n" + 74 | "
\n" + 75 | "
\n", 76 | scope: { 77 | heading: '@', // Interpolate the heading attribute onto this scope 78 | panelClass: '@?', // Ditto with panelClass 79 | isOpen: '=?', 80 | isDisabled: '=?' 81 | }, 82 | controller: function() { 83 | this.setHeading = function(element) { 84 | this.heading = element; 85 | }; 86 | }, 87 | link: function(scope, element, attrs, accordionCtrl) { 88 | element.addClass('panel'); 89 | accordionCtrl.addGroup(scope); 90 | 91 | scope.openClass = attrs.openClass || 'panel-open'; 92 | scope.panelClass = attrs.panelClass || 'panel-default'; 93 | scope.$watch('isOpen', function(value) { 94 | element.toggleClass(scope.openClass, !!value); 95 | if (value) { 96 | accordionCtrl.closeOthers(scope); 97 | } 98 | }); 99 | 100 | scope.toggleOpen = function($event) { 101 | if (!scope.isDisabled) { 102 | if (!$event || $event.which === 32) { 103 | scope.isOpen = !scope.isOpen; 104 | } 105 | } 106 | }; 107 | 108 | var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000); 109 | scope.headingId = id + '-tab'; 110 | scope.panelId = id + '-panel'; 111 | } 112 | }; 113 | }) 114 | 115 | // Use accordion-heading below an accordion-group to provide a heading containing HTML 116 | .directive('uibAccordionHeading', function() { 117 | return { 118 | transclude: true, // Grab the contents to be used as the heading 119 | template: '', // In effect remove this element! 120 | replace: true, 121 | require: '^uibAccordionGroup', 122 | link: function(scope, element, attrs, accordionGroupCtrl, transclude) { 123 | // Pass the heading to the accordion-group controller 124 | // so that it can be transcluded into the right place in the template 125 | // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] 126 | accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); 127 | } 128 | }; 129 | }) 130 | 131 | // Use in the accordion-group template to indicate where you want the heading to be transcluded 132 | // You must provide the property on the accordion-group controller that will hold the transcluded element 133 | .directive('uibAccordionTransclude', function() { 134 | return { 135 | require: '^uibAccordionGroup', 136 | link: function(scope, element, attrs, controller) { 137 | scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) { 138 | if (heading) { 139 | var elem = angular.element(element[0].querySelector(getHeaderSelectors())); 140 | elem.html(''); 141 | elem.append(heading); 142 | } 143 | }); 144 | } 145 | }; 146 | 147 | function getHeaderSelectors() { 148 | return 'uib-accordion-header,' + 149 | 'data-uib-accordion-header,' + 150 | 'x-uib-accordion-header,' + 151 | 'uib\\:accordion-header,' + 152 | '[uib-accordion-header],' + 153 | '[data-uib-accordion-header],' + 154 | '[x-uib-accordion-header]'; 155 | } 156 | }); 157 | 158 | angular.module('etm-ui.bootstrap.collapse', []) 159 | 160 | .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { 161 | var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; 162 | return { 163 | link: function(scope, element, attrs) { 164 | var expandingExpr = $parse(attrs.expanding), 165 | expandedExpr = $parse(attrs.expanded), 166 | collapsingExpr = $parse(attrs.collapsing), 167 | collapsedExpr = $parse(attrs.collapsed), 168 | horizontal = false, 169 | css = {}, 170 | cssTo = {}; 171 | 172 | init(); 173 | 174 | function init() { 175 | horizontal = !!('horizontal' in attrs); 176 | if (horizontal) { 177 | css = { 178 | width: '' 179 | }; 180 | cssTo = {width: '0'}; 181 | } else { 182 | css = { 183 | height: '' 184 | }; 185 | cssTo = {height: '0'}; 186 | } 187 | if (!scope.$eval(attrs.uibCollapse)) { 188 | element.addClass('in') 189 | .addClass('collapse') 190 | .attr('aria-expanded', true) 191 | .attr('aria-hidden', false) 192 | .css(css); 193 | } 194 | } 195 | 196 | function getScrollFromElement(element) { 197 | if (horizontal) { 198 | return {width: element.scrollWidth + 'px'}; 199 | } 200 | return {height: element.scrollHeight + 'px'}; 201 | } 202 | 203 | function expand() { 204 | if (element.hasClass('collapse') && element.hasClass('in')) { 205 | return; 206 | } 207 | 208 | $q.resolve(expandingExpr(scope)) 209 | .then(function() { 210 | element.removeClass('collapse') 211 | .addClass('collapsing') 212 | .attr('aria-expanded', true) 213 | .attr('aria-hidden', false); 214 | 215 | if ($animateCss) { 216 | $animateCss(element, { 217 | addClass: 'in', 218 | easing: 'ease', 219 | css: { 220 | overflow: 'hidden' 221 | }, 222 | to: getScrollFromElement(element[0]) 223 | }).start()['finally'](expandDone); 224 | } else { 225 | $animate.addClass(element, 'in', { 226 | css: { 227 | overflow: 'hidden' 228 | }, 229 | to: getScrollFromElement(element[0]) 230 | }).then(expandDone); 231 | } 232 | }); 233 | } 234 | 235 | function expandDone() { 236 | element.removeClass('collapsing') 237 | .addClass('collapse') 238 | .css(css); 239 | expandedExpr(scope); 240 | } 241 | 242 | function collapse() { 243 | if (!element.hasClass('collapse') && !element.hasClass('in')) { 244 | return collapseDone(); 245 | } 246 | 247 | $q.resolve(collapsingExpr(scope)) 248 | .then(function() { 249 | element 250 | // IMPORTANT: The width must be set before adding "collapsing" class. 251 | // Otherwise, the browser attempts to animate from width 0 (in 252 | // collapsing class) to the given width here. 253 | .css(getScrollFromElement(element[0])) 254 | // initially all panel collapse have the collapse class, this removal 255 | // prevents the animation from jumping to collapsed state 256 | .removeClass('collapse') 257 | .addClass('collapsing') 258 | .attr('aria-expanded', false) 259 | .attr('aria-hidden', true); 260 | 261 | if ($animateCss) { 262 | $animateCss(element, { 263 | removeClass: 'in', 264 | to: cssTo 265 | }).start()['finally'](collapseDone); 266 | } else { 267 | $animate.removeClass(element, 'in', { 268 | to: cssTo 269 | }).then(collapseDone); 270 | } 271 | }); 272 | } 273 | 274 | function collapseDone() { 275 | element.css(cssTo); // Required so that collapse works when animation is disabled 276 | element.removeClass('collapsing') 277 | .addClass('collapse'); 278 | collapsedExpr(scope); 279 | } 280 | 281 | scope.$watch(attrs.uibCollapse, function(shouldCollapse) { 282 | if (shouldCollapse) { 283 | collapse(); 284 | } else { 285 | expand(); 286 | } 287 | }); 288 | } 289 | }; 290 | }]); 291 | 292 | angular.module('etm-ui.bootstrap.tabindex', []) 293 | 294 | .directive('uibTabindexToggle', function() { 295 | return { 296 | restrict: 'A', 297 | link: function(scope, elem, attrs) { 298 | attrs.$observe('disabled', function(disabled) { 299 | attrs.$set('tabindex', disabled ? -1 : null); 300 | }); 301 | } 302 | }; 303 | }); -------------------------------------------------------------------------------- /public/visController.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import _ from 'lodash'; 3 | import $ from 'jquery'; 4 | import { Binder } from 'ui/binder'; 5 | import MapProvider from 'plugins/enhanced_tilemap/vislib/_map'; 6 | import { VislibVisTypeBuildChartDataProvider } from 'ui/vislib_vis_type/build_chart_data'; 7 | import { backwardsCompatible } from './backwardsCompatible'; 8 | import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; 9 | import { ResizeCheckerProvider } from 'ui/vislib/lib/resize_checker'; 10 | import { uiModules } from 'ui/modules'; 11 | import { TileMapTooltipFormatterProvider } from 'ui/agg_response/geo_json/_tooltip_formatter'; 12 | 13 | define(function (require) { 14 | var module = uiModules.get('kibana/enhanced_tilemap', [ 15 | 'kibana', 16 | 'etm-ui.bootstrap.accordion', 17 | 'rzModule', 18 | 'angularjs-dropdown-multiselect' 19 | ]); 20 | 21 | module.controller('KbnEnhancedTilemapVisController', function ( 22 | $scope, $rootScope, $element, $timeout, 23 | Private, courier, config, getAppState, indexPatterns) { 24 | let buildChartData = Private(VislibVisTypeBuildChartDataProvider); 25 | const queryFilter = Private(FilterBarQueryFilterProvider); 26 | const callbacks = Private(require('plugins/enhanced_tilemap/callbacks')); 27 | const geoFilter = Private(require('plugins/enhanced_tilemap/vislib/geoFilter')); 28 | const POIsProvider = Private(require('plugins/enhanced_tilemap/POIs')); 29 | const utils = require('plugins/enhanced_tilemap/utils'); 30 | let TileMapMap = Private(MapProvider); 31 | const ResizeChecker = Private(ResizeCheckerProvider); 32 | const SearchTooltip = Private(require('plugins/enhanced_tilemap/tooltip/searchTooltip')); 33 | const VisTooltip = Private(require('plugins/enhanced_tilemap/tooltip/visTooltip')); 34 | let map = null; 35 | let collar = null; 36 | let chartData = null; 37 | let tooltip = null; 38 | let tooltipFormatter = null; 39 | 40 | backwardsCompatible.updateParams($scope.vis.params); 41 | appendMap(); 42 | modifyToDsl(); 43 | setTooltipFormatter($scope.vis.params.tooltip); 44 | 45 | const shapeFields = $scope.vis.indexPattern.fields.filter(function (field) { 46 | return field.type === 'geo_shape'; 47 | }).map(function (field) { 48 | return field.name; 49 | }); 50 | //Using $root as mechanism to pass data to vis-editor-vis-options scope 51 | $scope.$root.etm = { 52 | shapeFields: shapeFields 53 | }; 54 | 55 | const binder = new Binder(); 56 | const resizeChecker = new ResizeChecker($element); 57 | binder.on(resizeChecker, 'resize', function() { 58 | resizeArea(); 59 | }); 60 | 61 | const respProcessor = { 62 | buildChartData: buildChartData, 63 | process: function(resp) { 64 | const aggs = resp.aggregations; 65 | _.keys(aggs).forEach(function(key) { 66 | if(_.has(aggs[key], "filtered_geohash")) { 67 | aggs[key].buckets = aggs[key].filtered_geohash.buckets; 68 | delete aggs[key].filtered_geohash; 69 | } 70 | }); 71 | 72 | const chartData = this.buildChartData(resp); 73 | const geoMinMax = utils.getGeoExtents(chartData); 74 | chartData.geoJson.properties.allmin = geoMinMax.min; 75 | chartData.geoJson.properties.allmax = geoMinMax.max; 76 | return chartData; 77 | }, 78 | vis: $scope.vis 79 | } 80 | 81 | function modifyToDsl() { 82 | $scope.vis.aggs.origToDsl = $scope.vis.aggs.toDsl; 83 | $scope.vis.aggs.toDsl = function() { 84 | resizeArea(); 85 | const dsl = $scope.vis.aggs.origToDsl(); 86 | 87 | //append map collar filter to geohash_grid aggregation 88 | _.keys(dsl).forEach(function(key) { 89 | if(_.has(dsl[key], "geohash_grid")) { 90 | const origAgg = dsl[key]; 91 | dsl[key] = { 92 | filter: aggFilter(origAgg.geohash_grid.field), 93 | aggs: { 94 | filtered_geohash: origAgg 95 | } 96 | } 97 | } 98 | }); 99 | return dsl; 100 | } 101 | } 102 | 103 | function aggFilter(field) { 104 | collar = utils.scaleBounds( 105 | map.mapBounds(), 106 | $scope.vis.params.collarScale); 107 | var filter = {geo_bounding_box: {}}; 108 | filter.geo_bounding_box[field] = collar; 109 | return filter; 110 | } 111 | 112 | $scope.$watch('vis.aggs', function (resp) { 113 | //'apply changes' creates new vis.aggs object - ensure toDsl is overwritten again 114 | if(!_.has($scope.vis.aggs, "origToDsl")) { 115 | modifyToDsl(); 116 | } 117 | }); 118 | 119 | function initPOILayer(layerParams) { 120 | const poi = new POIsProvider(layerParams); 121 | const options = { 122 | color: _.get(layerParams, 'color', '#008800'), 123 | size: _.get(layerParams, 'markerSize', 'm') 124 | }; 125 | poi.getLayer(options, function(layer) { 126 | map.addPOILayer(layerParams.savedSearchId, layer); 127 | }); 128 | } 129 | 130 | $scope.$watch('vis.params', function (visParams, oldParams) { 131 | if (visParams !== oldParams) { 132 | //When vis is first opened, vis.params gets updated with old context 133 | backwardsCompatible.updateParams($scope.vis.params); 134 | 135 | setTooltipFormatter(visParams.tooltip); 136 | 137 | draw(); 138 | 139 | map.saturateTiles(visParams.isDesaturated); 140 | map.clearPOILayers(); 141 | $scope.vis.params.overlays.savedSearches.forEach(function (layerParams) { 142 | initPOILayer(layerParams); 143 | }); 144 | } 145 | }); 146 | 147 | $scope.$watch('esResponse', function (resp) { 148 | if(_.has(resp, 'aggregations')) { 149 | chartData = respProcessor.process(resp); 150 | draw(); 151 | 152 | _.filter($scope.vis.params.overlays.savedSearches, function(layerParams) { 153 | return layerParams.syncFilters 154 | }).forEach(function (layerParams) { 155 | initPOILayer(layerParams); 156 | }); 157 | } 158 | }); 159 | 160 | $scope.$on("$destroy", function() { 161 | binder.destroy(); 162 | resizeChecker.destroy(); 163 | if (map) map.destroy(); 164 | if (tooltip) tooltip.destroy() 165 | }); 166 | 167 | function draw() { 168 | if(!chartData) return; 169 | 170 | //add overlay layer to provide visibility of filtered area 171 | let fieldName = getGeoField().fieldname; 172 | if (fieldName) { 173 | map.addFilters(geoFilter.getGeoFilters(fieldName)); 174 | } 175 | 176 | drawWmsOverlays(); 177 | 178 | map.addMarkers( 179 | chartData, 180 | $scope.vis.params, 181 | tooltipFormatter, 182 | _.get(chartData, 'valueFormatter', _.identity), 183 | collar); 184 | } 185 | 186 | function setTooltipFormatter(tooltipParams) { 187 | if (tooltip) { 188 | tooltip.destroy(); 189 | } 190 | 191 | const options = { 192 | xRatio: _.get(tooltipParams, 'options.xRatio', 0.6), 193 | yRatio: _.get(tooltipParams, 'options.yRatio', 0.6) 194 | }; 195 | const geoField = getGeoField(); 196 | // search directive changed a lot in 5.5 - no longer supported at this time 197 | /*if (_.get(tooltipParams, 'type') === 'search') { 198 | tooltip = new SearchTooltip( 199 | _.get(tooltipParams, 'options.searchId'), 200 | geoField.fieldname, 201 | geoField.geotype, 202 | options); 203 | tooltipFormatter = tooltip.getFormatter(); 204 | }*/ 205 | if (_.get(tooltipParams, 'type') === 'visualization') { 206 | tooltip = new VisTooltip( 207 | _.get(tooltipParams, 'options.visId'), 208 | geoField.fieldname, 209 | geoField.geotype, 210 | options); 211 | tooltipFormatter = tooltip.getFormatter(); 212 | } else { 213 | tooltipFormatter = Private(TileMapTooltipFormatterProvider); 214 | } 215 | 216 | } 217 | 218 | /** 219 | * Field used for Geospatial filtering can be set in multiple places 220 | * 1) field specified by geohash_grid aggregation 221 | * 2) field specified under options. Allows for filtering by geo_shape 222 | * 223 | * Use this method to locate the field 224 | */ 225 | function getGeoField() { 226 | let fieldname = null; 227 | let geotype = 'geo_point'; 228 | if ($scope.vis.params.filterByShape && $scope.vis.params.shapeField) { 229 | fieldname = $scope.vis.params.shapeField; 230 | geotype = 'geo_shape'; 231 | } else { 232 | const agg = utils.getAggConfig($scope.vis.aggs, 'segment'); 233 | if (agg) { 234 | fieldname = agg.fieldName(); 235 | } 236 | } 237 | return { 238 | fieldname: fieldname, 239 | geotype: geotype 240 | }; 241 | } 242 | 243 | function drawWmsOverlays() { 244 | const prevState = map.clearWMSOverlays(); 245 | if ($scope.vis.params.overlays.wmsOverlays.length === 0) { 246 | return; 247 | } 248 | 249 | $scope.vis.params.overlays.wmsOverlays.forEach(function(layerParams) { 250 | const wmsIndexId = _.get(layerParams, 'indexId', $scope.vis.indexPattern.id); 251 | indexPatterns.get(wmsIndexId).then(function(indexPattern) { 252 | const source = new courier.SearchSource(); 253 | const appState = getAppState(); 254 | source.set('filter', queryFilter.getFilters()); 255 | if (appState.query && !appState.linked) { 256 | source.set('query', appState.query); 257 | } 258 | source.index(indexPattern); 259 | source._flatten().then(function (fetchParams) { 260 | const esQuery = fetchParams.body.query; 261 | //remove kibana parts of query 262 | const cleanedMust = []; 263 | if (_.has(esQuery, 'bool.must')) { 264 | esQuery.bool.must.forEach(function(must) { 265 | cleanedMust.push(_.omit(must, ['$state', '$$hashKey'])); 266 | }); 267 | } 268 | esQuery.bool.must = cleanedMust; 269 | const cleanedMustNot = []; 270 | if (_.has(esQuery, 'bool.must_not')) { 271 | esQuery.bool.must_not.forEach(function(mustNot) { 272 | cleanedMustNot.push(_.omit(mustNot, ['$state', '$$hashKey'])); 273 | }); 274 | } 275 | esQuery.bool.must_not = cleanedMustNot; 276 | 277 | const name = _.get(layerParams, 'displayName', layerParams.layers); 278 | const wmsOptions = { 279 | format: 'image/png', 280 | layers: layerParams.layers, 281 | maxFeatures: _.get(layerParams, 'maxFeatures', 1000), 282 | minZoom: _.get(layerParams, 'minZoom', 13), 283 | transparent: true, 284 | version: '1.1.1' 285 | }; 286 | const viewparams = []; 287 | if (_.get(layerParams, 'viewparams')) { 288 | viewparams.push('q:' + JSON.stringify(esQuery)); 289 | } 290 | const aggs = _.get(layerParams, 'agg', ''); 291 | if (aggs.length !== 0) { 292 | viewparams.push('a:' + aggs); 293 | } 294 | if (viewparams.length >= 1) { 295 | //http://docs.geoserver.org/stable/en/user/data/database/sqlview.html#using-a-parametric-sql-view 296 | wmsOptions.viewparams = _.map(viewparams, param => { 297 | let escaped = param; 298 | escaped = escaped.replace(new RegExp('[,]', 'g'), '\\,'); //escape comma 299 | //escaped = escaped.replace(/\s/g, ''); //remove whitespace 300 | return escaped; 301 | }).join(';'); 302 | } 303 | const cqlFilter = _.get(layerParams, 'cqlFilter', ''); 304 | if (cqlFilter.length !== 0) { 305 | wmsOptions.CQL_FILTER = cqlFilter; 306 | } 307 | const styles = _.get(layerParams, 'styles', ''); 308 | if (styles.length !== 0) { 309 | wmsOptions.styles = styles; 310 | } 311 | const formatOptions = _.get(layerParams, 'formatOptions', ''); 312 | if (formatOptions.length !== 0) { 313 | wmsOptions.format_options = formatOptions; 314 | } 315 | const layerOptions = { 316 | isVisible: _.get(prevState, name, true), 317 | nonTiled: _.get(layerParams, 'nonTiled', false) 318 | } 319 | map.addWmsOverlay(layerParams.url, name, wmsOptions, layerOptions); 320 | }); 321 | }); 322 | }); 323 | } 324 | 325 | function appendMap() { 326 | const initialMapState = utils.getMapStateFromVis($scope.vis); 327 | var params = $scope.vis.params; 328 | var container = $element[0].querySelector('.tilemap'); 329 | map = new TileMapMap(container, { 330 | center: initialMapState.center, 331 | zoom: initialMapState.zoom, 332 | callbacks: callbacks, 333 | mapType: params.mapType, 334 | attr: params, 335 | editable: $scope.vis.getEditableVis() ? true : false 336 | }); 337 | } 338 | 339 | function resizeArea() { 340 | if (map) map.updateSize(); 341 | } 342 | }); 343 | }); 344 | -------------------------------------------------------------------------------- /public/vislib/marker_types/base_marker.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3'; 2 | import _ from 'lodash'; 3 | import $ from 'jquery'; 4 | import L from 'leaflet'; 5 | import utils from 'plugins/enhanced_tilemap/utils'; 6 | 7 | define(function (require) { 8 | return function MarkerFactory($compile, $rootScope) { 9 | 10 | /** 11 | * Base map marker overlay, all other markers inherit from this class 12 | * 13 | * @param map {Leaflet Object} 14 | * @param geoJson {geoJson Object} 15 | * @param params {Object} 16 | */ 17 | function BaseMarker(map, geoJson, layerControl, params) { 18 | this.map = map; 19 | this.geoJson = geoJson; 20 | this.layerControl = layerControl; 21 | this.popups = []; 22 | this.threshold = { 23 | min: _.get(geoJson, 'properties.allmin', 0), 24 | max: _.get(geoJson, 'properties.allmax', 1) 25 | }; 26 | this.isVisible = _.get(params, 'prevState.isVisible', true); 27 | 28 | if (params.prevState) { 29 | //Scale threshold to have same shape as previous zoom level 30 | const prevRange = params.prevState.threshold.ceil - params.prevState.threshold.floor; 31 | const newRange = _.get(geoJson, 'properties.allmax', 1) - _.get(geoJson, 'properties.allmin', 0); 32 | if (params.prevState.threshold.min > params.prevState.threshold.floor) { 33 | const prevMinRatio = (params.prevState.threshold.min - params.prevState.threshold.floor) / prevRange; 34 | this.threshold.min = prevMinRatio * newRange; 35 | } 36 | if (params.prevState.threshold.max < params.prevState.threshold.floor) { 37 | const prevMaxRatio = (params.prevState.threshold.max - params.prevState.threshold.floor) / prevRange; 38 | this.threshold.max = prevMaxRatio * newRange; 39 | } 40 | } 41 | 42 | this._tooltipFormatter = params.tooltipFormatter || _.identity; 43 | this._valueFormatter = params.valueFormatter || _.identity; 44 | this._attr = params.attr || {}; 45 | 46 | // set up the default legend colors 47 | this.quantizeLegendColors(); 48 | } 49 | 50 | /** 51 | * Adds legend div to each map when data is split 52 | * uses d3 scale from BaseMarker.prototype.quantizeLegendColors 53 | * 54 | * @method addLegend 55 | * @return {undefined} 56 | */ 57 | BaseMarker.prototype.addLegend = function () { 58 | // ensure we only ever create 1 legend 59 | if (this._legend) return; 60 | 61 | let self = this; 62 | 63 | // create the legend control, keep a reference 64 | self._legend = L.control({position: 'bottomright'}); 65 | 66 | self._legend.onAdd = function () { 67 | // creates all the neccessary DOM elements for the control, adds listeners 68 | // on relevant map events, and returns the element containing the control 69 | let $div = $('
').addClass('tilemap-legend'); 70 | 71 | self.$sliderScope = $rootScope.$new(); 72 | self.$sliderScope.slider = { 73 | min: self.threshold.min, 74 | max: self.threshold.max, 75 | options: { 76 | floor: _.get(self.geoJson, 'properties.allmin', 0), 77 | ceil: _.get(self.geoJson, 'properties.allmax', 1), 78 | onEnd: function(sliderId, modelValue, highValue, pointerType) { 79 | self.threshold.min = modelValue; 80 | self.threshold.max = highValue; 81 | self.destroy(); 82 | self._createMarkerGroup(self.markerOptions); 83 | } 84 | } 85 | }; 86 | const linkFn = $compile(require('./legendSlider.html')); 87 | const $sliderEl = linkFn(self.$sliderScope); 88 | $div.append($sliderEl); 89 | 90 | _.each(self._legendColors, function (color, i) { 91 | let labelText = self._legendQuantizer 92 | .invertExtent(color) 93 | .map(self._valueFormatter) 94 | .join(' – '); 95 | 96 | let label = $('
').text(labelText); 97 | 98 | let icon = $('').css({ 99 | background: color, 100 | 'border-color': self.darkerColor(color) 101 | }); 102 | 103 | label.append(icon); 104 | $div.append(label); 105 | }); 106 | 107 | return $div.get(0); 108 | }; 109 | 110 | if (self.isVisible) self._legend.addTo(self.map); 111 | }; 112 | 113 | BaseMarker.prototype.removeLegend = function () { 114 | if (this.$sliderScope) { 115 | this.$sliderScope.$destroy(); 116 | } 117 | 118 | if (this._legend) { 119 | if (this._legend._map) { 120 | this.map.removeControl(this._legend); 121 | } 122 | this._legend = undefined; 123 | } 124 | } 125 | 126 | /** 127 | * Apply style with shading to feature 128 | * 129 | * @method applyShadingStyle 130 | * @param value {Object} 131 | * @return {Object} 132 | */ 133 | BaseMarker.prototype.applyShadingStyle = function (value) { 134 | let color = this._legendQuantizer(value); 135 | if(color == undefined && 'Dynamic - Uneven' === this._attr.scaleType) { 136 | // Because this scale is threshold based and we added just as many ranges 137 | // as we did for the domain the max value is counted as being outside the 138 | // range so we get undefined. We want to count this as part of the last domain. 139 | color = this._legendColors[this._legendColors.length - 1]; 140 | } 141 | 142 | return { 143 | fillColor: color, 144 | color: this.darkerColor(color), 145 | weight: 1.5, 146 | opacity: 1, 147 | fillOpacity: 0.75 148 | }; 149 | }; 150 | 151 | /** 152 | * Binds popup and events to each feature on map 153 | * 154 | * @method bindPopup 155 | * @param feature {Object} 156 | * @param layer {Object} 157 | * return {undefined} 158 | */ 159 | BaseMarker.prototype.bindPopup = function (feature, layer) { 160 | let self = this; 161 | 162 | let popup = layer.on({ 163 | mouseover: function (e) { 164 | let layer = e.target; 165 | // bring layer to front if not older browser 166 | if (!L.Browser.ie && !L.Browser.opera) { 167 | layer.bringToFront(); 168 | } 169 | self._showTooltip(feature); 170 | }, 171 | mouseout: function (e) { 172 | if (_.get(self._attr, 'tooltip.closeOnMouseout', true)) { 173 | self._hidePopup(); 174 | } 175 | } 176 | }); 177 | 178 | self.popups.push(popup); 179 | }; 180 | 181 | /** 182 | * d3 method returns a darker hex color, 183 | * used for marker stroke color 184 | * 185 | * @method darkerColor 186 | * @param color {String} hex color 187 | * @param amount? {Number} amount to darken by 188 | * @return {String} hex color 189 | */ 190 | BaseMarker.prototype.darkerColor = function (color, amount) { 191 | amount = amount || 1.3; 192 | return d3.hcl(color).darker(amount).toString(); 193 | }; 194 | 195 | /** 196 | * Remove marker layer, popup, and legend from map 197 | * @return {Object} marker layer display state 198 | */ 199 | BaseMarker.prototype.destroy = function () { 200 | const state = { 201 | isVisible: this._markerGroup && this.map.hasLayer(this._markerGroup), 202 | threshold: { 203 | floor: _.get(this.geoJson, 'properties.allmin', 0), 204 | ceil: _.get(this.geoJson, 'properties.allmax', 1), 205 | min: this.threshold.min, 206 | max: this.threshold.max 207 | } 208 | }; 209 | 210 | this._stopLoadingGeohash(); 211 | 212 | // remove popups 213 | this.popups = this.popups.filter(function (popup) { 214 | popup.off('mouseover').off('mouseout'); 215 | }); 216 | this._hidePopup(); 217 | 218 | this.removeLegend(); 219 | 220 | // remove marker layer from map 221 | if (this._markerGroup) { 222 | this.layerControl.removeLayer(this._markerGroup); 223 | if (this.map.hasLayer(this._markerGroup)) { 224 | this.map.removeLayer(this._markerGroup); 225 | } 226 | this._markerGroup = undefined; 227 | } 228 | 229 | return state; 230 | }; 231 | 232 | BaseMarker.prototype.hide = function () { 233 | this._stopLoadingGeohash(); 234 | if (this._legend) { 235 | this.map.removeControl(this._legend); 236 | } 237 | } 238 | 239 | BaseMarker.prototype.show = function () { 240 | if (this._legend) { 241 | this._legend.addTo(this.map); 242 | } 243 | } 244 | 245 | BaseMarker.prototype._addToMap = function () { 246 | this.layerControl.addOverlay(this._markerGroup, "Aggregation"); 247 | if (this.isVisible) this.map.addLayer(this._markerGroup); 248 | 249 | if (this.geoJson.features.length > 1) { 250 | this.addLegend(); 251 | } 252 | }; 253 | 254 | /** 255 | * Creates leaflet marker group, passing options to L.geoJson 256 | * 257 | * @method _createMarkerGroup 258 | * @param options {Object} Options to pass to L.geoJson 259 | */ 260 | BaseMarker.prototype._createMarkerGroup = function (options) { 261 | let self = this; 262 | self.markerOptions = options; 263 | let defaultOptions = { 264 | filter: function(feature) { 265 | const value = _.get(feature, 'properties.value', 0); 266 | return value >= self.threshold.min && value <= self.threshold.max; 267 | }, 268 | onEachFeature: function (feature, layer) { 269 | self.bindPopup(feature, layer); 270 | }, 271 | style: function (feature) { 272 | let value = _.get(feature, 'properties.value'); 273 | return self.applyShadingStyle(value); 274 | } 275 | }; 276 | 277 | if(self.geoJson.features.length <= 250) { 278 | this._markerGroup = L.geoJson(self.geoJson, _.defaults(defaultOptions, options)); 279 | } else { 280 | //don't block UI when processing lots of features 281 | this._markerGroup = L.geoJson(self.geoJson.features.slice(0,100), _.defaults(defaultOptions, options)); 282 | this._stopLoadingGeohash(); 283 | 284 | this._createSpinControl(); 285 | var place = 100; 286 | this._intervalId = setInterval( 287 | function() { 288 | var stopIndex = place + 100; 289 | var halt = false; 290 | if(stopIndex > self.geoJson.features.length) { 291 | stopIndex = self.geoJson.features.length; 292 | halt = true; 293 | } 294 | for(var i=place; i