diff --git a/modules/svg/geolocate.js b/modules/svg/geolocate.js new file mode 100644 index 0000000000..f5ca10f97c --- /dev/null +++ b/modules/svg/geolocate.js @@ -0,0 +1,137 @@ +import { select as d3_select } from 'd3-selection'; + +import { svgPointTransform } from './helpers'; +import { geoMetersToLat } from '../geo'; +import _throttle from 'lodash-es/throttle'; + +export function svgGeolocate(projection, context, dispatch) { + var layer = d3_select(null); + var _position; + + + function init() { + if (svgGeolocate.initialized) return; // run once + svgGeolocate.enabled = false; + svgGeolocate.initialized = true; + } + + function showLayer() { + layer.style('display', 'block'); + } + + + function hideLayer() { + layer + .transition() + .duration(250) + .style('opacity', 0); + } + + function layerOn() { + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1); + + } + + function layerOff() { + layer.style('display', 'none'); + } + + function transform(d) { + return svgPointTransform(projection)(d); + } + + function accuracy(accuracy, loc) { // converts accuracy to pixels... + var degreesRadius = geoMetersToLat(accuracy), + tangentLoc = [loc[0], loc[1] + degreesRadius], + projectedTangent = projection(tangentLoc), + projectedLoc = projection([loc[0], loc[1]]); + + // southern most point will have higher pixel value... + return Math.round(projectedLoc[1] - projectedTangent[1]).toString(); + } + + function update() { + var geolocation = { loc: [_position.coords.longitude, _position.coords.latitude] }; + + var groups = layer.selectAll('.geolocations').selectAll('.geolocation') + .data([geolocation]); + + groups.exit() + .remove(); + + var pointsEnter = groups.enter() + .append('g') + .attr('class', 'geolocation'); + + pointsEnter + .append('circle') + .attr('id', 'geolocate-radius') + .attr('dx', '0') + .attr('dy', '0') + .attr('fill', 'rgb(15,128,225)') + .attr('fill-opacity', '0.3') + .attr('r', '0'); + + pointsEnter + .append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('fill', 'rgb(15,128,225)') + .attr('stroke', 'white') + .attr('stroke-width', '1.5') + .attr('r', '6'); + + groups.merge(pointsEnter) + .attr('transform', transform); + + d3_select('#geolocate-radius').attr('r', accuracy(_position.coords.accuracy, geolocation.loc)); + } + + function drawLocation(selection) { + var enabled = svgGeolocate.enabled; + + layer = selection.selectAll('.layer-geolocate') + .data([0]); + + layer.exit() + .remove(); + + var layerEnter = layer.enter() + .append('g') + .attr('class', 'layer-geolocate') + .style('display', enabled ? 'block' : 'none'); + + layerEnter + .append('g') + .attr('class', 'geolocations'); + + layer = layerEnter + .merge(layer); + + if (enabled) { + update(); + } else { + layerOff(); + } + } + + drawLocation.enabled = function (position, enabled) { + if (!arguments.length) return svgGeolocate.enabled; + _position = position; + svgGeolocate.enabled = enabled; + if (svgGeolocate.enabled) { + showLayer(); + layerOn(); + } else { + hideLayer(); + } + return this; + }; + + init(); + return drawLocation; +} diff --git a/modules/svg/index.js b/modules/svg/index.js index 310acc0587..b444bd7a05 100644 --- a/modules/svg/index.js +++ b/modules/svg/index.js @@ -3,6 +3,7 @@ export { svgData } from './data.js'; export { svgDebug } from './debug.js'; export { svgDefs } from './defs.js'; export { svgIcon } from './icon.js'; +export { svgGeolocate } from './geolocate'; export { svgLabels } from './labels.js'; export { svgLayers } from './layers.js'; export { svgLines } from './lines.js'; diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 220a53201b..4d0b9f7d22 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -9,6 +9,7 @@ import { select as d3_select } from 'd3-selection'; import { svgData } from './data'; import { svgDebug } from './debug'; +import { svgGeolocate } from './geolocate'; import { svgStreetside } from './streetside'; import { svgMapillaryImages } from './mapillary_images'; import { svgMapillarySigns } from './mapillary_signs'; @@ -32,6 +33,7 @@ export function svgLayers(projection, context) { { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) }, { id: 'openstreetcam-images', layer: svgOpenstreetcamImages(projection, context, dispatch) }, { id: 'debug', layer: svgDebug(projection, context, dispatch) }, + { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) }, { id: 'touch', layer: svgTouch(projection, context, dispatch) } ]; diff --git a/modules/ui/geolocate.js b/modules/ui/geolocate.js index e0efe190de..651196d056 100644 --- a/modules/ui/geolocate.js +++ b/modules/ui/geolocate.js @@ -9,27 +9,43 @@ import { uiLoading } from './loading'; export function uiGeolocate(context) { var geoOptions = { enableHighAccuracy: false, timeout: 6000 /* 6sec */ }, locating = uiLoading(context).message(t('geolocate.locating')).blocking(true), + layer = context.layers().layer('geolocate'), + position, + extent, timeoutId; function click() { if (context.inIntro()) return; context.enter(modeBrowse(context)); - context.container().call(locating); - navigator.geolocation.getCurrentPosition(success, error, geoOptions); - + if (!layer.enabled()) { + if (!position) { + context.container().call(locating); + navigator.geolocation.getCurrentPosition(success, error, geoOptions); + } else { + zoomTo(); + } + } else { + layer.enabled(null, false); + } // This timeout ensures that we still call finish() even if // the user declines to share their location in Firefox timeoutId = setTimeout(finish, 10000 /* 10sec */ ); } + function zoomTo() { + var map = context.map(); + layer.enabled(position, true); + map.centerZoom(extent.center(), Math.min(20, map.extentZoom(extent))); + } - function success(position) { - var map = context.map(), - extent = geoExtent([position.coords.longitude, position.coords.latitude]) - .padByMeters(position.coords.accuracy); - map.centerZoom(extent.center(), Math.min(20, map.extentZoom(extent))); + function success(geolocation) { + position = geolocation; + extent = geoExtent([position.coords.longitude, position.coords.latitude]) + .padByMeters(position.coords.accuracy); + + zoomTo(); finish(); } diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 2a574c1c1c..f2277229cd 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -26,7 +26,7 @@ describe('iD.svgLayers', function () { it('creates default data layers', function () { container.call(iD.svgLayers(projection, context)); var nodes = container.selectAll('svg .data-layer').nodes(); - expect(nodes.length).to.eql(9); + expect(nodes.length).to.eql(10); expect(d3.select(nodes[0]).classed('osm')).to.be.true; expect(d3.select(nodes[1]).classed('notes')).to.be.true; expect(d3.select(nodes[2]).classed('data')).to.be.true; @@ -35,7 +35,8 @@ describe('iD.svgLayers', function () { expect(d3.select(nodes[5]).classed('mapillary-signs')).to.be.true; expect(d3.select(nodes[6]).classed('openstreetcam-images')).to.be.true; expect(d3.select(nodes[7]).classed('debug')).to.be.true; - expect(d3.select(nodes[8]).classed('touch')).to.be.true; + expect(d3.select(nodes[8]).classed('geolocate')).to.be.true; + expect(d3.select(nodes[9]).classed('touch')).to.be.true; }); });