From 7118feceb6adcd78591576bf17e937bf4d89ddf2 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 17 Sep 2015 16:23:17 +0200 Subject: [PATCH 01/32] WIP first steps for adding a coordinate minichart. --- package.json | 3 +- src/minicharts/d3fns/coordinates.js | 79 +++++++++++++++++++++++++++++ src/minicharts/d3fns/index.js | 3 +- src/minicharts/index.js | 40 +++++++++++++-- 4 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 src/minicharts/d3fns/coordinates.js diff --git a/package.json b/package.json index ca22ed54db1..eeec5d634d1 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", diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js new file mode 100644 index 00000000000..78a8e86774c --- /dev/null +++ b/src/minicharts/d3fns/coordinates.js @@ -0,0 +1,79 @@ +var d3 = require('d3'); +var _ = require('lodash'); +var shared = require('./shared'); +var GoogleMapsLoader = require('google-maps'); + +var minicharts_d3fns_coordinates = function() { + // --- beginning chart setup --- + var width = 400; + var height = 100; + var options = { + view: null + }; + + var margin = shared.margin; + // --- end chart setup --- + + function chart(selection) { + selection.each(function(data) { + var el = d3.select(this); + var innerWidth = width - margin.left - margin.right; + var innerHeight = height - margin.top - margin.bottom; + + var lons = data.filter(function(val, idx) { + return idx % 2 === 0; + }); + var lats = data.filter(function(val, idx) { + return idx % 2 === 1; + }); + + var coords = _.zip(lons, lats); + + // Create the Google Map + GoogleMapsLoader.KEY = 'AIzaSyAZ7WUH271VlhhkX0gf0iVa58anGCZUtL0'; + GoogleMapsLoader.load(function(google) { + var map = new google.maps.Map(el.node(), { + zoom: 8, + center: new google.maps.LatLng(37.76487, -122.41948), + mapTypeId: google.maps.MapTypeId.TERRAIN + }); + }); + + // append g element if it doesn't exist yet + // div.enter() + // .append('g') + // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + // .attr('width', innerWidth) + // .attr('height', innerHeight); + + }); + } + + 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; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_coordinates; diff --git a/src/minicharts/d3fns/index.js b/src/minicharts/d3fns/index.js index 6cfb44607ef..9666b377e4b 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'), + coordinates: require('./coordinates') }; diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 4a4084deb4d..a1f6dc0f5ab 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -8,8 +8,12 @@ var DocumentRootMinichartView = require('./document-root'); var ArrayRootMinichartView = require('./array-root'); var vizFns = require('./d3fns'); var QueryBuilderMixin = require('./querybuilder'); -// var debug = require('debug')('scout:minicharts:index'); +var debug = require('debug')('scout:minicharts:index'); +var Collection = require('ampersand-collection'); +var ArrayCollection = Collection.extend({ + model: Array +}); /** * a wrapper around VizView to set common default values @@ -54,9 +58,37 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { 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); + var isCoordinates = false; + + // are these coordinates? Do a basic check for now, until we support semantic schema types + var lengths = this.model.lengths; + var coords; + if (_.min(lengths) === 2 && _.max(lengths) === 2) { + // now check value bounds + var values = this.model.types.get('Number').values.serialize(); + 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) { + isCoordinates = true; + // attach the zipped up coordinates to the model where VizView would expect it + this.model.values = new ArrayCollection(_.zip(lons, lats)); + debug('model.values', this.model.values); + } + } + if (isCoordinates) { + // coordinates get an HTML-based d3 VizView with `coordinates` vizFn + this.viewOptions.renderMode = 'html'; + this.viewOptions.vizFn = vizFns.coordinates; + this.subview = new VizView(this.viewOptions); + } else { + // plain 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); From c2065b8e6f4f8d505efb6c029436a1122a4ae4c5 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 18 Sep 2015 12:41:55 +0200 Subject: [PATCH 02/32] added google maps, no interaction yet --- src/index.jade | 2 +- src/minicharts/d3fns/coordinates.js | 94 +++++-- src/minicharts/d3fns/mapstyle.js | 363 ++++++++++++++++++++++++++++ src/minicharts/index.js | 1 + src/minicharts/index.less | 18 ++ 5 files changed, 454 insertions(+), 24 deletions(-) create mode 100644 src/minicharts/d3fns/mapstyle.js diff --git a/src/index.jade b/src/index.jade index c403c816e96..f3bf34b462b 100644 --- a/src/index.jade +++ b/src/index.jade @@ -2,7 +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' http://localhost:35729; style-src 'self' 'unsafe-inline';") meta(name='viewport', content='initial-scale=1') link(rel='stylesheet', href='index.css', charset='UTF-8') diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js index 78a8e86774c..0e86cc400b5 100644 --- a/src/minicharts/d3fns/coordinates.js +++ b/src/minicharts/d3fns/coordinates.js @@ -1,7 +1,9 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); +var debug = require('debug')('scout:minicharts:coordinates'); var GoogleMapsLoader = require('google-maps'); +var mapStyle = require('./mapstyle'); var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- @@ -17,35 +19,81 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { var el = d3.select(this); - var innerWidth = width - margin.left - margin.right; - var innerHeight = height - margin.top - margin.bottom; + // var innerWidth = width - margin.left - margin.right; + // var innerHeight = height - margin.top - margin.bottom; - var lons = data.filter(function(val, idx) { - return idx % 2 === 0; - }); - var lats = data.filter(function(val, idx) { - return idx % 2 === 1; - }); - - var coords = _.zip(lons, lats); - - // Create the Google Map - GoogleMapsLoader.KEY = 'AIzaSyAZ7WUH271VlhhkX0gf0iVa58anGCZUtL0'; + // set up the bounds GoogleMapsLoader.load(function(google) { + // compute map bounds from all coordinates + var bounds = new google.maps.LatLngBounds(); + _.each(data, function(coord) { + var p = new google.maps.LatLng(coord[1], coord[0]); + bounds.extend(p); + }); + + // Create the Google Map var map = new google.maps.Map(el.node(), { - zoom: 8, - center: new google.maps.LatLng(37.76487, -122.41948), - mapTypeId: google.maps.MapTypeId.TERRAIN + disableDefaultUI: true, + mapTypeId: google.maps.MapTypeId.ROADMAP, + styles: mapStyle }); - }); + map.fitBounds(bounds); + + var overlay = new google.maps.OverlayView(); - // append g element if it doesn't exist yet - // div.enter() - // .append('g') - // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - // .attr('width', innerWidth) - // .attr('height', innerHeight); + // Add the container when the overlay is added to the map. + overlay.onAdd = function() { + var layer = d3.select(this.getPanes().overlayLayer).append('div') + .attr('class', 'coords'); + // Draw each marker as a separate SVG element. + // We could use a single SVG, but what size would it have? + overlay.draw = function() { + var projection = this.getProjection(); + var padding = 10; + + var marker = layer.selectAll('svg') + .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); + + // Add a label. + // marker.append('svg:text') + // .attr('x', padding + 7) + // .attr('y', padding) + // .attr('dy', '.31em') + // .text(function(d) { return d; }); + + function transform(d) { + d = new google.maps.LatLng(d[1], d[0]); + d = projection.fromLatLngToDivPixel(d); + var s = d3.select(this) + .style('left', d.x - padding + 'px') + .style('top', d.y - padding + 'px'); + debug('s', s); + return s; + } + }; + }; + + // append g element if it doesn't exist yet + // div.enter() + // .append('g') + // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + // .attr('width', innerWidth) + // .attr('height', innerHeight); + + // Bind our overlay to the map… + overlay.setMap(map); + }); }); } diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js new file mode 100644 index 00000000000..c5aaf0feebd --- /dev/null +++ b/src/minicharts/d3fns/mapstyle.js @@ -0,0 +1,363 @@ +/*eslint quote-props: 0*/ +module.exports = [ + { + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [ + { + 'saturation': 36 + }, + { + 'color': '#333333' + }, + { + 'lightness': 40 + } + ] + }, + { + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#ffffff' + }, + { + 'lightness': 16 + } + ] + }, + { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [ + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'administrative', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'administrative', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'color': '#fefefe' + }, + { + 'lightness': 20 + } + ] + }, + { + 'featureType': 'administrative', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'color': '#fefefe' + }, + { + 'lightness': 17 + }, + { + 'weight': 1.2 + } + ] + }, + { + 'featureType': 'landscape', + 'elementType': 'geometry', + 'stylers': [ + { + 'lightness': 20 + }, + { + 'color': '#ececec' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#f0f0ef' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#f0f0ef' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#d4d4d4' + } + ] + }, + { + 'featureType': 'landscape.natural', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#ececec' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'geometry', + 'stylers': [ + { + 'lightness': 21 + }, + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#d4d4d4' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'labels.text.fill', + 'stylers': [ + { + 'color': '#303030' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'labels.icon', + 'stylers': [ + { + 'saturation': '-100' + } + ] + }, + { + 'featureType': 'poi.attraction', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.business', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.government', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.medical', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.park', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.park', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#dedede' + }, + { + 'lightness': 21 + } + ] + }, + { + 'featureType': 'poi.place_of_worship', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.school', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.school', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'lightness': '-61' + }, + { + 'gamma': '0.00' + }, + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'poi.sports_complex', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'road.highway', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 17 + } + ] + }, + { + 'featureType': 'road.highway', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 29 + }, + { + 'weight': 0.2 + } + ] + }, + { + 'featureType': 'road.arterial', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 18 + } + ] + }, + { + 'featureType': 'road.local', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 16 + } + ] + }, + { + 'featureType': 'transit', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#f2f2f2' + }, + { + 'lightness': 19 + } + ] + }, + { + 'featureType': 'water', + 'elementType': 'geometry', + 'stylers': [ + { + 'visibility': 'simplified' + }, + { + 'saturation': -60 + } + ] + } +]; diff --git a/src/minicharts/index.js b/src/minicharts/index.js index a1f6dc0f5ab..61c94ad206c 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -82,6 +82,7 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { if (isCoordinates) { // coordinates get an HTML-based d3 VizView with `coordinates` vizFn this.viewOptions.renderMode = 'html'; + this.viewOptions.height = 250; this.viewOptions.vizFn = vizFns.coordinates; this.subview = new VizView(this.viewOptions); } else { diff --git a/src/minicharts/index.less b/src/minicharts/index.less index 2a21921a586..0fd2f0d949c 100644 --- a/src/minicharts/index.less +++ b/src/minicharts/index.less @@ -78,6 +78,24 @@ div.minichart.unique { } } +.coords, .coords svg { + position: absolute; +} + +.coords svg { + width: 60px; + height: 20px; + padding-right: 100px; + font: 10px sans-serif; +} + +.coords circle { + fill: @mc-fg; + stroke: @pw; + stroke-width: 1.5px; +} + + svg.minichart { font-size: 10px; From 25a0dbfec59de208336de659d1ad197b12ce092e Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 23 Sep 2015 14:44:51 +0200 Subject: [PATCH 03/32] selection circle working --- src/minicharts/d3fns/coordinates.js | 179 ++++++++++++++++++++-------- src/minicharts/index.less | 32 +++-- 2 files changed, 152 insertions(+), 59 deletions(-) diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js index 0e86cc400b5..92b52367e6d 100644 --- a/src/minicharts/d3fns/coordinates.js +++ b/src/minicharts/d3fns/coordinates.js @@ -9,92 +9,173 @@ var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- var width = 400; var height = 100; + + var google = null; + var googleMap = null; + var overlay = null; + var projection = null; + var options = { view: null }; var margin = shared.margin; + + function startSelection() { + var frame = this; + var center = d3.mouse(frame); + var radius = 0; + var padding = 2; + + var selectionSvg = d3.select('svg.selection'); + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)) + .style('visibility', 'visible'); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); + + debug('start selection', d3.mouse(frame)); + + d3.select(window) + .on('mousemove', function() { + var m = d3.mouse(frame); + var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); + radius = Math.sqrt(radius_sqr); + + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); + + d3.select(frame).selectAll('.marker circle') + .classed('selected', function(d) { + return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; + }); + }) + .on('mouseup', function() { + d3.select(window) + .on('mouseup', null) + .on('mousemove', null); + + if (radius === 0) { + selectionSvg + .style('visibility', 'hidden'); + d3.select(frame).selectAll('.marker circle') + .classed('selected', false); + return; + } + + var m = d3.mouse(frame); + var currentPoint = new google.maps.Point(m[0], m[1]); + var centerPoint = new google.maps.Point(center[0], center[1]); + var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600).toFixed(2); + }); + } // --- end chart setup --- function chart(selection) { selection.each(function(data) { + if (!google) { + // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + google = g; + chart.call(this, selection); + }); + return; + } + var el = d3.select(this); - // var innerWidth = width - margin.left - margin.right; - // var innerHeight = height - margin.top - margin.bottom; + var bounds = new google.maps.LatLngBounds(); + _.each(data, function(d) { + var p = new google.maps.LatLng(d[1], d[0]); + bounds.extend(p); + }); - // set up the bounds - GoogleMapsLoader.load(function(google) { - // compute map bounds from all coordinates - var bounds = new google.maps.LatLngBounds(); - _.each(data, function(coord) { - var p = new google.maps.LatLng(coord[1], coord[0]); - bounds.extend(p); - }); + if (!googleMap) { + el.on('mousedown', startSelection); // Create the Google Map - var map = new google.maps.Map(el.node(), { + googleMap = new google.maps.Map(el.node(), { disableDefaultUI: true, - mapTypeId: google.maps.MapTypeId.ROADMAP, - styles: mapStyle + disableDoubleClickZoom: true, + scrollwheel: true, + draggable: false, + panControl: false, + mapTypeId: google.maps.MapTypeId.ROADMAP + // styles: mapStyle }); - map.fitBounds(bounds); - - var overlay = new google.maps.OverlayView(); // Add the container when the overlay is added to the map. + overlay = new google.maps.OverlayView(); overlay.onAdd = function() { - var layer = d3.select(this.getPanes().overlayLayer).append('div') - .attr('class', 'coords'); + var layer = d3.select(this.getPanes().overlayMouseTarget).append('div') + .attr('class', 'layer'); // Draw each marker as a separate SVG element. // We could use a single SVG, but what size would it have? overlay.draw = function() { - var projection = this.getProjection(); - var padding = 10; + projection = this.getProjection(); + var padding = 9; - var marker = layer.selectAll('svg') + 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. + // Add a circle marker.append('circle') .attr('r', 4.5) .attr('cx', padding) .attr('cy', padding); - // Add a label. - // marker.append('svg:text') - // .attr('x', padding + 7) - // .attr('y', padding) - // .attr('dy', '.31em') - // .text(function(d) { return d; }); + // add selection circle (hidden by default) + var selectionSvg = layer.selectAll('svg.selection') + .data([null]) + .enter().append('svg:svg') + .attr('class', 'selection'); + + selectionSvg.append('circle') + .attr('r', 50) + .attr('cx', 50) + .attr('cy', 50); function transform(d) { - d = new google.maps.LatLng(d[1], d[0]); - d = projection.fromLatLngToDivPixel(d); - var s = d3.select(this) - .style('left', d.x - padding + 'px') - .style('top', d.y - padding + 'px'); - debug('s', s); - return s; + var p = new google.maps.LatLng(d[1], d[0]); + p = projection.fromLatLngToDivPixel(p); + d.x = p.x; + d.y = p.y; + return d3.select(this) + .style('left', p.x - padding + 'px') + .style('top', p.y - padding + 'px'); } - }; - }; - - // append g element if it doesn't exist yet - // div.enter() - // .append('g') - // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - // .attr('width', innerWidth) - // .attr('height', innerHeight); - - // Bind our overlay to the map… - overlay.setMap(map); - }); - }); + }; // end overlay.draw + }; // end overlay.onAdd + overlay.setMap(googleMap); + } // end if (!googleMap) ... + + // var innerWidth = width - margin.left - margin.right; + // var innerHeight = height - margin.top - margin.bottom; + + googleMap.fitBounds(bounds); + }); // end selection.each() } chart.width = function(value) { diff --git a/src/minicharts/index.less b/src/minicharts/index.less index 0fd2f0d949c..76800559e70 100644 --- a/src/minicharts/index.less +++ b/src/minicharts/index.less @@ -78,23 +78,35 @@ div.minichart.unique { } } -.coords, .coords svg { +.layer, .layer svg { position: absolute; } -.coords svg { - width: 60px; +.layer svg.marker { + width: 20px; height: 20px; - padding-right: 100px; - font: 10px sans-serif; -} -.coords circle { - fill: @mc-fg; - stroke: @pw; - stroke-width: 1.5px; + 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; From bf07dac7c8ff865b378123285192fe4f99d5f827 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 6 Oct 2015 22:16:40 +0200 Subject: [PATCH 04/32] mostly name changes --- .../d3fns/{coordinates.js => geo.js} | 29 ++++++++- src/minicharts/d3fns/index.js | 2 +- src/minicharts/index.js | 9 ++- src/minicharts/querybuilder.js | 62 +++++++++++++++++-- 4 files changed, 90 insertions(+), 12 deletions(-) rename src/minicharts/d3fns/{coordinates.js => geo.js} (86%) diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/geo.js similarity index 86% rename from src/minicharts/d3fns/coordinates.js rename to src/minicharts/d3fns/geo.js index 92b52367e6d..3864e17c182 100644 --- a/src/minicharts/d3fns/coordinates.js +++ b/src/minicharts/d3fns/geo.js @@ -40,8 +40,6 @@ var minicharts_d3fns_coordinates = function() { .attr('cx', radius + padding) .attr('cy', radius + padding); - debug('start selection', d3.mouse(frame)); - d3.select(window) .on('mousemove', function() { var m = d3.mouse(frame); @@ -63,6 +61,25 @@ var minicharts_d3fns_coordinates = function() { .classed('selected', function(d) { return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; }); + + if (!options.view) { + return; + } + + var currentPoint = new google.maps.Point(m[0], m[1]); + var centerPoint = new google.maps.Point(center[0], center[1]); + var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600).toFixed(2); + + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: mileDistance + }; + options.view.trigger('querybuilder', evt); }) .on('mouseup', function() { d3.select(window) @@ -84,6 +101,14 @@ var minicharts_d3fns_coordinates = function() { var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( centerCoord, currentCoord) / 1600).toFixed(2); + + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: mileDistance + }; + options.view.trigger('querybuilder', evt); }); } // --- end chart setup --- diff --git a/src/minicharts/d3fns/index.js b/src/minicharts/d3fns/index.js index 9666b377e4b..84b2222f0e3 100644 --- a/src/minicharts/d3fns/index.js +++ b/src/minicharts/d3fns/index.js @@ -4,5 +4,5 @@ module.exports = { date: require('./date'), string: require('./string'), objectid: require('./date'), - coordinates: require('./coordinates') + geo: require('./geo') }; diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 61c94ad206c..8e0e78525e9 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -58,11 +58,10 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.viewOptions.height = 55; this.subview = new DocumentRootMinichartView(this.viewOptions); } else if (this.model.name === 'Array') { - var isCoordinates = false; + var isGeo = false; // are these coordinates? Do a basic check for now, until we support semantic schema types var lengths = this.model.lengths; - var coords; if (_.min(lengths) === 2 && _.max(lengths) === 2) { // now check value bounds var values = this.model.types.get('Number').values.serialize(); @@ -73,17 +72,17 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { return idx % 2 === 1; }); if (_.min(lons) >= -180 && _.max(lons) <= 180 && _.min(lats) >= -90 && _.max(lats) <= 90) { - isCoordinates = true; + isGeo = true; // attach the zipped up coordinates to the model where VizView would expect it this.model.values = new ArrayCollection(_.zip(lons, lats)); debug('model.values', this.model.values); } } - if (isCoordinates) { + if (isGeo) { // coordinates get an HTML-based d3 VizView with `coordinates` vizFn this.viewOptions.renderMode = 'html'; this.viewOptions.height = 250; - this.viewOptions.vizFn = vizFns.coordinates; + this.viewOptions.vizFn = vizFns.geo; this.subview = new VizView(this.viewOptions); } else { // plain arrays get a div-based ArrayRootMinichart diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index a1b54ca90c9..5c3435a3627 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -6,6 +6,7 @@ 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 OperatorObject = require('mongodb-language-model').OperatorObject; var Range = require('mongodb-language-model').helpers.Range; // var debug = require('debug')('scout:minicharts:querybuilder'); @@ -58,7 +59,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 +74,15 @@ module.exports = { break; case 'ObjectID': // fall-through to Date case 'Date': - queryType = 'range'; + queryType = 'range'; + break; + case 'Array': + case 'Document': + 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 +93,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 +181,23 @@ 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) { + this.selectedValues = [ + message.data.center[0], + message.data.center[1], + message.distance + ]; + message.selected = this.selectedValues; + return message; + }, /** * build new distinct ($in) query based on current selection * @@ -255,6 +281,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 OperatorObject({ + $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. * From a980ca15c6637090667b288b351f5fb0e009ed9b Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 6 Oct 2015 17:48:49 -0400 Subject: [PATCH 05/32] geo queries are created in the query bar. no backwards pass yet. --- src/minicharts/d3fns/geo.js | 13 +++++++++---- src/minicharts/querybuilder.js | 20 ++++++++++++-------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 3864e17c182..13d8c54ad03 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -70,8 +70,8 @@ var minicharts_d3fns_coordinates = function() { var centerPoint = new google.maps.Point(center[0], center[1]); var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600).toFixed(2); + var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600; var evt = { type: 'geo', @@ -91,6 +91,11 @@ var minicharts_d3fns_coordinates = function() { .style('visibility', 'hidden'); d3.select(frame).selectAll('.marker circle') .classed('selected', false); + var evt = { + type: 'geo', + source: 'geo' + }; + options.view.trigger('querybuilder', evt); return; } @@ -99,8 +104,8 @@ var minicharts_d3fns_coordinates = function() { var centerPoint = new google.maps.Point(center[0], center[1]); var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600).toFixed(2); + var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600; var evt = { type: 'geo', diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index 5c3435a3627..f0639bfee3d 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -6,9 +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 OperatorObject = require('mongodb-language-model').OperatorObject; +var GeoOperator = require('mongodb-language-model').GeoOperator; var Range = require('mongodb-language-model').helpers.Range; -// var debug = require('debug')('scout:minicharts:querybuilder'); +var debug = require('debug')('scout:minicharts:querybuilder'); var MODIFIERKEY = 'shiftKey'; var checkBounds = { @@ -190,11 +190,15 @@ module.exports = { * @return {Object} message message with keys: data, selected */ updateSelection_geo: function(message) { - this.selectedValues = [ - message.data.center[0], - message.data.center[1], - message.distance - ]; + 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; }, @@ -297,7 +301,7 @@ module.exports = { message.value = null; } else { // multiple values - message.value = new OperatorObject({ + message.value = new GeoOperator({ $geoWithin: { $centerSphere: [ [message.selected[0], message.selected[1] ], message.selected[2] ] } From e3c9a3f3ef520cc56619b9e1d79466e3e45502eb Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 7 Oct 2015 10:25:39 -0400 Subject: [PATCH 06/32] map style, enabling controls (for now) --- src/minicharts/d3fns/geo.js | 8 +- src/minicharts/d3fns/mapstyle.js | 350 ++++--------------------------- 2 files changed, 39 insertions(+), 319 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 13d8c54ad03..9f696ce8811 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -107,7 +107,7 @@ var minicharts_d3fns_coordinates = function() { var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( centerCoord, currentCoord) / 1600; - var evt = { + evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], @@ -142,13 +142,13 @@ var minicharts_d3fns_coordinates = function() { // Create the Google Map googleMap = new google.maps.Map(el.node(), { - disableDefaultUI: true, + disableDefaultUI: false, disableDoubleClickZoom: true, scrollwheel: true, draggable: false, panControl: false, - mapTypeId: google.maps.MapTypeId.ROADMAP - // styles: mapStyle + mapTypeId: google.maps.MapTypeId.ROADMAP, + styles: mapStyle }); // Add the container when the overlay is added to the map. diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js index c5aaf0feebd..f58ffb5030f 100644 --- a/src/minicharts/d3fns/mapstyle.js +++ b/src/minicharts/d3fns/mapstyle.js @@ -1,363 +1,83 @@ -/*eslint quote-props: 0*/ module.exports = [ { - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [ + featureType: 'administrative', + elementType: 'all', + stylers: [ { - 'saturation': 36 - }, - { - 'color': '#333333' - }, - { - 'lightness': 40 + visibility: 'simplified' } ] }, { - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [ - { - 'visibility': 'on' - }, + featureType: 'landscape', + elementType: 'geometry', + stylers: [ { - 'color': '#ffffff' + visibility: 'simplified' }, { - 'lightness': 16 - } - ] - }, - { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [ - { - 'visibility': 'off' + color: '#fcfcfc' } ] }, { - 'featureType': 'administrative', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'administrative', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'color': '#fefefe' - }, - { - 'lightness': 20 - } - ] - }, - { - 'featureType': 'administrative', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'color': '#fefefe' - }, - { - 'lightness': 17 - }, - { - 'weight': 1.2 - } - ] - }, - { - 'featureType': 'landscape', - 'elementType': 'geometry', - 'stylers': [ - { - 'lightness': 20 - }, - { - 'color': '#ececec' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#f0f0ef' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#f0f0ef' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#d4d4d4' - } - ] - }, - { - 'featureType': 'landscape.natural', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#ececec' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'geometry', - 'stylers': [ - { - 'lightness': 21 - }, - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#d4d4d4' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'labels.text.fill', - 'stylers': [ - { - 'color': '#303030' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'labels.icon', - 'stylers': [ - { - 'saturation': '-100' - } - ] - }, - { - 'featureType': 'poi.attraction', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.business', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.government', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.medical', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.park', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.park', - 'elementType': 'geometry', - 'stylers': [ - { - 'color': '#dedede' - }, - { - 'lightness': 21 - } - ] - }, - { - 'featureType': 'poi.place_of_worship', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.school', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.school', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'lightness': '-61' - }, - { - 'gamma': '0.00' - }, - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'poi.sports_complex', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'road.highway', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'color': '#ffffff' - }, - { - 'lightness': 17 - } - ] - }, - { - 'featureType': 'road.highway', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'color': '#ffffff' - }, + featureType: 'poi', + elementType: 'geometry', + stylers: [ { - 'lightness': 29 + visibility: 'simplified' }, { - 'weight': 0.2 + color: '#fcfcfc' } ] }, { - 'featureType': 'road.arterial', - 'elementType': 'geometry', - 'stylers': [ + featureType: 'road.highway', + elementType: 'geometry', + stylers: [ { - 'color': '#ffffff' + visibility: 'simplified' }, { - 'lightness': 18 + color: '#dddddd' } ] }, { - 'featureType': 'road.local', - 'elementType': 'geometry', - 'stylers': [ + featureType: 'road.arterial', + elementType: 'geometry', + stylers: [ { - 'color': '#ffffff' + visibility: 'simplified' }, { - 'lightness': 16 + color: '#dddddd' } ] }, { - 'featureType': 'transit', - 'elementType': 'geometry', - 'stylers': [ + featureType: 'road.local', + elementType: 'geometry', + stylers: [ { - 'color': '#f2f2f2' + visibility: 'simplified' }, { - 'lightness': 19 + color: '#eeeeee' } ] }, { - 'featureType': 'water', - 'elementType': 'geometry', - 'stylers': [ + featureType: 'water', + elementType: 'geometry', + stylers: [ { - 'visibility': 'simplified' + visibility: 'simplified' }, { - 'saturation': -60 + color: '#dddddd' } ] } -]; +] From 9612732cb4aa140368fa31fbf08c7412bbeed3da Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 7 Oct 2015 13:36:51 -0400 Subject: [PATCH 07/32] debug message when starting to drag map --- src/minicharts/d3fns/geo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 9f696ce8811..559864e5746 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -205,6 +205,10 @@ var minicharts_d3fns_coordinates = function() { // var innerHeight = height - margin.top - margin.bottom; googleMap.fitBounds(bounds); + + googleMap.addListener('dragstart', function() { + debug('drag start'); + }); }); // end selection.each() } From 5bf4f7f5dac55403456fcab19e7a3c0bcc3d2cdf Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 11:17:06 -0400 Subject: [PATCH 08/32] use google maps Circle class instead of d3 automatic dragging, projection, panning... --- src/minicharts/d3fns/geo.js | 257 +++++++++++++++++++++--------------- 1 file changed, 154 insertions(+), 103 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 559864e5746..c5d1f3c637b 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,19 +1,40 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:coordinates'); +var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); +var Singleton = (function() { + var instance; + + function createInstance() { + var object = {}; + return object; + } + + return { + getInstance: function() { + if (!instance) { + instance = createInstance(); + } + return instance; + } + }; +})(); + +var singleton = Singleton.getInstance(); + var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- var width = 400; var height = 100; - var google = null; var googleMap = null; var overlay = null; var projection = null; + var selectionCircle; + var currentCoord; var options = { view: null @@ -21,63 +42,73 @@ var minicharts_d3fns_coordinates = function() { var margin = shared.margin; + function pointInCircle(point, radius, center) { + return singleton.google.maps.geometry.spherical.computeDistanceBetween(point, center) <= radius; + } + + function selectPoints(frame) { + 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 startSelection() { + if (!d3.event.shiftKey) { + return; + } + + var google = singleton.google; + var frame = this; var center = d3.mouse(frame); - var radius = 0; - var padding = 2; - - var selectionSvg = d3.select('svg.selection'); - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)) - .style('visibility', 'visible'); - - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); + + // 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); - var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); - radius = Math.sqrt(radius_sqr); - - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)); - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); - - d3.select(frame).selectAll('.marker circle') - .classed('selected', function(d) { - return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; - }); + // d3.select(frame).selectAll('.marker circle') + // .classed('selected', function(d) { + // return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; + // }); if (!options.view) { return; } - var currentPoint = new google.maps.Point(m[0], m[1]); - var centerPoint = new google.maps.Point(center[0], center[1]); - var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); - var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600; + 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(frame); var evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: mileDistance + distance: meterDistance / 1600 }; options.view.trigger('querybuilder', evt); }) @@ -86,11 +117,12 @@ var minicharts_d3fns_coordinates = function() { .on('mouseup', null) .on('mousemove', null); - if (radius === 0) { - selectionSvg - .style('visibility', 'hidden'); + if (selectionCircle.getRadius() === 0) { + selectionCircle.setVisible(false); + d3.select(frame).selectAll('.marker circle') .classed('selected', false); + var evt = { type: 'geo', source: 'geo' @@ -99,19 +131,11 @@ var minicharts_d3fns_coordinates = function() { return; } - var m = d3.mouse(frame); - var currentPoint = new google.maps.Point(m[0], m[1]); - var centerPoint = new google.maps.Point(center[0], center[1]); - var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); - var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600; - evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: mileDistance + distance: meterDistance / 1600 }; options.view.trigger('querybuilder', evt); }); @@ -120,16 +144,18 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { - if (!google) { + if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { - google = g; + singleton.google = g; chart.call(this, selection); }); return; } + var google = singleton.google; + var el = d3.select(this); var bounds = new google.maps.LatLngBounds(); _.each(data, function(d) { @@ -138,15 +164,13 @@ var minicharts_d3fns_coordinates = function() { }); if (!googleMap) { - el.on('mousedown', startSelection); - // Create the Google Map googleMap = new google.maps.Map(el.node(), { - disableDefaultUI: false, - disableDoubleClickZoom: true, - scrollwheel: true, + // disableDefaultUI: false, + // disableDoubleClickZoom: true, + // scrollwheel: true, draggable: false, - panControl: false, + panControl: true, mapTypeId: google.maps.MapTypeId.ROADMAP, styles: mapStyle }); @@ -154,51 +178,53 @@ var minicharts_d3fns_coordinates = function() { // Add the container when the overlay is added to the map. overlay = new google.maps.OverlayView(); overlay.onAdd = function() { - var layer = d3.select(this.getPanes().overlayMouseTarget).append('div') + d3.select(this.getPanes().overlayMouseTarget).append('div') .attr('class', 'layer'); - - // Draw each marker as a separate SVG element. - // We could use a single SVG, but what size would it have? - overlay.draw = function() { - 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); - - // add selection circle (hidden by default) - var selectionSvg = layer.selectAll('svg.selection') - .data([null]) - .enter().append('svg:svg') - .attr('class', 'selection'); - - selectionSvg.append('circle') - .attr('r', 50) - .attr('cx', 50) - .attr('cy', 50); - - 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; - return d3.select(this) - .style('left', p.x - padding + 'px') - .style('top', p.y - padding + 'px'); - } - }; // end overlay.draw }; // 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; + } + + // function transformRadius(d) { + // var p = projection.fromLatLngToDivPixel(currentCoord); + // debug('transformRadius', currentCoord, p); + // if (!p) return d3.select(this); + // var r = Math.sqrt(Math.pow(d.x - p.x, 2) + Math.pow(d.y - p.y, 2)); + // debug('radius', r); + // return d3.select(this).attr('r', r); + // } + }; // end overlay.draw + overlay.setMap(googleMap); + el.on('mousedown', startSelection); } // end if (!googleMap) ... // var innerWidth = width - margin.left - margin.right; @@ -206,6 +232,31 @@ var minicharts_d3fns_coordinates = function() { googleMap.fitBounds(bounds); + 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 + }); + + selectionCircle.addListener('drag', function() { + var centerCoord = selectionCircle.getCenter(); + selectPoints(el.node()); + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: selectionCircle.getRadius() / 1600 + }; + options.view.trigger('querybuilder', evt); + }); + googleMap.addListener('dragstart', function() { debug('drag start'); }); From 6e9918cc17a5845968b60131b1ad9c0fd866dfa4 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 11:47:12 -0400 Subject: [PATCH 09/32] feature flag for geo minicharts --- src/app.js | 3 +-- src/minicharts/d3fns/geo.js | 2 ++ src/minicharts/index.js | 37 +++++++++++++++++++------------------ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/app.js b/src/app.js index 6b0b393d2a6..5d266a6e170 100644 --- a/src/app.js +++ b/src/app.js @@ -237,6 +237,7 @@ var state = new Application({ 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 +246,6 @@ var FEATURES = { app.extend({ client: null, - // @note (imlucas): Backwards compat for querybuilder - features: FEATURES, /** * Check whether a feature flag is currently enabled. * diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index c5d1f3c637b..c275f14bb6a 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -144,6 +144,7 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { + // debugger; if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; @@ -260,6 +261,7 @@ var minicharts_d3fns_coordinates = function() { googleMap.addListener('dragstart', function() { debug('drag start'); }); + google.maps.event.trigger(googleMap, 'resize'); }); // end selection.each() } diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 8e0e78525e9..b33e81a9d16 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -59,23 +59,24 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.subview = new DocumentRootMinichartView(this.viewOptions); } else if (this.model.name === 'Array') { var isGeo = false; - - // are these coordinates? Do a basic check for now, until we support semantic schema types - var lengths = this.model.lengths; - if (_.min(lengths) === 2 && _.max(lengths) === 2) { - // now check value bounds - var values = this.model.types.get('Number').values.serialize(); - 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) { - isGeo = true; - // attach the zipped up coordinates to the model where VizView would expect it - this.model.values = new ArrayCollection(_.zip(lons, lats)); - debug('model.values', this.model.values); + if (app.isFeatureEnabled('Geo Minicharts')) { + // are these coordinates? Do a basic check for now, until we support semantic schema types + var lengths = this.model.lengths; + if (_.min(lengths) === 2 && _.max(lengths) === 2) { + // now check value bounds + var values = this.model.types.get('Number').values.serialize(); + 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) { + isGeo = true; + // attach the zipped up coordinates to the model where VizView would expect it + this.model.values = new ArrayCollection(_.zip(lons, lats)); + debug('model.values', this.model.values); + } } } if (isGeo) { @@ -93,7 +94,7 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { // 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() { From e89be93a89722cf13c1f017cb8798b4b9733440f Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 13:27:54 -0400 Subject: [PATCH 10/32] defer map resizing after stack has cleared. fixes second load issues. --- src/minicharts/d3fns/geo.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index c275f14bb6a..1c84469ffe9 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -25,7 +25,7 @@ var Singleton = (function() { var singleton = Singleton.getInstance(); -var minicharts_d3fns_coordinates = function() { +var minicharts_d3fns_geo = function() { // --- beginning chart setup --- var width = 400; var height = 100; @@ -144,7 +144,6 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { - // debugger; if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; @@ -261,7 +260,10 @@ var minicharts_d3fns_coordinates = function() { googleMap.addListener('dragstart', function() { debug('drag start'); }); - google.maps.event.trigger(googleMap, 'resize'); + _.defer(function() { + google.maps.event.trigger(googleMap, 'resize'); + googleMap.fitBounds(bounds); + }, 100); }); // end selection.each() } @@ -292,4 +294,4 @@ var minicharts_d3fns_coordinates = function() { return chart; }; -module.exports = minicharts_d3fns_coordinates; +module.exports = minicharts_d3fns_geo; From 876b0f4072a5901f7d8c361d4bb37e6330397102 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 15:27:19 -0400 Subject: [PATCH 11/32] shiftkey handling, language model bump, geojson --- src/minicharts/d3fns/geo.js | 29 +++++++++++---- src/minicharts/index.js | 70 +++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 1c84469ffe9..52d27b1d7b0 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -4,6 +4,7 @@ var shared = require('./shared'); var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); +var SHIFTKEY = 16; var Singleton = (function() { var instance; @@ -62,6 +63,20 @@ var minicharts_d3fns_geo = function() { }); } + 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; @@ -166,11 +181,11 @@ var minicharts_d3fns_geo = function() { if (!googleMap) { // Create the Google Map googleMap = new google.maps.Map(el.node(), { - // disableDefaultUI: false, + disableDefaultUI: true, // disableDoubleClickZoom: true, - // scrollwheel: true, - draggable: false, - panControl: true, + scrollwheel: true, + draggable: true, + zoomControl: true, mapTypeId: google.maps.MapTypeId.ROADMAP, styles: mapStyle }); @@ -257,14 +272,14 @@ var minicharts_d3fns_geo = function() { options.view.trigger('querybuilder', evt); }); - googleMap.addListener('dragstart', function() { - debug('drag start'); - }); _.defer(function() { google.maps.event.trigger(googleMap, 'resize'); googleMap.fitBounds(bounds); }, 100); }); // end selection.each() + d3.select('body') + .on('keydown', onKeyDown) + .on('keyup', onKeyUp); } chart.width = function(value) { diff --git a/src/minicharts/index.js b/src/minicharts/index.js index b33e81a9d16..35b27883dc5 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -43,6 +43,22 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { }); this.listenTo(app.volatileQueryOptions, 'change:query', this.handleVolatileQueryChange); }, + _mangleGeoCoordinates: function(values) { + debug('mangle values', 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; + }, render: function() { this.renderWithTemplate(this); @@ -54,28 +70,52 @@ 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); + // are these coordinates? Do a basic check for now, until we support semantic schema types + // here we check for GeoJSON form: { loc: {type: "Point", "coordinates": [47.80, 9.63] } } + var isGeo = false; + if (app.isFeatureEnabled('Geo Minicharts')) { + 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 + ) { + var coords =this._mangleGeoCoordinates( + this.model.fields.get('coordinates').types.get('Array') + .types.get('Number').values.serialize()); + if (coords) { + this.model.values = coords; + this.model.fields.reset(); + isGeo = true; + } + } + } + if (isGeo) { + // coordinates get an HTML-based d3 VizView with `coordinates` vizFn + this.viewOptions.renderMode = 'html'; + this.viewOptions.height = 250; + this.viewOptions.vizFn = vizFns.geo; + this.subview = new VizView(this.viewOptions); + } else { + // nested objects get a div-based DocumentRootMinichart + this.viewOptions.height = 55; + this.subview = new DocumentRootMinichartView(this.viewOptions); + } } else if (this.model.name === 'Array') { var isGeo = false; if (app.isFeatureEnabled('Geo Minicharts')) { // are these coordinates? Do a basic check for now, until we support semantic schema types + // here we check for legacy coordinates in array form: { loc: [47.80, 9.63] } var lengths = this.model.lengths; if (_.min(lengths) === 2 && _.max(lengths) === 2) { - // now check value bounds - var values = this.model.types.get('Number').values.serialize(); - 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) { + var coords = this._mangleGeoCoordinates( + this.model.types.get('Number').values.serialize()); + if (coords) { + this.model.values = coords; isGeo = true; - // attach the zipped up coordinates to the model where VizView would expect it - this.model.values = new ArrayCollection(_.zip(lons, lats)); - debug('model.values', this.model.values); } } } From da655fa458c412cd6550d4f93cf511640c48ed05 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 23:28:24 -0400 Subject: [PATCH 12/32] geo backwards pass implemented. --- src/minicharts/d3fns/geo.js | 93 ++++++++++++++++++---------------- src/minicharts/querybuilder.js | 35 +++++++++++-- 2 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 52d27b1d7b0..ef81cf7d295 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,7 +1,7 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:geo'); +// var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; @@ -47,7 +47,8 @@ var minicharts_d3fns_geo = function() { return singleton.google.maps.geometry.spherical.computeDistanceBetween(point, center) <= radius; } - function selectPoints(frame) { + function selectPoints() { + var frame = options.el; var google = singleton.google; if (selectionCircle.getRadius() === 0) { @@ -102,11 +103,6 @@ var minicharts_d3fns_geo = function() { .on('mousemove', function() { var m = d3.mouse(frame); - // d3.select(frame).selectAll('.marker circle') - // .classed('selected', function(d) { - // return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; - // }); - if (!options.view) { return; } @@ -117,7 +113,7 @@ var minicharts_d3fns_geo = function() { centerCoord, currentCoord); selectionCircle.setRadius(meterDistance); - selectPoints(frame); + selectPoints(); var evt = { type: 'geo', @@ -160,7 +156,7 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { if (!singleton.google) { - // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { singleton.google = g; @@ -227,19 +223,14 @@ var minicharts_d3fns_geo = function() { .style('top', p.y - padding + 'px'); return self; } - - // function transformRadius(d) { - // var p = projection.fromLatLngToDivPixel(currentCoord); - // debug('transformRadius', currentCoord, p); - // if (!p) return d3.select(this); - // var r = Math.sqrt(Math.pow(d.x - p.x, 2) + Math.pow(d.y - p.y, 2)); - // debug('radius', r); - // return d3.select(this).attr('r', r); - // } }; // 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; @@ -247,39 +238,39 @@ var minicharts_d3fns_geo = function() { googleMap.fitBounds(bounds); - 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 - }); + 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(el.node()); - var evt = { - type: 'geo', - source: 'geo', - center: [centerCoord.lng(), centerCoord.lat()], - distance: selectionCircle.getRadius() / 1600 - }; - options.view.trigger('querybuilder', evt); - }); + 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); + }); + } _.defer(function() { google.maps.event.trigger(googleMap, 'resize'); googleMap.fitBounds(bounds); }, 100); }); // end selection.each() - d3.select('body') - .on('keydown', onKeyDown) - .on('keyup', onKeyUp); } chart.width = function(value) { @@ -306,6 +297,20 @@ var minicharts_d3fns_geo = function() { 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; }; diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index f0639bfee3d..b7c070f1fcc 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -415,6 +415,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 @@ -481,10 +498,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') { @@ -501,6 +522,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]; From 89ca12d4490cd5b8dd53215d89774edc792d459c Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 23:46:09 -0400 Subject: [PATCH 13/32] update security policy, fix eslint errors --- src/index.jade | 2 +- src/minicharts/d3fns/geo.js | 2 ++ src/minicharts/index.js | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.jade b/src/index.jade index f3bf34b462b..5d6f975a268 100644 --- a/src/index.jade +++ b/src/index.jade @@ -2,7 +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://maps.googleapis.com https://maps.gstatic.com http://localhost:35729 https://mts0.googleapis.com https://mts1.googleapis.com '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') diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index ef81cf7d295..77550ccbbe4 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -6,6 +6,7 @@ var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; +/* eslint wrap-iife:0 */ var Singleton = (function() { var instance; @@ -156,6 +157,7 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { if (!singleton.google) { + // @todo: replace with corporate api key GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 35b27883dc5..6d188114c17 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -44,7 +44,6 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.listenTo(app.volatileQueryOptions, 'change:query', this.handleVolatileQueryChange); }, _mangleGeoCoordinates: function(values) { - debug('mangle values', values) // now check value bounds var lons = values.filter(function(val, idx) { return idx % 2 === 0; @@ -53,7 +52,6 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { 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)); } From 2c4d0cbd699c45233ac053e4c045f57ddbb36e6c Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 15 Oct 2015 15:55:44 -0400 Subject: [PATCH 14/32] detect google map loading timeouts and disable --- src/app.js | 10 ++++++ src/minicharts/d3fns/geo.js | 66 +++++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/app.js b/src/app.js index 5d266a6e170..b6fad81b703 100644 --- a/src/app.js +++ b/src/app.js @@ -255,6 +255,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/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 77550ccbbe4..dab9fb8a5db 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,10 +1,13 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -// var debug = require('debug')('scout:minicharts:geo'); +var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; +var async = require('async'); +var app = require('ampersand-app'); + /* eslint wrap-iife:0 */ var Singleton = (function() { @@ -25,6 +28,38 @@ var Singleton = (function() { }; })(); +// 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() { @@ -156,12 +191,30 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { + var el = d3.select(this); + if (!singleton.google) { - // @todo: replace with corporate api key - GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; - GoogleMapsLoader.LIBRARIES = ['geometry']; - GoogleMapsLoader.load(function(g) { - singleton.google = g; + parallel({ timeoutMS: 3000 }, [ // 10 second timeout + // tasks + function(done) { + GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + done(null, g); + }); + } + ], + // calback + function(err, results) { + debug('err/res', 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; @@ -169,7 +222,6 @@ var minicharts_d3fns_geo = function() { var google = singleton.google; - var el = d3.select(this); var bounds = new google.maps.LatLngBounds(); _.each(data, function(d) { var p = new google.maps.LatLng(d[1], d[0]); From 231efe93305f7456d3b5fa558ed0718e1c20583f Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 17 Sep 2015 16:23:17 +0200 Subject: [PATCH 15/32] WIP first steps for adding a coordinate minichart. --- src/minicharts/d3fns/coordinates.js | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/minicharts/d3fns/coordinates.js diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js new file mode 100644 index 00000000000..78a8e86774c --- /dev/null +++ b/src/minicharts/d3fns/coordinates.js @@ -0,0 +1,79 @@ +var d3 = require('d3'); +var _ = require('lodash'); +var shared = require('./shared'); +var GoogleMapsLoader = require('google-maps'); + +var minicharts_d3fns_coordinates = function() { + // --- beginning chart setup --- + var width = 400; + var height = 100; + var options = { + view: null + }; + + var margin = shared.margin; + // --- end chart setup --- + + function chart(selection) { + selection.each(function(data) { + var el = d3.select(this); + var innerWidth = width - margin.left - margin.right; + var innerHeight = height - margin.top - margin.bottom; + + var lons = data.filter(function(val, idx) { + return idx % 2 === 0; + }); + var lats = data.filter(function(val, idx) { + return idx % 2 === 1; + }); + + var coords = _.zip(lons, lats); + + // Create the Google Map + GoogleMapsLoader.KEY = 'AIzaSyAZ7WUH271VlhhkX0gf0iVa58anGCZUtL0'; + GoogleMapsLoader.load(function(google) { + var map = new google.maps.Map(el.node(), { + zoom: 8, + center: new google.maps.LatLng(37.76487, -122.41948), + mapTypeId: google.maps.MapTypeId.TERRAIN + }); + }); + + // append g element if it doesn't exist yet + // div.enter() + // .append('g') + // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + // .attr('width', innerWidth) + // .attr('height', innerHeight); + + }); + } + + 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; + }; + + return chart; +}; + +module.exports = minicharts_d3fns_coordinates; From 412f98798a1710477bd04aa99d23322b5f6c8d26 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 18 Sep 2015 12:41:55 +0200 Subject: [PATCH 16/32] added google maps, no interaction yet --- src/index.jade | 1 - src/minicharts/d3fns/coordinates.js | 94 ++++++-- src/minicharts/d3fns/mapstyle.js | 360 ++++++++++++++++++++++++++++ 3 files changed, 431 insertions(+), 24 deletions(-) diff --git a/src/index.jade b/src/index.jade index 5d6f975a268..e38955fff48 100644 --- a/src/index.jade +++ b/src/index.jade @@ -3,7 +3,6 @@ html(lang='en') head title MongoDB meta(http-equiv="Content-Security-Policy", content="default-src *; script-src 'self' https://maps.googleapis.com https://maps.gstatic.com http://localhost:35729 https://mts0.googleapis.com https://mts1.googleapis.com '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/coordinates.js b/src/minicharts/d3fns/coordinates.js index 78a8e86774c..0e86cc400b5 100644 --- a/src/minicharts/d3fns/coordinates.js +++ b/src/minicharts/d3fns/coordinates.js @@ -1,7 +1,9 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); +var debug = require('debug')('scout:minicharts:coordinates'); var GoogleMapsLoader = require('google-maps'); +var mapStyle = require('./mapstyle'); var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- @@ -17,35 +19,81 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { var el = d3.select(this); - var innerWidth = width - margin.left - margin.right; - var innerHeight = height - margin.top - margin.bottom; + // var innerWidth = width - margin.left - margin.right; + // var innerHeight = height - margin.top - margin.bottom; - var lons = data.filter(function(val, idx) { - return idx % 2 === 0; - }); - var lats = data.filter(function(val, idx) { - return idx % 2 === 1; - }); - - var coords = _.zip(lons, lats); - - // Create the Google Map - GoogleMapsLoader.KEY = 'AIzaSyAZ7WUH271VlhhkX0gf0iVa58anGCZUtL0'; + // set up the bounds GoogleMapsLoader.load(function(google) { + // compute map bounds from all coordinates + var bounds = new google.maps.LatLngBounds(); + _.each(data, function(coord) { + var p = new google.maps.LatLng(coord[1], coord[0]); + bounds.extend(p); + }); + + // Create the Google Map var map = new google.maps.Map(el.node(), { - zoom: 8, - center: new google.maps.LatLng(37.76487, -122.41948), - mapTypeId: google.maps.MapTypeId.TERRAIN + disableDefaultUI: true, + mapTypeId: google.maps.MapTypeId.ROADMAP, + styles: mapStyle }); - }); + map.fitBounds(bounds); + + var overlay = new google.maps.OverlayView(); - // append g element if it doesn't exist yet - // div.enter() - // .append('g') - // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - // .attr('width', innerWidth) - // .attr('height', innerHeight); + // Add the container when the overlay is added to the map. + overlay.onAdd = function() { + var layer = d3.select(this.getPanes().overlayLayer).append('div') + .attr('class', 'coords'); + // Draw each marker as a separate SVG element. + // We could use a single SVG, but what size would it have? + overlay.draw = function() { + var projection = this.getProjection(); + var padding = 10; + + var marker = layer.selectAll('svg') + .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); + + // Add a label. + // marker.append('svg:text') + // .attr('x', padding + 7) + // .attr('y', padding) + // .attr('dy', '.31em') + // .text(function(d) { return d; }); + + function transform(d) { + d = new google.maps.LatLng(d[1], d[0]); + d = projection.fromLatLngToDivPixel(d); + var s = d3.select(this) + .style('left', d.x - padding + 'px') + .style('top', d.y - padding + 'px'); + debug('s', s); + return s; + } + }; + }; + + // append g element if it doesn't exist yet + // div.enter() + // .append('g') + // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + // .attr('width', innerWidth) + // .attr('height', innerHeight); + + // Bind our overlay to the map… + overlay.setMap(map); + }); }); } diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js index f58ffb5030f..27a2fb21823 100644 --- a/src/minicharts/d3fns/mapstyle.js +++ b/src/minicharts/d3fns/mapstyle.js @@ -1,3 +1,4 @@ +<<<<<<< HEAD module.exports = [ { featureType: 'administrative', @@ -5,10 +6,27 @@ module.exports = [ stylers: [ { visibility: 'simplified' +======= +/*eslint quote-props: 0*/ +module.exports = [ + { + 'featureType': 'all', + 'elementType': 'labels.text.fill', + 'stylers': [ + { + 'saturation': 36 + }, + { + 'color': '#333333' + }, + { + 'lightness': 40 +>>>>>>> added google maps, no interaction yet } ] }, { +<<<<<<< HEAD featureType: 'landscape', elementType: 'geometry', stylers: [ @@ -17,10 +35,33 @@ module.exports = [ }, { color: '#fcfcfc' +======= + 'featureType': 'all', + 'elementType': 'labels.text.stroke', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#ffffff' + }, + { + 'lightness': 16 } ] }, { + 'featureType': 'all', + 'elementType': 'labels.icon', + 'stylers': [ + { + 'visibility': 'off' +>>>>>>> added google maps, no interaction yet + } + ] + }, + { +<<<<<<< HEAD featureType: 'poi', elementType: 'geometry', stylers: [ @@ -29,10 +70,282 @@ module.exports = [ }, { color: '#fcfcfc' +======= + 'featureType': 'administrative', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'administrative', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'color': '#fefefe' + }, + { + 'lightness': 20 + } + ] + }, + { + 'featureType': 'administrative', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'color': '#fefefe' + }, + { + 'lightness': 17 + }, + { + 'weight': 1.2 + } + ] + }, + { + 'featureType': 'landscape', + 'elementType': 'geometry', + 'stylers': [ + { + 'lightness': 20 + }, + { + 'color': '#ececec' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#f0f0ef' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#f0f0ef' + } + ] + }, + { + 'featureType': 'landscape.man_made', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#d4d4d4' + } + ] + }, + { + 'featureType': 'landscape.natural', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#ececec' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'geometry', + 'stylers': [ + { + 'lightness': 21 + }, + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'visibility': 'on' + }, + { + 'color': '#d4d4d4' + } + ] + }, + { + 'featureType': 'poi', + 'elementType': 'labels.text.fill', + 'stylers': [ + { + 'color': '#303030' } ] }, { + 'featureType': 'poi', + 'elementType': 'labels.icon', + 'stylers': [ + { + 'saturation': '-100' + } + ] + }, + { + 'featureType': 'poi.attraction', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.business', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.government', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.medical', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.park', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.park', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#dedede' + }, + { + 'lightness': 21 + } + ] + }, + { + 'featureType': 'poi.place_of_worship', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.school', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'poi.school', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'lightness': '-61' + }, + { + 'gamma': '0.00' + }, + { + 'visibility': 'off' + } + ] + }, + { + 'featureType': 'poi.sports_complex', + 'elementType': 'all', + 'stylers': [ + { + 'visibility': 'on' + } + ] + }, + { + 'featureType': 'road.highway', + 'elementType': 'geometry.fill', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 17 + } + ] + }, + { + 'featureType': 'road.highway', + 'elementType': 'geometry.stroke', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 29 + }, + { + 'weight': 0.2 +>>>>>>> added google maps, no interaction yet + } + ] + }, + { +<<<<<<< HEAD featureType: 'road.highway', elementType: 'geometry', stylers: [ @@ -41,10 +354,21 @@ module.exports = [ }, { color: '#dddddd' +======= + 'featureType': 'road.arterial', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 18 +>>>>>>> added google maps, no interaction yet } ] }, { +<<<<<<< HEAD featureType: 'road.arterial', elementType: 'geometry', stylers: [ @@ -53,10 +377,21 @@ module.exports = [ }, { color: '#dddddd' +======= + 'featureType': 'road.local', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#ffffff' + }, + { + 'lightness': 16 +>>>>>>> added google maps, no interaction yet } ] }, { +<<<<<<< HEAD featureType: 'road.local', elementType: 'geometry', stylers: [ @@ -65,10 +400,21 @@ module.exports = [ }, { color: '#eeeeee' +======= + 'featureType': 'transit', + 'elementType': 'geometry', + 'stylers': [ + { + 'color': '#f2f2f2' + }, + { + 'lightness': 19 +>>>>>>> added google maps, no interaction yet } ] }, { +<<<<<<< HEAD featureType: 'water', elementType: 'geometry', stylers: [ @@ -81,3 +427,17 @@ module.exports = [ ] } ] +======= + 'featureType': 'water', + 'elementType': 'geometry', + 'stylers': [ + { + 'visibility': 'simplified' + }, + { + 'saturation': -60 + } + ] + } +]; +>>>>>>> added google maps, no interaction yet From 486c9e3b933804c80a84cfb9c7e7f05fd94fa2d9 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 23 Sep 2015 14:44:51 +0200 Subject: [PATCH 17/32] selection circle working --- src/minicharts/d3fns/coordinates.js | 179 ++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 49 deletions(-) diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js index 0e86cc400b5..92b52367e6d 100644 --- a/src/minicharts/d3fns/coordinates.js +++ b/src/minicharts/d3fns/coordinates.js @@ -9,92 +9,173 @@ var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- var width = 400; var height = 100; + + var google = null; + var googleMap = null; + var overlay = null; + var projection = null; + var options = { view: null }; var margin = shared.margin; + + function startSelection() { + var frame = this; + var center = d3.mouse(frame); + var radius = 0; + var padding = 2; + + var selectionSvg = d3.select('svg.selection'); + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)) + .style('visibility', 'visible'); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); + + debug('start selection', d3.mouse(frame)); + + d3.select(window) + .on('mousemove', function() { + var m = d3.mouse(frame); + var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); + radius = Math.sqrt(radius_sqr); + + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); + + d3.select(frame).selectAll('.marker circle') + .classed('selected', function(d) { + return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; + }); + }) + .on('mouseup', function() { + d3.select(window) + .on('mouseup', null) + .on('mousemove', null); + + if (radius === 0) { + selectionSvg + .style('visibility', 'hidden'); + d3.select(frame).selectAll('.marker circle') + .classed('selected', false); + return; + } + + var m = d3.mouse(frame); + var currentPoint = new google.maps.Point(m[0], m[1]); + var centerPoint = new google.maps.Point(center[0], center[1]); + var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600).toFixed(2); + }); + } // --- end chart setup --- function chart(selection) { selection.each(function(data) { + if (!google) { + // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + google = g; + chart.call(this, selection); + }); + return; + } + var el = d3.select(this); - // var innerWidth = width - margin.left - margin.right; - // var innerHeight = height - margin.top - margin.bottom; + var bounds = new google.maps.LatLngBounds(); + _.each(data, function(d) { + var p = new google.maps.LatLng(d[1], d[0]); + bounds.extend(p); + }); - // set up the bounds - GoogleMapsLoader.load(function(google) { - // compute map bounds from all coordinates - var bounds = new google.maps.LatLngBounds(); - _.each(data, function(coord) { - var p = new google.maps.LatLng(coord[1], coord[0]); - bounds.extend(p); - }); + if (!googleMap) { + el.on('mousedown', startSelection); // Create the Google Map - var map = new google.maps.Map(el.node(), { + googleMap = new google.maps.Map(el.node(), { disableDefaultUI: true, - mapTypeId: google.maps.MapTypeId.ROADMAP, - styles: mapStyle + disableDoubleClickZoom: true, + scrollwheel: true, + draggable: false, + panControl: false, + mapTypeId: google.maps.MapTypeId.ROADMAP + // styles: mapStyle }); - map.fitBounds(bounds); - - var overlay = new google.maps.OverlayView(); // Add the container when the overlay is added to the map. + overlay = new google.maps.OverlayView(); overlay.onAdd = function() { - var layer = d3.select(this.getPanes().overlayLayer).append('div') - .attr('class', 'coords'); + var layer = d3.select(this.getPanes().overlayMouseTarget).append('div') + .attr('class', 'layer'); // Draw each marker as a separate SVG element. // We could use a single SVG, but what size would it have? overlay.draw = function() { - var projection = this.getProjection(); - var padding = 10; + projection = this.getProjection(); + var padding = 9; - var marker = layer.selectAll('svg') + 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. + // Add a circle marker.append('circle') .attr('r', 4.5) .attr('cx', padding) .attr('cy', padding); - // Add a label. - // marker.append('svg:text') - // .attr('x', padding + 7) - // .attr('y', padding) - // .attr('dy', '.31em') - // .text(function(d) { return d; }); + // add selection circle (hidden by default) + var selectionSvg = layer.selectAll('svg.selection') + .data([null]) + .enter().append('svg:svg') + .attr('class', 'selection'); + + selectionSvg.append('circle') + .attr('r', 50) + .attr('cx', 50) + .attr('cy', 50); function transform(d) { - d = new google.maps.LatLng(d[1], d[0]); - d = projection.fromLatLngToDivPixel(d); - var s = d3.select(this) - .style('left', d.x - padding + 'px') - .style('top', d.y - padding + 'px'); - debug('s', s); - return s; + var p = new google.maps.LatLng(d[1], d[0]); + p = projection.fromLatLngToDivPixel(p); + d.x = p.x; + d.y = p.y; + return d3.select(this) + .style('left', p.x - padding + 'px') + .style('top', p.y - padding + 'px'); } - }; - }; - - // append g element if it doesn't exist yet - // div.enter() - // .append('g') - // .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - // .attr('width', innerWidth) - // .attr('height', innerHeight); - - // Bind our overlay to the map… - overlay.setMap(map); - }); - }); + }; // end overlay.draw + }; // end overlay.onAdd + overlay.setMap(googleMap); + } // end if (!googleMap) ... + + // var innerWidth = width - margin.left - margin.right; + // var innerHeight = height - margin.top - margin.bottom; + + googleMap.fitBounds(bounds); + }); // end selection.each() } chart.width = function(value) { From b472ab720bc96765a687ee05209b726113d74d11 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 6 Oct 2015 22:16:40 +0200 Subject: [PATCH 18/32] mostly name changes --- src/minicharts/d3fns/coordinates.js | 208 ---------------- src/minicharts/d3fns/geo.js | 362 +++++++++------------------- 2 files changed, 112 insertions(+), 458 deletions(-) delete mode 100644 src/minicharts/d3fns/coordinates.js diff --git a/src/minicharts/d3fns/coordinates.js b/src/minicharts/d3fns/coordinates.js deleted file mode 100644 index 92b52367e6d..00000000000 --- a/src/minicharts/d3fns/coordinates.js +++ /dev/null @@ -1,208 +0,0 @@ -var d3 = require('d3'); -var _ = require('lodash'); -var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:coordinates'); -var GoogleMapsLoader = require('google-maps'); -var mapStyle = require('./mapstyle'); - -var minicharts_d3fns_coordinates = function() { - // --- beginning chart setup --- - var width = 400; - var height = 100; - - var google = null; - var googleMap = null; - var overlay = null; - var projection = null; - - var options = { - view: null - }; - - var margin = shared.margin; - - function startSelection() { - var frame = this; - var center = d3.mouse(frame); - var radius = 0; - var padding = 2; - - var selectionSvg = d3.select('svg.selection'); - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)) - .style('visibility', 'visible'); - - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); - - debug('start selection', d3.mouse(frame)); - - d3.select(window) - .on('mousemove', function() { - var m = d3.mouse(frame); - var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); - radius = Math.sqrt(radius_sqr); - - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)); - - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); - - d3.select(frame).selectAll('.marker circle') - .classed('selected', function(d) { - return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; - }); - }) - .on('mouseup', function() { - d3.select(window) - .on('mouseup', null) - .on('mousemove', null); - - if (radius === 0) { - selectionSvg - .style('visibility', 'hidden'); - d3.select(frame).selectAll('.marker circle') - .classed('selected', false); - return; - } - - var m = d3.mouse(frame); - var currentPoint = new google.maps.Point(m[0], m[1]); - var centerPoint = new google.maps.Point(center[0], center[1]); - var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); - var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600).toFixed(2); - }); - } - // --- end chart setup --- - - function chart(selection) { - selection.each(function(data) { - if (!google) { - // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; - GoogleMapsLoader.LIBRARIES = ['geometry']; - GoogleMapsLoader.load(function(g) { - google = g; - chart.call(this, selection); - }); - return; - } - - var el = d3.select(this); - 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) { - el.on('mousedown', startSelection); - - // Create the Google Map - googleMap = new google.maps.Map(el.node(), { - disableDefaultUI: true, - disableDoubleClickZoom: true, - scrollwheel: true, - draggable: false, - panControl: false, - 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() { - var layer = d3.select(this.getPanes().overlayMouseTarget).append('div') - .attr('class', 'layer'); - - // Draw each marker as a separate SVG element. - // We could use a single SVG, but what size would it have? - overlay.draw = function() { - 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); - - // add selection circle (hidden by default) - var selectionSvg = layer.selectAll('svg.selection') - .data([null]) - .enter().append('svg:svg') - .attr('class', 'selection'); - - selectionSvg.append('circle') - .attr('r', 50) - .attr('cx', 50) - .attr('cy', 50); - - 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; - return d3.select(this) - .style('left', p.x - padding + 'px') - .style('top', p.y - padding + 'px'); - } - }; // end overlay.draw - }; // end overlay.onAdd - overlay.setMap(googleMap); - } // end if (!googleMap) ... - - // var innerWidth = width - margin.left - margin.right; - // var innerHeight = height - margin.top - margin.bottom; - - 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; - }; - - return chart; -}; - -module.exports = minicharts_d3fns_coordinates; diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index dab9fb8a5db..3864e17c182 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,77 +1,19 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:geo'); +var debug = require('debug')('scout:minicharts:coordinates'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); -var SHIFTKEY = 16; -var async = require('async'); -var app = require('ampersand-app'); - -/* 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() { +var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- var width = 400; var height = 100; + var google = null; var googleMap = null; var overlay = null; var projection = null; - var selectionCircle; - var currentCoord; var options = { view: null @@ -79,83 +21,63 @@ var minicharts_d3fns_geo = function() { 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; + var radius = 0; + var padding = 2; + + var selectionSvg = d3.select('svg.selection'); + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)) + .style('visibility', 'visible'); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); d3.select(window) .on('mousemove', function() { var m = d3.mouse(frame); + var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); + radius = Math.sqrt(radius_sqr); + + selectionSvg + .style('left', center[0] - radius - padding + 'px') + .style('top', center[1] - radius - padding + 'px') + .style('width', 2 * (radius + padding)) + .style('height', 2 * (radius + padding)); + + selectionSvg.select('circle') + .attr('r', radius) + .attr('cx', radius + padding) + .attr('cy', radius + padding); + + d3.select(frame).selectAll('.marker circle') + .classed('selected', function(d) { + return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; + }); 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 currentPoint = new google.maps.Point(m[0], m[1]); + var centerPoint = new google.maps.Point(center[0], center[1]); + var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600).toFixed(2); var evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: meterDistance / 1600 + distance: mileDistance }; options.view.trigger('querybuilder', evt); }) @@ -164,25 +86,27 @@ var minicharts_d3fns_geo = function() { .on('mouseup', null) .on('mousemove', null); - if (selectionCircle.getRadius() === 0) { - selectionCircle.setVisible(false); - + if (radius === 0) { + selectionSvg + .style('visibility', 'hidden'); d3.select(frame).selectAll('.marker circle') .classed('selected', false); - - var evt = { - type: 'geo', - source: 'geo' - }; - options.view.trigger('querybuilder', evt); return; } - evt = { + var m = d3.mouse(frame); + var currentPoint = new google.maps.Point(m[0], m[1]); + var centerPoint = new google.maps.Point(center[0], center[1]); + var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); + var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); + var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600).toFixed(2); + + var evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: meterDistance / 1600 + distance: mileDistance }; options.view.trigger('querybuilder', evt); }); @@ -191,37 +115,17 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { - var el = d3.select(this); - - if (!singleton.google) { - parallel({ timeoutMS: 3000 }, [ // 10 second timeout - // tasks - function(done) { - GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; - GoogleMapsLoader.LIBRARIES = ['geometry']; - GoogleMapsLoader.load(function(g) { - done(null, g); - }); - } - ], - // calback - function(err, results) { - debug('err/res', 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]; + if (!google) { + // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + google = g; chart.call(this, selection); }); return; } - var google = singleton.google; - + var el = d3.select(this); var bounds = new google.maps.LatLngBounds(); _.each(data, function(d) { var p = new google.maps.LatLng(d[1], d[0]); @@ -229,101 +133,73 @@ var minicharts_d3fns_geo = function() { }); if (!googleMap) { + el.on('mousedown', startSelection); + // Create the Google Map googleMap = new google.maps.Map(el.node(), { disableDefaultUI: true, - // disableDoubleClickZoom: true, + disableDoubleClickZoom: true, scrollwheel: true, - draggable: true, - zoomControl: true, - mapTypeId: google.maps.MapTypeId.ROADMAP, - styles: mapStyle + draggable: false, + panControl: false, + 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') + var layer = 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 + // Draw each marker as a separate SVG element. + // We could use a single SVG, but what size would it have? + overlay.draw = function() { + 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); + + // add selection circle (hidden by default) + var selectionSvg = layer.selectAll('svg.selection') + .data([null]) + .enter().append('svg:svg') + .attr('class', 'selection'); + + selectionSvg.append('circle') + .attr('r', 50) + .attr('cx', 50) + .attr('cy', 50); + + 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; + return d3.select(this) + .style('left', p.x - padding + 'px') + .style('top', p.y - padding + 'px'); + } + }; // end overlay.draw + }; // end overlay.onAdd 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); - }); - } - - _.defer(function() { - google.maps.event.trigger(googleMap, 'resize'); - googleMap.fitBounds(bounds); - }, 100); }); // end selection.each() } @@ -351,21 +227,7 @@ var minicharts_d3fns_geo = function() { 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; +module.exports = minicharts_d3fns_coordinates; From 57cfc7646a26c378e9eab87a850cbfb685a304da Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 6 Oct 2015 17:48:49 -0400 Subject: [PATCH 19/32] geo queries are created in the query bar. no backwards pass yet. --- src/minicharts/d3fns/geo.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 3864e17c182..13d8c54ad03 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -70,8 +70,8 @@ var minicharts_d3fns_coordinates = function() { var centerPoint = new google.maps.Point(center[0], center[1]); var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600).toFixed(2); + var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600; var evt = { type: 'geo', @@ -91,6 +91,11 @@ var minicharts_d3fns_coordinates = function() { .style('visibility', 'hidden'); d3.select(frame).selectAll('.marker circle') .classed('selected', false); + var evt = { + type: 'geo', + source: 'geo' + }; + options.view.trigger('querybuilder', evt); return; } @@ -99,8 +104,8 @@ var minicharts_d3fns_coordinates = function() { var centerPoint = new google.maps.Point(center[0], center[1]); var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = (google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600).toFixed(2); + var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( + centerCoord, currentCoord) / 1600; var evt = { type: 'geo', From 87068cfdd010fed1d8138ecea828b0ecdf55d040 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 7 Oct 2015 10:25:39 -0400 Subject: [PATCH 20/32] map style, enabling controls (for now) --- src/minicharts/d3fns/geo.js | 8 +- src/minicharts/d3fns/mapstyle.js | 360 ------------------------------- 2 files changed, 4 insertions(+), 364 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 13d8c54ad03..9f696ce8811 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -107,7 +107,7 @@ var minicharts_d3fns_coordinates = function() { var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( centerCoord, currentCoord) / 1600; - var evt = { + evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], @@ -142,13 +142,13 @@ var minicharts_d3fns_coordinates = function() { // Create the Google Map googleMap = new google.maps.Map(el.node(), { - disableDefaultUI: true, + disableDefaultUI: false, disableDoubleClickZoom: true, scrollwheel: true, draggable: false, panControl: false, - mapTypeId: google.maps.MapTypeId.ROADMAP - // styles: mapStyle + mapTypeId: google.maps.MapTypeId.ROADMAP, + styles: mapStyle }); // Add the container when the overlay is added to the map. diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js index 27a2fb21823..f58ffb5030f 100644 --- a/src/minicharts/d3fns/mapstyle.js +++ b/src/minicharts/d3fns/mapstyle.js @@ -1,4 +1,3 @@ -<<<<<<< HEAD module.exports = [ { featureType: 'administrative', @@ -6,27 +5,10 @@ module.exports = [ stylers: [ { visibility: 'simplified' -======= -/*eslint quote-props: 0*/ -module.exports = [ - { - 'featureType': 'all', - 'elementType': 'labels.text.fill', - 'stylers': [ - { - 'saturation': 36 - }, - { - 'color': '#333333' - }, - { - 'lightness': 40 ->>>>>>> added google maps, no interaction yet } ] }, { -<<<<<<< HEAD featureType: 'landscape', elementType: 'geometry', stylers: [ @@ -35,33 +17,10 @@ module.exports = [ }, { color: '#fcfcfc' -======= - 'featureType': 'all', - 'elementType': 'labels.text.stroke', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#ffffff' - }, - { - 'lightness': 16 } ] }, { - 'featureType': 'all', - 'elementType': 'labels.icon', - 'stylers': [ - { - 'visibility': 'off' ->>>>>>> added google maps, no interaction yet - } - ] - }, - { -<<<<<<< HEAD featureType: 'poi', elementType: 'geometry', stylers: [ @@ -70,282 +29,10 @@ module.exports = [ }, { color: '#fcfcfc' -======= - 'featureType': 'administrative', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'administrative', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'color': '#fefefe' - }, - { - 'lightness': 20 - } - ] - }, - { - 'featureType': 'administrative', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'color': '#fefefe' - }, - { - 'lightness': 17 - }, - { - 'weight': 1.2 - } - ] - }, - { - 'featureType': 'landscape', - 'elementType': 'geometry', - 'stylers': [ - { - 'lightness': 20 - }, - { - 'color': '#ececec' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#f0f0ef' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#f0f0ef' - } - ] - }, - { - 'featureType': 'landscape.man_made', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#d4d4d4' - } - ] - }, - { - 'featureType': 'landscape.natural', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#ececec' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'geometry', - 'stylers': [ - { - 'lightness': 21 - }, - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'visibility': 'on' - }, - { - 'color': '#d4d4d4' - } - ] - }, - { - 'featureType': 'poi', - 'elementType': 'labels.text.fill', - 'stylers': [ - { - 'color': '#303030' } ] }, { - 'featureType': 'poi', - 'elementType': 'labels.icon', - 'stylers': [ - { - 'saturation': '-100' - } - ] - }, - { - 'featureType': 'poi.attraction', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.business', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.government', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.medical', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.park', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.park', - 'elementType': 'geometry', - 'stylers': [ - { - 'color': '#dedede' - }, - { - 'lightness': 21 - } - ] - }, - { - 'featureType': 'poi.place_of_worship', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.school', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'poi.school', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'lightness': '-61' - }, - { - 'gamma': '0.00' - }, - { - 'visibility': 'off' - } - ] - }, - { - 'featureType': 'poi.sports_complex', - 'elementType': 'all', - 'stylers': [ - { - 'visibility': 'on' - } - ] - }, - { - 'featureType': 'road.highway', - 'elementType': 'geometry.fill', - 'stylers': [ - { - 'color': '#ffffff' - }, - { - 'lightness': 17 - } - ] - }, - { - 'featureType': 'road.highway', - 'elementType': 'geometry.stroke', - 'stylers': [ - { - 'color': '#ffffff' - }, - { - 'lightness': 29 - }, - { - 'weight': 0.2 ->>>>>>> added google maps, no interaction yet - } - ] - }, - { -<<<<<<< HEAD featureType: 'road.highway', elementType: 'geometry', stylers: [ @@ -354,21 +41,10 @@ module.exports = [ }, { color: '#dddddd' -======= - 'featureType': 'road.arterial', - 'elementType': 'geometry', - 'stylers': [ - { - 'color': '#ffffff' - }, - { - 'lightness': 18 ->>>>>>> added google maps, no interaction yet } ] }, { -<<<<<<< HEAD featureType: 'road.arterial', elementType: 'geometry', stylers: [ @@ -377,21 +53,10 @@ module.exports = [ }, { color: '#dddddd' -======= - 'featureType': 'road.local', - 'elementType': 'geometry', - 'stylers': [ - { - 'color': '#ffffff' - }, - { - 'lightness': 16 ->>>>>>> added google maps, no interaction yet } ] }, { -<<<<<<< HEAD featureType: 'road.local', elementType: 'geometry', stylers: [ @@ -400,21 +65,10 @@ module.exports = [ }, { color: '#eeeeee' -======= - 'featureType': 'transit', - 'elementType': 'geometry', - 'stylers': [ - { - 'color': '#f2f2f2' - }, - { - 'lightness': 19 ->>>>>>> added google maps, no interaction yet } ] }, { -<<<<<<< HEAD featureType: 'water', elementType: 'geometry', stylers: [ @@ -427,17 +81,3 @@ module.exports = [ ] } ] -======= - 'featureType': 'water', - 'elementType': 'geometry', - 'stylers': [ - { - 'visibility': 'simplified' - }, - { - 'saturation': -60 - } - ] - } -]; ->>>>>>> added google maps, no interaction yet From dcb3ce8f8f8b44d158c38bd9800f5493877b5260 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Wed, 7 Oct 2015 13:36:51 -0400 Subject: [PATCH 21/32] debug message when starting to drag map --- src/minicharts/d3fns/geo.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 9f696ce8811..559864e5746 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -205,6 +205,10 @@ var minicharts_d3fns_coordinates = function() { // var innerHeight = height - margin.top - margin.bottom; googleMap.fitBounds(bounds); + + googleMap.addListener('dragstart', function() { + debug('drag start'); + }); }); // end selection.each() } From f3ebd2259b689d1722a80b7f6f4904d486655e2e Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 11:17:06 -0400 Subject: [PATCH 22/32] use google maps Circle class instead of d3 automatic dragging, projection, panning... --- src/minicharts/d3fns/geo.js | 257 +++++++++++++++++++++--------------- 1 file changed, 154 insertions(+), 103 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 559864e5746..c5d1f3c637b 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,19 +1,40 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:coordinates'); +var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); +var Singleton = (function() { + var instance; + + function createInstance() { + var object = {}; + return object; + } + + return { + getInstance: function() { + if (!instance) { + instance = createInstance(); + } + return instance; + } + }; +})(); + +var singleton = Singleton.getInstance(); + var minicharts_d3fns_coordinates = function() { // --- beginning chart setup --- var width = 400; var height = 100; - var google = null; var googleMap = null; var overlay = null; var projection = null; + var selectionCircle; + var currentCoord; var options = { view: null @@ -21,63 +42,73 @@ var minicharts_d3fns_coordinates = function() { var margin = shared.margin; + function pointInCircle(point, radius, center) { + return singleton.google.maps.geometry.spherical.computeDistanceBetween(point, center) <= radius; + } + + function selectPoints(frame) { + 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 startSelection() { + if (!d3.event.shiftKey) { + return; + } + + var google = singleton.google; + var frame = this; var center = d3.mouse(frame); - var radius = 0; - var padding = 2; - - var selectionSvg = d3.select('svg.selection'); - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)) - .style('visibility', 'visible'); - - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); + + // 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); - var radius_sqr = Math.pow(m[0] - center[0], 2) + Math.pow(m[1] - center[1], 2); - radius = Math.sqrt(radius_sqr); - - selectionSvg - .style('left', center[0] - radius - padding + 'px') - .style('top', center[1] - radius - padding + 'px') - .style('width', 2 * (radius + padding)) - .style('height', 2 * (radius + padding)); - selectionSvg.select('circle') - .attr('r', radius) - .attr('cx', radius + padding) - .attr('cy', radius + padding); - - d3.select(frame).selectAll('.marker circle') - .classed('selected', function(d) { - return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; - }); + // d3.select(frame).selectAll('.marker circle') + // .classed('selected', function(d) { + // return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; + // }); if (!options.view) { return; } - var currentPoint = new google.maps.Point(m[0], m[1]); - var centerPoint = new google.maps.Point(center[0], center[1]); - var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); - var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600; + 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(frame); var evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: mileDistance + distance: meterDistance / 1600 }; options.view.trigger('querybuilder', evt); }) @@ -86,11 +117,12 @@ var minicharts_d3fns_coordinates = function() { .on('mouseup', null) .on('mousemove', null); - if (radius === 0) { - selectionSvg - .style('visibility', 'hidden'); + if (selectionCircle.getRadius() === 0) { + selectionCircle.setVisible(false); + d3.select(frame).selectAll('.marker circle') .classed('selected', false); + var evt = { type: 'geo', source: 'geo' @@ -99,19 +131,11 @@ var minicharts_d3fns_coordinates = function() { return; } - var m = d3.mouse(frame); - var currentPoint = new google.maps.Point(m[0], m[1]); - var centerPoint = new google.maps.Point(center[0], center[1]); - var currentCoord = projection.fromContainerPixelToLatLng(currentPoint); - var centerCoord = projection.fromContainerPixelToLatLng(centerPoint); - var mileDistance = google.maps.geometry.spherical.computeDistanceBetween( - centerCoord, currentCoord) / 1600; - evt = { type: 'geo', source: 'geo', center: [centerCoord.lng(), centerCoord.lat()], - distance: mileDistance + distance: meterDistance / 1600 }; options.view.trigger('querybuilder', evt); }); @@ -120,16 +144,18 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { - if (!google) { + if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { - google = g; + singleton.google = g; chart.call(this, selection); }); return; } + var google = singleton.google; + var el = d3.select(this); var bounds = new google.maps.LatLngBounds(); _.each(data, function(d) { @@ -138,15 +164,13 @@ var minicharts_d3fns_coordinates = function() { }); if (!googleMap) { - el.on('mousedown', startSelection); - // Create the Google Map googleMap = new google.maps.Map(el.node(), { - disableDefaultUI: false, - disableDoubleClickZoom: true, - scrollwheel: true, + // disableDefaultUI: false, + // disableDoubleClickZoom: true, + // scrollwheel: true, draggable: false, - panControl: false, + panControl: true, mapTypeId: google.maps.MapTypeId.ROADMAP, styles: mapStyle }); @@ -154,51 +178,53 @@ var minicharts_d3fns_coordinates = function() { // Add the container when the overlay is added to the map. overlay = new google.maps.OverlayView(); overlay.onAdd = function() { - var layer = d3.select(this.getPanes().overlayMouseTarget).append('div') + d3.select(this.getPanes().overlayMouseTarget).append('div') .attr('class', 'layer'); - - // Draw each marker as a separate SVG element. - // We could use a single SVG, but what size would it have? - overlay.draw = function() { - 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); - - // add selection circle (hidden by default) - var selectionSvg = layer.selectAll('svg.selection') - .data([null]) - .enter().append('svg:svg') - .attr('class', 'selection'); - - selectionSvg.append('circle') - .attr('r', 50) - .attr('cx', 50) - .attr('cy', 50); - - 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; - return d3.select(this) - .style('left', p.x - padding + 'px') - .style('top', p.y - padding + 'px'); - } - }; // end overlay.draw }; // 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; + } + + // function transformRadius(d) { + // var p = projection.fromLatLngToDivPixel(currentCoord); + // debug('transformRadius', currentCoord, p); + // if (!p) return d3.select(this); + // var r = Math.sqrt(Math.pow(d.x - p.x, 2) + Math.pow(d.y - p.y, 2)); + // debug('radius', r); + // return d3.select(this).attr('r', r); + // } + }; // end overlay.draw + overlay.setMap(googleMap); + el.on('mousedown', startSelection); } // end if (!googleMap) ... // var innerWidth = width - margin.left - margin.right; @@ -206,6 +232,31 @@ var minicharts_d3fns_coordinates = function() { googleMap.fitBounds(bounds); + 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 + }); + + selectionCircle.addListener('drag', function() { + var centerCoord = selectionCircle.getCenter(); + selectPoints(el.node()); + var evt = { + type: 'geo', + source: 'geo', + center: [centerCoord.lng(), centerCoord.lat()], + distance: selectionCircle.getRadius() / 1600 + }; + options.view.trigger('querybuilder', evt); + }); + googleMap.addListener('dragstart', function() { debug('drag start'); }); From 08209c7a22b844f699e787ffecb52df996a49c8a Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 11:47:12 -0400 Subject: [PATCH 23/32] feature flag for geo minicharts --- src/app.js | 11 ------ src/minicharts/d3fns/geo.js | 2 ++ src/minicharts/index.js | 68 ++++++++----------------------------- 3 files changed, 17 insertions(+), 64 deletions(-) diff --git a/src/app.js b/src/app.js index b6fad81b703..276fdc8ea66 100644 --- a/src/app.js +++ b/src/app.js @@ -236,7 +236,6 @@ 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, @@ -255,16 +254,6 @@ 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/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index c5d1f3c637b..c275f14bb6a 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -144,6 +144,7 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { + // debugger; if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; @@ -260,6 +261,7 @@ var minicharts_d3fns_coordinates = function() { googleMap.addListener('dragstart', function() { debug('drag start'); }); + google.maps.event.trigger(googleMap, 'resize'); }); // end selection.each() } diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 6d188114c17..b33e81a9d16 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -43,20 +43,6 @@ 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; - }, render: function() { this.renderWithTemplate(this); @@ -68,52 +54,28 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.viewOptions.className = 'minichart unique'; this.subview = new UniqueMinichartView(this.viewOptions); } else if (this.model.name === 'Document') { - // are these coordinates? Do a basic check for now, until we support semantic schema types - // here we check for GeoJSON form: { loc: {type: "Point", "coordinates": [47.80, 9.63] } } - var isGeo = false; - if (app.isFeatureEnabled('Geo Minicharts')) { - 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 - ) { - var coords =this._mangleGeoCoordinates( - this.model.fields.get('coordinates').types.get('Array') - .types.get('Number').values.serialize()); - if (coords) { - this.model.values = coords; - this.model.fields.reset(); - isGeo = true; - } - } - } - if (isGeo) { - // coordinates get an HTML-based d3 VizView with `coordinates` vizFn - this.viewOptions.renderMode = 'html'; - this.viewOptions.height = 250; - this.viewOptions.vizFn = vizFns.geo; - this.subview = new VizView(this.viewOptions); - } else { - // nested objects get a div-based DocumentRootMinichart - this.viewOptions.height = 55; - this.subview = new DocumentRootMinichartView(this.viewOptions); - } + // nested objects get a div-based DocumentRootMinichart + this.viewOptions.height = 55; + this.subview = new DocumentRootMinichartView(this.viewOptions); } else if (this.model.name === 'Array') { var isGeo = false; if (app.isFeatureEnabled('Geo Minicharts')) { // are these coordinates? Do a basic check for now, until we support semantic schema types - // here we check for legacy coordinates in array form: { loc: [47.80, 9.63] } var lengths = this.model.lengths; if (_.min(lengths) === 2 && _.max(lengths) === 2) { - var coords = this._mangleGeoCoordinates( - this.model.types.get('Number').values.serialize()); - if (coords) { - this.model.values = coords; + // now check value bounds + var values = this.model.types.get('Number').values.serialize(); + 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) { isGeo = true; + // attach the zipped up coordinates to the model where VizView would expect it + this.model.values = new ArrayCollection(_.zip(lons, lats)); + debug('model.values', this.model.values); } } } From cf5c698e06779b5b7fed8fd31d31de13fe25eff6 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Fri, 9 Oct 2015 13:27:54 -0400 Subject: [PATCH 24/32] defer map resizing after stack has cleared. fixes second load issues. --- src/minicharts/d3fns/geo.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index c275f14bb6a..1c84469ffe9 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -25,7 +25,7 @@ var Singleton = (function() { var singleton = Singleton.getInstance(); -var minicharts_d3fns_coordinates = function() { +var minicharts_d3fns_geo = function() { // --- beginning chart setup --- var width = 400; var height = 100; @@ -144,7 +144,6 @@ var minicharts_d3fns_coordinates = function() { function chart(selection) { selection.each(function(data) { - // debugger; if (!singleton.google) { // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; @@ -261,7 +260,10 @@ var minicharts_d3fns_coordinates = function() { googleMap.addListener('dragstart', function() { debug('drag start'); }); - google.maps.event.trigger(googleMap, 'resize'); + _.defer(function() { + google.maps.event.trigger(googleMap, 'resize'); + googleMap.fitBounds(bounds); + }, 100); }); // end selection.each() } @@ -292,4 +294,4 @@ var minicharts_d3fns_coordinates = function() { return chart; }; -module.exports = minicharts_d3fns_coordinates; +module.exports = minicharts_d3fns_geo; From 79e04948cf4c615835b8c7cdc19aaf77360cad67 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 15:27:19 -0400 Subject: [PATCH 25/32] shiftkey handling, language model bump, geojson --- src/minicharts/d3fns/geo.js | 29 +++++++++++---- src/minicharts/index.js | 70 +++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 1c84469ffe9..52d27b1d7b0 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -4,6 +4,7 @@ var shared = require('./shared'); var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); +var SHIFTKEY = 16; var Singleton = (function() { var instance; @@ -62,6 +63,20 @@ var minicharts_d3fns_geo = function() { }); } + 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; @@ -166,11 +181,11 @@ var minicharts_d3fns_geo = function() { if (!googleMap) { // Create the Google Map googleMap = new google.maps.Map(el.node(), { - // disableDefaultUI: false, + disableDefaultUI: true, // disableDoubleClickZoom: true, - // scrollwheel: true, - draggable: false, - panControl: true, + scrollwheel: true, + draggable: true, + zoomControl: true, mapTypeId: google.maps.MapTypeId.ROADMAP, styles: mapStyle }); @@ -257,14 +272,14 @@ var minicharts_d3fns_geo = function() { options.view.trigger('querybuilder', evt); }); - googleMap.addListener('dragstart', function() { - debug('drag start'); - }); _.defer(function() { google.maps.event.trigger(googleMap, 'resize'); googleMap.fitBounds(bounds); }, 100); }); // end selection.each() + d3.select('body') + .on('keydown', onKeyDown) + .on('keyup', onKeyUp); } chart.width = function(value) { diff --git a/src/minicharts/index.js b/src/minicharts/index.js index b33e81a9d16..35b27883dc5 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -43,6 +43,22 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { }); this.listenTo(app.volatileQueryOptions, 'change:query', this.handleVolatileQueryChange); }, + _mangleGeoCoordinates: function(values) { + debug('mangle values', 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; + }, render: function() { this.renderWithTemplate(this); @@ -54,28 +70,52 @@ 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); + // are these coordinates? Do a basic check for now, until we support semantic schema types + // here we check for GeoJSON form: { loc: {type: "Point", "coordinates": [47.80, 9.63] } } + var isGeo = false; + if (app.isFeatureEnabled('Geo Minicharts')) { + 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 + ) { + var coords =this._mangleGeoCoordinates( + this.model.fields.get('coordinates').types.get('Array') + .types.get('Number').values.serialize()); + if (coords) { + this.model.values = coords; + this.model.fields.reset(); + isGeo = true; + } + } + } + if (isGeo) { + // coordinates get an HTML-based d3 VizView with `coordinates` vizFn + this.viewOptions.renderMode = 'html'; + this.viewOptions.height = 250; + this.viewOptions.vizFn = vizFns.geo; + this.subview = new VizView(this.viewOptions); + } else { + // nested objects get a div-based DocumentRootMinichart + this.viewOptions.height = 55; + this.subview = new DocumentRootMinichartView(this.viewOptions); + } } else if (this.model.name === 'Array') { var isGeo = false; if (app.isFeatureEnabled('Geo Minicharts')) { // are these coordinates? Do a basic check for now, until we support semantic schema types + // here we check for legacy coordinates in array form: { loc: [47.80, 9.63] } var lengths = this.model.lengths; if (_.min(lengths) === 2 && _.max(lengths) === 2) { - // now check value bounds - var values = this.model.types.get('Number').values.serialize(); - 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) { + var coords = this._mangleGeoCoordinates( + this.model.types.get('Number').values.serialize()); + if (coords) { + this.model.values = coords; isGeo = true; - // attach the zipped up coordinates to the model where VizView would expect it - this.model.values = new ArrayCollection(_.zip(lons, lats)); - debug('model.values', this.model.values); } } } From 52f6e0bfd896575498bf500c5f74cd1c54a6af19 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 23:28:24 -0400 Subject: [PATCH 26/32] geo backwards pass implemented. --- src/minicharts/d3fns/geo.js | 93 +++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 52d27b1d7b0..ef81cf7d295 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,7 +1,7 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -var debug = require('debug')('scout:minicharts:geo'); +// var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; @@ -47,7 +47,8 @@ var minicharts_d3fns_geo = function() { return singleton.google.maps.geometry.spherical.computeDistanceBetween(point, center) <= radius; } - function selectPoints(frame) { + function selectPoints() { + var frame = options.el; var google = singleton.google; if (selectionCircle.getRadius() === 0) { @@ -102,11 +103,6 @@ var minicharts_d3fns_geo = function() { .on('mousemove', function() { var m = d3.mouse(frame); - // d3.select(frame).selectAll('.marker circle') - // .classed('selected', function(d) { - // return Math.pow(d.x - center[0], 2) + Math.pow(d.y - center[1], 2) <= radius_sqr; - // }); - if (!options.view) { return; } @@ -117,7 +113,7 @@ var minicharts_d3fns_geo = function() { centerCoord, currentCoord); selectionCircle.setRadius(meterDistance); - selectPoints(frame); + selectPoints(); var evt = { type: 'geo', @@ -160,7 +156,7 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { if (!singleton.google) { - // GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { singleton.google = g; @@ -227,19 +223,14 @@ var minicharts_d3fns_geo = function() { .style('top', p.y - padding + 'px'); return self; } - - // function transformRadius(d) { - // var p = projection.fromLatLngToDivPixel(currentCoord); - // debug('transformRadius', currentCoord, p); - // if (!p) return d3.select(this); - // var r = Math.sqrt(Math.pow(d.x - p.x, 2) + Math.pow(d.y - p.y, 2)); - // debug('radius', r); - // return d3.select(this).attr('r', r); - // } }; // 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; @@ -247,39 +238,39 @@ var minicharts_d3fns_geo = function() { googleMap.fitBounds(bounds); - 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 - }); + 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(el.node()); - var evt = { - type: 'geo', - source: 'geo', - center: [centerCoord.lng(), centerCoord.lat()], - distance: selectionCircle.getRadius() / 1600 - }; - options.view.trigger('querybuilder', evt); - }); + 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); + }); + } _.defer(function() { google.maps.event.trigger(googleMap, 'resize'); googleMap.fitBounds(bounds); }, 100); }); // end selection.each() - d3.select('body') - .on('keydown', onKeyDown) - .on('keyup', onKeyUp); } chart.width = function(value) { @@ -306,6 +297,20 @@ var minicharts_d3fns_geo = function() { 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; }; From 9820dd4368aac7a98b7cdfb4d753a05494da8fae Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 13 Oct 2015 23:46:09 -0400 Subject: [PATCH 27/32] update security policy, fix eslint errors --- src/minicharts/d3fns/geo.js | 2 ++ src/minicharts/index.js | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index ef81cf7d295..77550ccbbe4 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -6,6 +6,7 @@ var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; +/* eslint wrap-iife:0 */ var Singleton = (function() { var instance; @@ -156,6 +157,7 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { if (!singleton.google) { + // @todo: replace with corporate api key GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; GoogleMapsLoader.LIBRARIES = ['geometry']; GoogleMapsLoader.load(function(g) { diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 35b27883dc5..6d188114c17 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -44,7 +44,6 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.listenTo(app.volatileQueryOptions, 'change:query', this.handleVolatileQueryChange); }, _mangleGeoCoordinates: function(values) { - debug('mangle values', values) // now check value bounds var lons = values.filter(function(val, idx) { return idx % 2 === 0; @@ -53,7 +52,6 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { 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)); } From 57259a6b72107de88724bc2c591069cebd960b72 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Thu, 15 Oct 2015 15:55:44 -0400 Subject: [PATCH 28/32] detect google map loading timeouts and disable --- src/app.js | 10 ++++++ src/minicharts/d3fns/geo.js | 66 +++++++++++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/app.js b/src/app.js index 276fdc8ea66..a4856c3afe0 100644 --- a/src/app.js +++ b/src/app.js @@ -254,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/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 77550ccbbe4..dab9fb8a5db 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -1,10 +1,13 @@ var d3 = require('d3'); var _ = require('lodash'); var shared = require('./shared'); -// var debug = require('debug')('scout:minicharts:geo'); +var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); var SHIFTKEY = 16; +var async = require('async'); +var app = require('ampersand-app'); + /* eslint wrap-iife:0 */ var Singleton = (function() { @@ -25,6 +28,38 @@ var Singleton = (function() { }; })(); +// 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() { @@ -156,12 +191,30 @@ var minicharts_d3fns_geo = function() { function chart(selection) { selection.each(function(data) { + var el = d3.select(this); + if (!singleton.google) { - // @todo: replace with corporate api key - GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; - GoogleMapsLoader.LIBRARIES = ['geometry']; - GoogleMapsLoader.load(function(g) { - singleton.google = g; + parallel({ timeoutMS: 3000 }, [ // 10 second timeout + // tasks + function(done) { + GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; + GoogleMapsLoader.LIBRARIES = ['geometry']; + GoogleMapsLoader.load(function(g) { + done(null, g); + }); + } + ], + // calback + function(err, results) { + debug('err/res', 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; @@ -169,7 +222,6 @@ var minicharts_d3fns_geo = function() { var google = singleton.google; - var el = d3.select(this); var bounds = new google.maps.LatLngBounds(); _.each(data, function(d) { var p = new google.maps.LatLng(d[1], d[0]); From ee13bb1de608005164f088e8200a78760970b9e2 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Mon, 2 Nov 2015 22:24:13 +1100 Subject: [PATCH 29/32] security policy updated, POIs hidden. --- src/index.jade | 2 +- src/minicharts/d3fns/geo.js | 5 +++-- src/minicharts/d3fns/mapstyle.js | 14 ++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/index.jade b/src/index.jade index e38955fff48..31cacfb4f20 100644 --- a/src/index.jade +++ b/src/index.jade @@ -2,7 +2,7 @@ doctype html html(lang='en') head title MongoDB - meta(http-equiv="Content-Security-Policy", content="default-src *; script-src 'self' https://maps.googleapis.com https://maps.gstatic.com http://localhost:35729 https://mts0.googleapis.com https://mts1.googleapis.com 'unsafe-eval'; style-src 'self' https://fonts.googleapis.com '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 index dab9fb8a5db..e59bdd22d08 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -4,10 +4,11 @@ var shared = require('./shared'); var debug = require('debug')('scout:minicharts:geo'); var GoogleMapsLoader = require('google-maps'); var mapStyle = require('./mapstyle'); -var SHIFTKEY = 16; var async = require('async'); var app = require('ampersand-app'); +var SHIFTKEY = 16; + /* eslint wrap-iife:0 */ var Singleton = (function() { @@ -35,7 +36,7 @@ function parallel(options, tasks, cb) { options = options || {}; // no timeout wrapper; passthrough to async.parallel - if (typeof options.timeoutMS != 'number') return async.parallel(tasks, cb); + if (typeof options.timeoutMS !== 'number') return async.parallel(tasks, cb); var timeout = setTimeout(function() { // remove timeout, so we'll know we already erred out diff --git a/src/minicharts/d3fns/mapstyle.js b/src/minicharts/d3fns/mapstyle.js index f58ffb5030f..bee2a1cd02d 100644 --- a/src/minicharts/d3fns/mapstyle.js +++ b/src/minicharts/d3fns/mapstyle.js @@ -22,13 +22,19 @@ module.exports = [ }, { featureType: 'poi', - elementType: 'geometry', + elementType: 'all', stylers: [ { - visibility: 'simplified' - }, + visibility: 'off' + } + ] + }, + { + featureType: 'transit.station', + elementType: 'all', + stylers: [ { - color: '#fcfcfc' + visibility: 'off' } ] }, From 58e9c096a2832f2284ead1628c2491baf5456d72 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 3 Nov 2015 20:06:16 +1100 Subject: [PATCH 30/32] :bug: :shirt: fix nested geo fields, eslint cleanup --- package.json | 1 + src/field-list/index.js | 4 ++-- src/minicharts/d3fns/geo.js | 28 +++++++++++++++++++++++----- src/minicharts/index.js | 14 +++++++++----- src/minicharts/querybuilder.js | 4 +++- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index eeec5d634d1..25ac32e5ad6 100644 --- a/package.json +++ b/package.json @@ -92,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/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/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index e59bdd22d08..82f34d15514 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -195,7 +195,7 @@ var minicharts_d3fns_geo = function() { var el = d3.select(this); if (!singleton.google) { - parallel({ timeoutMS: 3000 }, [ // 10 second timeout + parallel({ timeoutMS: 5000 }, [ // 5 second timeout // tasks function(done) { GoogleMapsLoader.KEY = 'AIzaSyDrhE1qbcnNIh4sK3t7GEcbLRdCNKWjlt0'; @@ -321,10 +321,28 @@ var minicharts_d3fns_geo = function() { }); } - _.defer(function() { - google.maps.event.trigger(googleMap, 'resize'); - googleMap.fitBounds(bounds); - }, 100); + // need to fit the map bounds after view became visible + var fieldView = options.view.parent.parent; + if (fieldView.parent.parent.modelType === 'FieldView') { + debug('we are in a nested field, wait until it expands...'); + // 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() } diff --git a/src/minicharts/index.js b/src/minicharts/index.js index 6d188114c17..b84b5b97d06 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -8,9 +8,10 @@ var DocumentRootMinichartView = require('./document-root'); var ArrayRootMinichartView = require('./array-root'); var vizFns = require('./d3fns'); var QueryBuilderMixin = require('./querybuilder'); -var debug = require('debug')('scout:minicharts:index'); var Collection = require('ampersand-collection'); +// var debug = require('debug')('scout:minicharts:index'); + var ArrayCollection = Collection.extend({ model: Array }); @@ -57,7 +58,11 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { } return false; }, + /* eslint complexity: 0 */ render: function() { + var isGeo = false; + var coords; + this.renderWithTemplate(this); if (['String', 'Number'].indexOf(this.model.name) !== -1 @@ -70,7 +75,6 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { } else if (this.model.name === 'Document') { // are these coordinates? Do a basic check for now, until we support semantic schema types // here we check for GeoJSON form: { loc: {type: "Point", "coordinates": [47.80, 9.63] } } - var isGeo = false; if (app.isFeatureEnabled('Geo Minicharts')) { if (this.model.fields.length === 2 && this.model.fields.get('type') @@ -81,7 +85,7 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { === this.model.fields.get('coordinates').count && this.model.fields.get('coordinates').types.get('Array').average_length === 2 ) { - var coords =this._mangleGeoCoordinates( + coords = this._mangleGeoCoordinates( this.model.fields.get('coordinates').types.get('Array') .types.get('Number').values.serialize()); if (coords) { @@ -103,13 +107,13 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.subview = new DocumentRootMinichartView(this.viewOptions); } } else if (this.model.name === 'Array') { - var isGeo = false; + isGeo = false; if (app.isFeatureEnabled('Geo Minicharts')) { // are these coordinates? Do a basic check for now, until we support semantic schema types // here we check for legacy coordinates in array form: { loc: [47.80, 9.63] } var lengths = this.model.lengths; if (_.min(lengths) === 2 && _.max(lengths) === 2) { - var coords = this._mangleGeoCoordinates( + coords = this._mangleGeoCoordinates( this.model.types.get('Number').values.serialize()); if (coords) { this.model.values = coords; diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index b7c070f1fcc..d942b9ea5d3 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -8,7 +8,8 @@ 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 debug = require('debug')('scout:minicharts:querybuilder'); var MODIFIERKEY = 'shiftKey'; var checkBounds = { @@ -48,6 +49,7 @@ module.exports = { * } * */ + /* eslint complexity: 0 */ handleQueryBuilderEvent: function(data) { var queryType; From bba40ee4cd561b9806dbfbd78beb2b54ff5a09f1 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 3 Nov 2015 21:09:34 +1100 Subject: [PATCH 31/32] DRY'ed minicharts/index.js. Offline detection. --- src/minicharts/d3fns/geo.js | 4 +- src/minicharts/index.js | 107 +++++++++++++++--------------------- 2 files changed, 46 insertions(+), 65 deletions(-) diff --git a/src/minicharts/d3fns/geo.js b/src/minicharts/d3fns/geo.js index 82f34d15514..94369d6317b 100644 --- a/src/minicharts/d3fns/geo.js +++ b/src/minicharts/d3fns/geo.js @@ -207,7 +207,6 @@ var minicharts_d3fns_geo = function() { ], // calback function(err, results) { - debug('err/res', 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 @@ -324,12 +323,11 @@ var minicharts_d3fns_geo = function() { // need to fit the map bounds after view became visible var fieldView = options.view.parent.parent; if (fieldView.parent.parent.modelType === 'FieldView') { - debug('we are in a nested field, wait until it expands...'); // 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); + // debug('field %s expanded. fitting map.', view.model.name); _.defer(function() { google.maps.event.trigger(googleMap, 'resize'); googleMap.fitBounds(bounds); diff --git a/src/minicharts/index.js b/src/minicharts/index.js index b84b5b97d06..d306ec624f3 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -9,6 +9,7 @@ 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'); @@ -59,13 +60,49 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { return false; }, /* eslint complexity: 0 */ - render: function() { - var isGeo = false; + _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(); + 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; + 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'; @@ -73,65 +110,11 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { this.viewOptions.className = 'minichart unique'; this.subview = new UniqueMinichartView(this.viewOptions); } else if (this.model.name === 'Document') { - // are these coordinates? Do a basic check for now, until we support semantic schema types - // here we check for GeoJSON form: { loc: {type: "Point", "coordinates": [47.80, 9.63] } } - if (app.isFeatureEnabled('Geo Minicharts')) { - 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 - ) { - coords = this._mangleGeoCoordinates( - this.model.fields.get('coordinates').types.get('Array') - .types.get('Number').values.serialize()); - if (coords) { - this.model.values = coords; - this.model.fields.reset(); - isGeo = true; - } - } - } - if (isGeo) { - // coordinates get an HTML-based d3 VizView with `coordinates` vizFn - this.viewOptions.renderMode = 'html'; - this.viewOptions.height = 250; - this.viewOptions.vizFn = vizFns.geo; - this.subview = new VizView(this.viewOptions); - } else { - // nested objects get a div-based DocumentRootMinichart - this.viewOptions.height = 55; - this.subview = new DocumentRootMinichartView(this.viewOptions); - } + this.viewOptions.height = 55; + this.subview = new DocumentRootMinichartView(this.viewOptions); } else if (this.model.name === 'Array') { - isGeo = false; - if (app.isFeatureEnabled('Geo Minicharts')) { - // are these coordinates? Do a basic check for now, until we support semantic schema types - // here we check for legacy coordinates in array form: { loc: [47.80, 9.63] } - var lengths = this.model.lengths; - if (_.min(lengths) === 2 && _.max(lengths) === 2) { - coords = this._mangleGeoCoordinates( - this.model.types.get('Number').values.serialize()); - if (coords) { - this.model.values = coords; - isGeo = true; - } - } - } - if (isGeo) { - // coordinates get an HTML-based d3 VizView with `coordinates` vizFn - this.viewOptions.renderMode = 'html'; - this.viewOptions.height = 250; - this.viewOptions.vizFn = vizFns.geo; - this.subview = new VizView(this.viewOptions); - } else { - // plain arrays get a div-based ArrayRootMinichart - this.viewOptions.height = 55; - this.subview = new ArrayRootMinichartView(this.viewOptions); - } + 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); From 6e897348469041239e28339fad8f1bb6f52e6073 Mon Sep 17 00:00:00 2001 From: Thomas Rueckstiess Date: Tue, 3 Nov 2015 22:28:29 +1100 Subject: [PATCH 32/32] rename type to "coordinates". suppress fields/types. --- src/minicharts/index.js | 3 +++ src/minicharts/querybuilder.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/minicharts/index.js b/src/minicharts/index.js index d306ec624f3..9f93e704daa 100644 --- a/src/minicharts/index.js +++ b/src/minicharts/index.js @@ -82,6 +82,7 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { // 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; @@ -91,6 +92,8 @@ module.exports = AmpersandView.extend(QueryBuilderMixin, { 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; diff --git a/src/minicharts/querybuilder.js b/src/minicharts/querybuilder.js index d942b9ea5d3..3b6d64808cf 100644 --- a/src/minicharts/querybuilder.js +++ b/src/minicharts/querybuilder.js @@ -80,6 +80,7 @@ module.exports = { break; case 'Array': case 'Document': + case 'Coordinates': if (data.source === 'geo') { queryType = 'geo'; } else {