diff --git a/package.json b/package.json index ca22ed54db1..25ac32e5ad6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "mongodb-connection-model": "^3.0.6", "mongodb-instance-model": "^1.0.2", "mongodb-ns": "^1.0.0", - "scout-server": "http://bin.mongodb.org/js/scout-server/v0.4.2/scout-server-0.4.2.tar.gz" + "scout-server": "http://bin.mongodb.org/js/scout-server/v0.4.2/scout-server-0.4.2.tar.gz", + "google-maps": "^3.1.0" }, "devDependencies": { "ampersand-app": "^1.0.4", @@ -91,6 +92,7 @@ "ampersand-sync-localforage": "^0.1.1", "ampersand-view": "^9.0.0", "ampersand-view-switcher": "^2.0.0", + "async": "^1.5.0", "backoff": "^2.4.1", "bootstrap": "https://github.com/twbs/bootstrap/archive/v3.3.5.tar.gz", "browserify": "^12.0.1", diff --git a/src/app.js b/src/app.js index 6b0b393d2a6..a4856c3afe0 100644 --- a/src/app.js +++ b/src/app.js @@ -236,7 +236,7 @@ var state = new Application({ // via `window.localStorage`. var FEATURES = { querybuilder: true, - 'First Run Tour': false, + 'Geo Minicharts': true, 'Connect with SSL': false, 'Connect with Kerberos': false, 'Connect with LDAP': false, @@ -245,8 +245,6 @@ var FEATURES = { app.extend({ client: null, - // @note (imlucas): Backwards compat for querybuilder - features: FEATURES, /** * Check whether a feature flag is currently enabled. * @@ -256,6 +254,16 @@ app.extend({ isFeatureEnabled: function(id) { return FEATURES[id] === true; }, + /** + * Enable or disable a feature programatically. + * + * @param {String} id - A key in `FEATURES`. + * @param {Boolean} bool - whether to enable (true) or disable (false) + * @return {Boolean} + */ + setFeature: function(id, bool) { + FEATURES[id] = bool; + }, sendMessage: function(msg) { ipc.send('message', msg); }, diff --git a/src/field-list/index.js b/src/field-list/index.js index c27769555d0..489b04fb2a7 100644 --- a/src/field-list/index.js +++ b/src/field-list/index.js @@ -147,9 +147,9 @@ FieldListView = View.extend({ }, makeFieldVisible: function() { var views = this.fieldCollectionView.views; - _.each(views, function(field_view) { + _.each(views, function(fieldView) { raf(function() { - field_view.visible = true; + fieldView.visible = true; }); }); }, diff --git a/src/index.jade b/src/index.jade index c403c816e96..31cacfb4f20 100644 --- a/src/index.jade +++ b/src/index.jade @@ -2,8 +2,7 @@ doctype html html(lang='en') head title MongoDB - meta(http-equiv="Content-Security-Policy", content="default-src *; script-src 'self' http://localhost:35729; style-src 'self' 'unsafe-inline';") - + meta(http-equiv="Content-Security-Policy", content="default-src *; script-src 'self' https://*.googleapis.com https://maps.gstatic.com http://localhost:35729 'unsafe-eval'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline';") meta(name='viewport', content='initial-scale=1') link(rel='stylesheet', href='index.css', charset='UTF-8') body diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js new file mode 100644 index 00000000000..94369d6317b --- /dev/null +++ b/src/minicharts/d3fns/geo.js @@ -0,0 +1,388 @@ +var d3 = require('d3'); +var _ = require('lodash'); +var shared = require('./shared'); +var debug = require('debug')('scout:minicharts:geo'); +var GoogleMapsLoader = require('google-maps'); +var mapStyle = require('./mapstyle'); +var async = require('async'); +var app = require('ampersand-app'); + +var SHIFTKEY = 16; + + +/* eslint wrap-iife:0 */ +var Singleton = (function() { + var instance; + + function createInstance() { + var object = {}; + return object; + } + + return { + getInstance: function() { + if (!instance) { + instance = createInstance(); + } + return instance; + } + }; +})(); + +// From: http://davidbcalhoun.com/2014/async.parallel-with-a-simple-timeout-node-js/ +// async.parallel with optional timeout (options.timeoutMS) +function parallel(options, tasks, cb) { + // sanity checks + options = options || {}; + + // no timeout wrapper; passthrough to async.parallel + if (typeof options.timeoutMS !== 'number') return async.parallel(tasks, cb); + + var timeout = setTimeout(function() { + // remove timeout, so we'll know we already erred out + timeout = null; + + // error out + cb('async.parallel timed out out after ' + options.timeoutMS + 'ms.', null); + }, options.timeoutMS); + + async.parallel(tasks, function(err, result) { + // after all tasks are complete + + // noop if timeout was called and annulled + if (!timeout) return; + + // cancel timeout (if timeout was set longer, and all parallel tasks finished sooner) + clearTimeout(timeout); + + // passthrough the data to the cb + cb(err, result); + }); +} + + +var singleton = Singleton.getInstance(); + +var minicharts_d3fns_geo = function() { + // --- beginning chart setup --- + var width = 400; + var height = 100; + + var googleMap = null; + var overlay = null; + var projection = null; + var selectionCircle; + var currentCoord; + + var options = { + view: null + }; + + var margin = shared.margin; + + function pointInCircle(point, radius, center) { + return singleton.google.maps.geometry.spherical.computeDistanceBetween(point, center) <= radius; + } + + function selectPoints() { + var frame = options.el; + var google = singleton.google; + + if (selectionCircle.getRadius() === 0) { + d3.select(frame).selectAll('.marker circle') + .classed('selected', false); + return; + } + + d3.select(frame).selectAll('.marker circle') + .classed('selected', function(d) { + var p = new google.maps.LatLng(d[1], d[0]); + return pointInCircle(p, selectionCircle.getRadius(), selectionCircle.getCenter()); + }); + } + + function onKeyDown() { + if (d3.event.keyCode === SHIFTKEY) { + // disable dragging while shift is pressed + googleMap.setOptions({ draggable: false }); + } + } + + function onKeyUp() { + if (d3.event.keyCode === SHIFTKEY) { + // disable dragging while shift is pressed + googleMap.setOptions({ draggable: true }); + } + } + + function startSelection() { + if (!d3.event.shiftKey) { + return; + } + + var google = singleton.google; + + var frame = this; + var center = d3.mouse(frame); + + // set selectionCoordinates, they are needed to pan the selection circle + var centerPoint = new google.maps.Point(center[0], center[1]); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + + selectionCircle.setCenter(centerCoord); + selectionCircle.setRadius(0); + selectionCircle.setVisible(true); + + var currentPoint; + var meterDistance; + + d3.select(window) + .on('mousemove', function() { + var m = d3.mouse(frame); + + if (!options.view) { + return; + } + + currentPoint = new google.maps.Point(m[0], m[1]); + currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + meterDistance = google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord); + + selectionCircle.setRadius(meterDistance); + selectPoints(); + + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: meterDistance / 1600 + }; + options.view.trigger('querybuilder', evt); + }) + .on('mouseup', function() { + d3.select(window) + .on('mouseup', null) + .on('mousemove', null); + + if (selectionCircle.getRadius() === 0) { + selectionCircle.setVisible(false); + + d3.select(frame).selectAll('.marker circle') + .classed('selected', false); + + var evt = { + type: 'geo', + source: 'geo' + }; + options.view.trigger('querybuilder', evt); + return; + } + + evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: meterDistance / 1600 + }; + options.view.trigger('querybuilder', evt); + }); + } + // --- end chart setup --- + + function chart(selection) { + selection.each(function(data) { + var el = d3.select(this); + + if (!singleton.google) { + parallel({ timeoutMS: 5000 }, [ // 5 second timeout + // tasks + function(done) { + GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + done(null, g); + }); + } + ], + // calback + function(err, results) { + if (err) { + debug('Error: Google map could not be loaded, disabling feature', el); + // google map load timed out, disable geo feature for runtime remainder and reload + app.setFeature('Geo Minicharts', false); + options.view.parent.render(); + return; + } + singleton.google = results[0]; + chart.call(this, selection); + }); + return; + } + + var google = singleton.google; + + var bounds = new google.maps.LatLngBounds(); + _.each(data, function(d) { + var p = new google.maps.LatLng(d[1], d[0]); + bounds.extend(p); + }); + + if (!googleMap) { + // Create the Google Map + googleMap = new google.maps.Map(el.node(), { + disableDefaultUI: true, + // disableDoubleClickZoom: true, + scrollwheel: true, + draggable: true, + zoomControl: true, + mapTypeId: google.maps.MapTypeId.ROADMAP, + styles: mapStyle + }); + + // Add the container when the overlay is added to the map. + overlay = new google.maps.OverlayView(); + overlay.onAdd = function() { + d3.select(this.getPanes().overlayMouseTarget).append('div') + .attr('class', 'layer'); + }; // end overlay.onAdd + + // Draw each marker as a separate SVG element. + overlay.draw = function() { + var layer = d3.select('div.layer'); + projection = this.getProjection(); + var padding = 9; + + var marker = layer.selectAll('svg.marker') + .data(data) + .each(transform) // update existing markers + .enter().append('svg:svg') + .each(transform) + .attr('class', 'marker'); + + // Add a circle + marker.append('circle') + .attr('r', 4.5) + .attr('cx', padding) + .attr('cy', padding); + + function transform(d) { + var p = new google.maps.LatLng(d[1], d[0]); + p = projection.fromLatLngToDivPixel(p); + d.x = p.x; + d.y = p.y; + var self = d3.select(this); + self + .style('left', p.x - padding + 'px') + .style('top', p.y - padding + 'px'); + return self; + } + }; // end overlay.draw + + overlay.setMap(googleMap); + el.on('mousedown', startSelection); + + d3.select('body') + .on('keydown', onKeyDown) + .on('keyup', onKeyUp); + } // end if (!googleMap) ... + + // var innerWidth = width - margin.left - margin.right; + // var innerHeight = height - margin.top - margin.bottom; + + googleMap.fitBounds(bounds); + + if (!selectionCircle) { + selectionCircle = new google.maps.Circle({ + strokeColor: '#F68A1E', + strokeOpacity: 0.8, + strokeWeight: 2, + fillColor: '#F68A1E', + fillOpacity: 0.35, + map: googleMap, + center: { lat: 0, lng: 0 }, + radius: 0, + visible: false, + draggable: true + // editable: true + }); + + selectionCircle.addListener('drag', function() { + var centerCoord = selectionCircle.getCenter(); + selectPoints(); + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: selectionCircle.getRadius() / 1600 + }; + options.view.trigger('querybuilder', evt); + }); + } + + // need to fit the map bounds after view became visible + var fieldView = options.view.parent.parent; + if (fieldView.parent.parent.modelType === 'FieldView') { + // we're inside a nested list, wait until expanded + var parentFieldView = fieldView.parent.parent; + parentFieldView.on('change:expanded', function(view, value) { + if (value) { + // debug('field %s expanded. fitting map.', view.model.name); + _.defer(function() { + google.maps.event.trigger(googleMap, 'resize'); + googleMap.fitBounds(bounds); + }); + } + }); + } else { + debug('toplevel location field. fitting map.'); + _.defer(function() { + google.maps.event.trigger(googleMap, 'resize'); + googleMap.fitBounds(bounds); + }); + } + }); // end selection.each() + } + + chart.width = function(value) { + if (!arguments.length) { + return width; + } + width = value; + return chart; + }; + + chart.height = function(value) { + if (!arguments.length) { + return height; + } + height = value; + return chart; + }; + + chart.options = function(value) { + if (!arguments.length) { + return options; + } + _.assign(options, value); + return chart; + }; + + chart.geoSelection = function(value) { + if (!value) { + selectionCircle.setVisible(false); + selectionCircle.setRadius(0); + selectPoints(); + return; + } + selectionCircle.setVisible(true); + var c = new google.maps.LatLng(value[0][1], value[0][0]); + selectionCircle.setCenter(c); + selectionCircle.setRadius(value[1] * 1600); + selectPoints(); + } + + return chart; +}; + +module.exports = minicharts_d3fns_geo; diff --git a/src/minicharts/d3fns/index.js b/src/minicharts/d3fns/index.js index 6cfb44607ef..84b2222f0e3 100644 --- a/src/minicharts/d3fns/index.js +++ b/src/minicharts/d3fns/index.js @@ -3,5 +3,6 @@ module.exports = { boolean: require('./boolean'), date: require('./date'), string: require('./string'), - objectid: require('./date') + objectid: require('./date'), + geo: require('./geo') }; diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js new file mode 100644 index 00000000000..bee2a1cd02d --- /dev/null +++ b/src/minicharts/d3fns/mapstyle.js @@ -0,0 +1,89 @@ +module.exports = [ + { + featureType: 'administrative', + elementType: 'all', + stylers: [ + { + visibility: 'simplified' + } + ] + }, + { + featureType: 'landscape', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#fcfcfc' + } + ] + }, + { + featureType: 'poi', + elementType: 'all', + stylers: [ + { + visibility: 'off' + } + ] + }, + { + featureType: 'transit.station', + elementType: 'all', + stylers: [ + { + visibility: 'off' + } + ] + }, + { + featureType: 'road.highway', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + }, + { + featureType: 'road.arterial', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + }, + { + featureType: 'road.local', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#eeeeee' + } + ] + }, + { + featureType: 'water', + elementType: 'geometry', + stylers: [ + { + visibility: 'simplified' + }, + { + color: '#dddddd' + } + ] + } +] diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 4a4084deb4d..9f93e704daa 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -8,8 +8,14 @@ var DocumentRootMinichartView = require('./document-root'); var ArrayRootMinichartView = require('./array-root'); var vizFns = require('./d3fns'); var QueryBuilderMixin = require('./querybuilder'); +var Collection = require('ampersand-collection'); +var navigator = window.navigator; + // var debug = require('debug')('scout:minicharts:index'); +var ArrayCollection = Collection.extend({ + model: Array +}); /** * a wrapper around VizView to set common default values @@ -39,10 +45,67 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { }); this.listenTo(app.volatileQueryOptions, 'change:query', this.handleVolatileQueryChange); }, + _mangleGeoCoordinates: function(values) { + // now check value bounds + var lons = values.filter(function(val, idx) { + return idx % 2 === 0; + }); + var lats = values.filter(function(val, idx) { + return idx % 2 === 1; + }); + if (_.min(lons) >= -180 && _.max(lons) <= 180 && _.min(lats) >= -90 && _.max(lats) <= 90) { + // attach the zipped up coordinates to the model where VizView would expect it + return new ArrayCollection(_.zip(lons, lats)); + } + return false; + }, + /* eslint complexity: 0 */ + _geoCoordinateCheck: function() { + var coords; + if (!app.isFeatureEnabled('Geo Minicharts')) return false; + if (!navigator.onLine) return false; + + if (this.model.name === 'Document') { + if (this.model.fields.length !== 2 + || !this.model.fields.get('type') + || this.model.fields.get('type').type !== 'String' + || this.model.fields.get('type').types.get('String').unique !== 1 + || this.model.fields.get('type').types.get('String').values.at(0).value !== 'Point' + || this.model.fields.get('coordinates').types.get('Array').count + !== this.model.fields.get('coordinates').count + || this.model.fields.get('coordinates').types.get('Array').average_length !== 2 + ) return false; + coords = this._mangleGeoCoordinates( + this.model.fields.get('coordinates').types.get('Array') + .types.get('Number').values.serialize()); + if (!coords) return false; + // we have a GeoJSON document: {type: "Point", coordinates: [lng, lat]} + this.model.values = coords; + this.model.fields.reset(); + this.model.name = 'Coordinates'; + return true; + } else if (this.model.name === 'Array') { + var lengths = this.model.lengths; + if (_.min(lengths) !== 2 || _.max(lengths) !== 2) return false; + coords = this._mangleGeoCoordinates( + this.model.types.get('Number').values.serialize()); + if (!coords) return false; + // we have a legacy coordinate pair: [lng, lat] + this.model.values = coords; + this.model.types.reset(); + this.model.name = 'Coordinates'; + return true; + } + return false; + }, render: function() { this.renderWithTemplate(this); - - if (['String', 'Number'].indexOf(this.model.name) !== -1 + if (this._geoCoordinateCheck()) { + this.viewOptions.renderMode = 'html'; + this.viewOptions.height = 250; + this.viewOptions.vizFn = vizFns.geo; + this.subview = new VizView(this.viewOptions); + } else if (['String', 'Number'].indexOf(this.model.name) !== -1 && this.model.unique === this.model.count) { // unique values get a div-based UniqueMinichart this.viewOptions.renderMode = 'html'; @@ -50,18 +113,16 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.viewOptions.className = 'minichart unique'; this.subview = new UniqueMinichartView(this.viewOptions); } else if (this.model.name === 'Document') { - // nested objects get a div-based DocumentRootMinichart this.viewOptions.height = 55; this.subview = new DocumentRootMinichartView(this.viewOptions); } else if (this.model.name === 'Array') { - // arrays get a div-based ArrayRootMinichart this.viewOptions.height = 55; this.subview = new ArrayRootMinichartView(this.viewOptions); } else { // otherwise, create a svg-based VizView for d3 this.subview = new VizView(this.viewOptions); } - if (app.features.querybuilder) { + if (app.isFeatureEnabled('querybuilder')) { this.listenTo(this.subview, 'querybuilder', this.handleQueryBuilderEvent); } raf(function() { diff --git a/src/minicharts/index.less b/src/minicharts/index.less index 2a21921a586..76800559e70 100644 --- a/src/minicharts/index.less +++ b/src/minicharts/index.less @@ -78,6 +78,36 @@ div.minichart.unique { } } +.layer, .layer svg { + position: absolute; +} + +.layer svg.marker { + width: 20px; + height: 20px; + + circle { + fill: @mc-fg; + stroke: @pw; + stroke-width: 1.5px; + + &.selected { + fill: @mc-fg-selected; + } + } +} + +.layer svg.selection { + visibility: hidden; + + circle { + fill: @mc-fg-selected; + fill-opacity:0.2; + stroke: @mc-fg-selected; + stroke-width: 2px; + } +} + svg.minichart { font-size: 10px; diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index a1b54ca90c9..3b6d64808cf 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -6,7 +6,9 @@ var app = require('ampersand-app'); var LeafValue = require('mongodb-language-model').LeafValue; var LeafClause = require('mongodb-language-model').LeafClause; var ListOperator = require('mongodb-language-model').ListOperator; +var GeoOperator = require('mongodb-language-model').GeoOperator; var Range = require('mongodb-language-model').helpers.Range; + // var debug = require('debug')('scout:minicharts:querybuilder'); var MODIFIERKEY = 'shiftKey'; @@ -47,6 +49,7 @@ module.exports = { * } * */ + /* eslint complexity: 0 */ handleQueryBuilderEvent: function(data) { var queryType; @@ -58,7 +61,7 @@ module.exports = { // defocus currently active element (likely the refine bar) so it can update the query $(document.activeElement).blur(); - // determine what kind of query this is (distinct or range) + // determine what kind of query this is (distinct, range, geo, ...) switch (this.model.getType()) { case 'Boolean': // fall-through to String case 'String': @@ -73,7 +76,16 @@ module.exports = { break; case 'ObjectID': // fall-through to Date case 'Date': - queryType = 'range'; + queryType = 'range'; + break; + case 'Array': + case 'Document': + case 'Coordinates': + if (data.source === 'geo') { + queryType = 'geo'; + } else { + throw new Error('unsupported querybuilder type ' + this.model.getType()); + } break; default: // @todo other types not implemented yet throw new Error('unsupported querybuilder type ' + this.model.getType()); @@ -84,8 +96,8 @@ module.exports = { data: data }; - if (data.type === 'drag') { - message = this.updateSelection_drag(message); + if (data.type === 'drag' || data.type === 'geo') { + message = this['updateSelection_' + data.type](message); message = this['buildQuery_' + queryType](message); } else { message = this['updateSelection_' + queryType](message); @@ -172,6 +184,27 @@ module.exports = { return message; }, + /** + * updates `selected` for query builder events created on a geo map. + * The visual updates are handled by d3 directly, selected will just contain the center + * longitude, latitude and distance in miles. + * + * @param {Object} message message with key: data + * @return {Object} message message with keys: data, selected + */ + updateSelection_geo: function(message) { + if (!message.data.center || !message.data.distance) { + this.selectedValues = []; + } else { + this.selectedValues = [ + message.data.center[0], + message.data.center[1], + message.data.distance / 3963.2 // equatorial radius of earth in miles + ]; + } + message.selected = this.selectedValues; + return message; + }, /** * build new distinct ($in) query based on current selection * @@ -255,6 +288,34 @@ module.exports = { return message; }, + /** + * build new geo ($geoWithin) query based on current selection + * + * @param {Object} message message with keys: data, selected + * @return {Object} message message with keys: data, selected, value, elements, op + */ + buildQuery_geo: function(message) { + message.elements = this.queryAll('.selectable'); + message.op = '$geoWithin'; + + // build new value + if (message.selected.length === 0) { + // no value + message.value = null; + } else { + // multiple values + message.value = new GeoOperator({ + $geoWithin: { + $centerSphere: [ [message.selected[0], message.selected[1] ], message.selected[2] ] + } + }, { + parse: true + }); + } + return message; + }, + + /** * update the UI after a distinct query and mark appropriate elements with .select class. * @@ -357,6 +418,23 @@ module.exports = { } return message; }, + /** + * update the UI after a geo query was manipulated in the refine bar. + * + * @param {Object} message message with keys: data, selected, value + * @return {Object} message no changes on message, just pass it through for consistency + */ + updateUI_geo: function(message) { + if (this.selectedValues && this.selectedValues.length) { + // convert radius back to miles + var params = this.selectedValues.slice(); + params[1] *= 3963.2; + this.subview.chart.geoSelection(params); + } else { + this.subview.chart.geoSelection(null); + } + return message; + }, /** * Query Builder upwards pass * The user interacted with the minichart to build a query. We need to ask the volatile query @@ -423,10 +501,14 @@ module.exports = { if (!value || !value.valid) { this.selectedValues = []; - // updateUI_distinct will do the right thing here and clear any selection, - // even in the case where the minichart is a range type. message.selected = this.selectedValues; - this.updateUI_distinct(message); + if (_.has(this.subview.chart, 'geoSelection')) { + this.updateUI_geo(message); + } else { + // updateUI_distinct will do the right thing here and clear any selection, + // even in the case where the minichart is a range type. + this.updateUI_distinct(message); + } return; } if (value.className === 'LeafValue') { @@ -443,6 +525,14 @@ module.exports = { this.updateUI_distinct(message); return; } + var geoOperator = value.operators.get('$geoWithin'); + if (geoOperator) { + // case: $geoWithin query + this.selectedValues = geoOperator.shape.parameters; + message.selected = this.selectedValues; + this.updateUI_geo(message); + return; + } if (['$gt', '$lt', '$gte', '$lte'].indexOf(value.operators.at(0).operator) !== -1) { // case: range query this.selectedValues = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];