diff --git a/src/ui/camera.js b/src/ui/camera.js index 13ffed46e7d..6820457b0f0 100644 --- a/src/ui/camera.js +++ b/src/ui/camera.js @@ -65,10 +65,10 @@ type AnimationOptions = { class Camera extends Evented { transform: Transform; - moving: boolean; - zooming: boolean; - rotating: boolean; - pitching: boolean; + _moving: boolean; + _zooming: boolean; + _rotating: boolean; + _pitching: boolean; _bearingSnap: number; _onEaseEnd: number; @@ -82,8 +82,8 @@ class Camera extends Evented { constructor(transform: Transform, options: {bearingSnap: number}) { super(); - this.moving = false; - this.zooming = false; + this._moving = false; + this._zooming = false; this.transform = transform; this._bearingSnap = options.bearingSnap; } @@ -562,22 +562,22 @@ class Camera extends Evented { aroundPoint = tr.locationPoint(around); } - this.zooming = (zoom !== startZoom); - this.rotating = (startBearing !== bearing); - this.pitching = (pitch !== startPitch); + this._zooming = (zoom !== startZoom); + this._rotating = (startBearing !== bearing); + this._pitching = (pitch !== startPitch); this._prepareEase(eventData, options.noMoveStart); clearTimeout(this._onEaseEnd); this._ease((k) => { - if (this.zooming) { + if (this._zooming) { tr.zoom = interpolate(startZoom, zoom, k); } - if (this.rotating) { + if (this._rotating) { tr.bearing = interpolate(startBearing, bearing, k); } - if (this.pitching) { + if (this._pitching) { tr.pitch = interpolate(startPitch, pitch, k); } @@ -607,43 +607,43 @@ class Camera extends Evented { } _prepareEase(eventData?: Object, noMoveStart: boolean) { - this.moving = true; + this._moving = true; if (!noMoveStart) { this.fire('movestart', eventData); } - if (this.zooming) { + if (this._zooming) { this.fire('zoomstart', eventData); } - if (this.rotating) { + if (this._rotating) { this.fire('rotatestart', eventData); } - if (this.pitching) { + if (this._pitching) { this.fire('pitchstart', eventData); } } _fireMoveEvents(eventData?: Object) { this.fire('move', eventData); - if (this.zooming) { + if (this._zooming) { this.fire('zoom', eventData); } - if (this.rotating) { + if (this._rotating) { this.fire('rotate', eventData); } - if (this.pitching) { + if (this._pitching) { this.fire('pitch', eventData); } } _afterEase(eventData?: Object) { - const wasZooming = this.zooming; - const wasRotating = this.rotating; - const wasPitching = this.pitching; - this.moving = false; - this.zooming = false; - this.rotating = false; - this.pitching = false; + const wasZooming = this._zooming; + const wasRotating = this._rotating; + const wasPitching = this._pitching; + this._moving = false; + this._zooming = false; + this._rotating = false; + this._pitching = false; if (wasZooming) { this.fire('zoomend', eventData); @@ -825,9 +825,9 @@ class Camera extends Evented { options.duration = 0; } - this.zooming = true; - this.rotating = (startBearing !== bearing); - this.pitching = (pitch !== startPitch); + this._zooming = true; + this._rotating = (startBearing !== bearing); + this._pitching = (pitch !== startPitch); this._prepareEase(eventData, false); @@ -837,10 +837,10 @@ class Camera extends Evented { const scale = 1 / w(s); tr.zoom = startZoom + tr.scaleZoom(scale); - if (this.rotating) { + if (this._rotating) { tr.bearing = interpolate(startBearing, bearing, k); } - if (this.pitching) { + if (this._pitching) { tr.pitch = interpolate(startPitch, pitch, k); } @@ -858,26 +858,6 @@ class Camera extends Evented { return !!this._isEasing; } - /** - * Returns a Boolean indicating whether the camera is moving. - * - * @memberof Map# - * @returns A Boolean indicating whether the camera is moving. - */ - isMoving(): boolean { - return this.moving; - } - - /** - * Returns a Boolean indicating whether the camera is zooming. - * - * @memberof Map# - * @returns A Boolean indicating whether the camera is zooming. - */ - isZooming(): boolean { - return this.zooming; - } - /** * Stops any animated transition underway. * diff --git a/src/ui/handler/drag_pan.js b/src/ui/handler/drag_pan.js index 5e6533bbfd3..193e3926a68 100644 --- a/src/ui/handler/drag_pan.js +++ b/src/ui/handler/drag_pan.js @@ -122,7 +122,6 @@ class DragPanHandler { // we treat the first move event (rather than the mousedown event) // as the start of the drag this._active = true; - this._map.moving = true; this._fireEvent('dragstart', e); this._fireEvent('movestart', e); } @@ -170,7 +169,6 @@ class DragPanHandler { this._drainInertiaBuffer(); const finish = () => { - this._map.moving = false; this._fireEvent('moveend', e); }; diff --git a/src/ui/handler/drag_rotate.js b/src/ui/handler/drag_rotate.js index 87a5c9f08e9..61c9bf3d1af 100644 --- a/src/ui/handler/drag_rotate.js +++ b/src/ui/handler/drag_rotate.js @@ -142,7 +142,6 @@ class DragRotateHandler { if (!this.isActive()) { this._active = true; - this._map.moving = true; this._fireEvent('rotatestart', e); this._fireEvent('movestart', e); if (this._pitchWithRotate) { @@ -206,7 +205,6 @@ class DragRotateHandler { if (Math.abs(mapBearing) < this._bearingSnap) { map.resetNorth({noMoveStart: true}, { originalEvent: e }); } else { - this._map.moving = false; this._fireEvent('moveend', e); } if (this._pitchWithRotate) this._fireEvent('pitchend', e); diff --git a/src/ui/handler/scroll_zoom.js b/src/ui/handler/scroll_zoom.js index 912b832e997..07730f92829 100644 --- a/src/ui/handler/scroll_zoom.js +++ b/src/ui/handler/scroll_zoom.js @@ -189,8 +189,6 @@ class ScrollZoomHandler { if (!this._delta) return; this._active = true; - this._map.moving = true; - this._map.zooming = true; this._map.fire('movestart', {originalEvent: e}); this._map.fire('zoomstart', {originalEvent: e}); clearTimeout(this._finishTimeout); @@ -251,8 +249,6 @@ class ScrollZoomHandler { if (!this.isActive()) return; this._active = false; this._finishTimeout = setTimeout(() => { - this._map.moving = false; - this._map.zooming = false; this._map.fire('zoomend'); this._map.fire('moveend'); delete this._targetZoom; diff --git a/src/ui/map.js b/src/ui/map.js index b2c46c4b82c..141039964da 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -580,6 +580,32 @@ class Map extends Camera { return this.transform.pointLocation(Point.convert(point)); } + /** + * Returns true if the map is panning, zooming, rotating, or pitching due to a camera animation or user gesture. + */ + isMoving(): boolean { + return this._moving || + this.dragPan.isActive() || + this.dragRotate.isActive() || + this.scrollZoom.isActive(); + } + + /** + * Returns true if the map is zooming due to a camera animation or user gesture. + */ + isZooming(): boolean { + return this._zooming || + this.scrollZoom.isActive(); + } + + /** + * Returns true if the map is rotating due to a camera animation or user gesture. + */ + isRotating(): boolean { + return this._rotating || + this.dragRotate.isActive(); + } + /** * Adds a listener for events of a specified type. * @@ -1526,8 +1552,8 @@ class Map extends Camera { this.painter.render(this.style, { showTileBoundaries: this.showTileBoundaries, showOverdrawInspector: this._showOverdrawInspector, - rotating: this.rotating, - zooming: this.zooming, + rotating: this.isRotating(), + zooming: this.isZooming(), fadeDuration: this._fadeDuration }); diff --git a/test/node_modules/mapbox-gl-js-test/simulate_interaction.js b/test/node_modules/mapbox-gl-js-test/simulate_interaction.js index c167d70783e..254fdcaad96 100644 --- a/test/node_modules/mapbox-gl-js-test/simulate_interaction.js +++ b/test/node_modules/mapbox-gl-js-test/simulate_interaction.js @@ -18,6 +18,18 @@ exports.click = function (target, options) { target.dispatchEvent(new MouseEvent('click', options)); }; +exports.dblclick = function (target, options) { + options = Object.assign({bubbles: true}, options); + const MouseEvent = window(target).MouseEvent; + target.dispatchEvent(new MouseEvent('mousedown', options)); + target.dispatchEvent(new MouseEvent('mouseup', options)); + target.dispatchEvent(new MouseEvent('click', options)); + target.dispatchEvent(new MouseEvent('mousedown', options)); + target.dispatchEvent(new MouseEvent('mouseup', options)); + target.dispatchEvent(new MouseEvent('click', options)); + target.dispatchEvent(new MouseEvent('dblclick', options)); +}; + [ 'mouseup', 'mousedown', 'mouseover', 'mousemove', 'mouseout' ].forEach((event) => { exports[event] = function (target, options) { options = Object.assign({bubbles: true}, options); @@ -34,6 +46,10 @@ exports.click = function (target, options) { }; }); +// magic deltaY value that indicates the event is from a mouse wheel +// (rather than a trackpad) +exports.magicWheelZoomDelta = 4.000244140625; + [ 'touchstart', 'touchend', 'touchmove', 'touchcancel' ].forEach((event) => { exports[event] = function (target, options) { // Should be using Touch constructor here, but https://github.com/jsdom/jsdom/issues/2152. diff --git a/test/unit/ui/camera.test.js b/test/unit/ui/camera.test.js index 2741111fecf..cfb4d7bae3c 100644 --- a/test/unit/ui/camera.test.js +++ b/test/unit/ui/camera.test.js @@ -701,9 +701,9 @@ test('camera', (t) => { .on('movestart', (d) => { movestarted = d.data; }) .on('move', (d) => { moved = d.data; }) .on('moveend', (d) => { - t.notOk(camera.zooming); - t.notOk(camera.panning); - t.notOk(camera.rotating); + t.notOk(camera._zooming); + t.notOk(camera._panning); + t.notOk(camera._rotating); t.equal(movestarted, 'ok'); t.equal(moved, 'ok'); @@ -997,9 +997,9 @@ test('camera', (t) => { .on('rotate', (d) => { rotated = d.data; }) .on('pitch', (d) => { pitched = d.data; }) .on('moveend', function(d) { - t.notOk(this.zooming); - t.notOk(this.panning); - t.notOk(this.rotating); + t.notOk(this._zooming); + t.notOk(this._panning); + t.notOk(this._rotating); t.equal(movestarted, 'ok'); t.equal(moved, 'ok'); @@ -1064,9 +1064,9 @@ test('camera', (t) => { .on('pitch', (d) => { pitched = d.data; }) .on('pitchend', (d) => { pitchended = d.data; }) .on('moveend', function(d) { - t.notOk(this.zooming); - t.notOk(this.panning); - t.notOk(this.rotating); + t.notOk(this._zooming); + t.notOk(this._panning); + t.notOk(this._rotating); t.equal(movestarted, 'ok'); t.equal(moved, 'ok'); @@ -1579,19 +1579,19 @@ test('camera', (t) => { }); t.test('#stop', (t) => { - t.test('resets camera.zooming', (t) => { + t.test('resets camera._zooming', (t) => { const camera = createCamera(); camera.zoomTo(3.2); camera.stop(); - t.ok(!camera.zooming); + t.ok(!camera._zooming); t.end(); }); - t.test('resets camera.rotating', (t) => { + t.test('resets camera._rotating', (t) => { const camera = createCamera(); camera.rotateTo(90); camera.stop(); - t.ok(!camera.rotating); + t.ok(!camera._rotating); t.end(); }); diff --git a/test/unit/ui/handler/scroll_zoom.test.js b/test/unit/ui/handler/scroll_zoom.test.js index 73b979c396a..45029189ff2 100644 --- a/test/unit/ui/handler/scroll_zoom.test.js +++ b/test/unit/ui/handler/scroll_zoom.test.js @@ -19,10 +19,6 @@ function createMap(options) { }, options)); } -// magic deltaY value that indicates the event is from a mouse wheel -// (rather than a trackpad) -const magicWheelZoomDelta = 4.000244140625; - test('ScrollZoomHandler zooms in response to wheel events', (t) => { const browserNow = t.stub(browser, 'now'); let now = 1555555555555; @@ -35,7 +31,7 @@ test('ScrollZoomHandler zooms in response to wheel events', (t) => { // simulate a single 'wheel' event const startZoom = map.getZoom(); - simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -magicWheelZoomDelta}); + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); map._updateCamera(); now += 400; @@ -68,7 +64,7 @@ test('ScrollZoomHandler zooms in response to wheel events', (t) => { const startZoom = map.getZoom(); const events = [ - [2, {type: 'wheel', deltaY: -magicWheelZoomDelta}], + [2, {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}], [7, {type: 'wheel', deltaY: -41}], [30, {type: 'wheel', deltaY: -169}], [1, {type: 'wheel', deltaY: -801}], diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index 91adf57e02d..4042d9c4660 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -1299,40 +1299,6 @@ test('Map', (t) => { }); }); - t.test('Map#isMoving', (t) => { - t.plan(3); - const map = createMap(); - - t.equal(map.isMoving(), false, 'false before moving'); - - map.on('movestart', () => { - t.equal(map.isMoving(), true, 'true on movestart'); - }); - - map.on('moveend', () => { - t.equal(map.isMoving(), false, 'false on moveend'); - }); - - map.zoomTo(5, { duration: 0 }); - }); - - t.test('Map#isZooming', (t) => { - t.plan(3); - const map = createMap(); - - t.equal(map.isZooming(), false, 'false before zooming'); - - map.on('zoomstart', () => { - t.equal(map.isZooming(), true, 'true on zoomstart'); - }); - - map.on('zoomend', () => { - t.equal(map.isZooming(), false, 'false on zoomend'); - }); - - map.zoomTo(5, { duration: 0 }); - }); - t.end(); }); diff --git a/test/unit/ui/map/isMoving.test.js b/test/unit/ui/map/isMoving.test.js new file mode 100644 index 00000000000..1e7deea4159 --- /dev/null +++ b/test/unit/ui/map/isMoving.test.js @@ -0,0 +1,148 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const browser = require('../../../../src/util/browser'); +const window = require('../../../../src/util/window'); +const Map = require('../../../../src/ui/map'); +const DOM = require('../../../../src/util/dom'); +const simulate = require('mapbox-gl-js-test/simulate_interaction'); + +function createMap() { + return new Map({ container: DOM.create('div', '', window.document.body) }); +} + +test('Map#isMoving returns false by default', (t) => { + const map = createMap(); + t.equal(map.isMoving(), false); + map.remove(); + t.end(); +}); + +test('Map#isMoving returns true during a camera zoom animation', (t) => { + const map = createMap(); + + map.on('zoomstart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isMoving(), false); + map.remove(); + t.end(); + }); + + map.zoomTo(5, { duration: 0 }); +}); + +test('Map#isMoving returns true when drag panning', (t) => { + const map = createMap(); + + map.on('dragstart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('dragend', () => { + t.equal(map.isMoving(), false); + map.remove(); + t.end(); + }); + + simulate.mousedown(map.getCanvas()); + map._updateCamera(); + + simulate.mousemove(map.getCanvas()); + map._updateCamera(); + + simulate.mouseup(map.getCanvas()); + map._updateCamera(); +}); + +test('Map#isMoving returns true when drag rotating', (t) => { + const map = createMap(); + + map.on('rotatestart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('rotateend', () => { + t.equal(map.isMoving(), false); + map.remove(); + t.end(); + }); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._updateCamera(); + + simulate.mousemove(map.getCanvas(), {buttons: 2}); + map._updateCamera(); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._updateCamera(); +}); + +test('Map#isMoving returns true when scroll zooming', (t) => { + const map = createMap(); + + map.on('zoomstart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isMoving(), false); + map.remove(); + t.end(); + }); + + const browserNow = t.stub(browser, 'now'); + let now = 0; + browserNow.callsFake(() => now); + + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._updateCamera(); + + now += 400; + map._updateCamera(); +}); + +test('Map#isMoving returns true when drag panning and scroll zooming interleave', (t) => { + const map = createMap(); + + map.on('dragstart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('zoomstart', () => { + t.equal(map.isMoving(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isMoving(), true); + simulate.mouseup(map.getCanvas()); + map._updateCamera(); + }); + + map.on('dragend', () => { + t.equal(map.isMoving(), false); + map.remove(); + t.end(); + }); + + // The following should trigger the above events, where a zoomstart/zoomend + // pair is nested within a dragstart/dragend pair. + + simulate.mousedown(map.getCanvas()); + map._updateCamera(); + + simulate.mousemove(map.getCanvas()); + map._updateCamera(); + + const browserNow = t.stub(browser, 'now'); + let now = 0; + browserNow.callsFake(() => now); + + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._updateCamera(); + + now += 400; + map._updateCamera(); +}); diff --git a/test/unit/ui/map/isRotating.test.js b/test/unit/ui/map/isRotating.test.js new file mode 100644 index 00000000000..3c84d8e850c --- /dev/null +++ b/test/unit/ui/map/isRotating.test.js @@ -0,0 +1,57 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const window = require('../../../../src/util/window'); +const Map = require('../../../../src/ui/map'); +const DOM = require('../../../../src/util/dom'); +const simulate = require('mapbox-gl-js-test/simulate_interaction'); + +function createMap() { + return new Map({ container: DOM.create('div', '', window.document.body) }); +} + +test('Map#isRotating returns false by default', (t) => { + const map = createMap(); + t.equal(map.isRotating(), false); + map.remove(); + t.end(); +}); + +test('Map#isRotating returns true during a camera rotate animation', (t) => { + const map = createMap(); + + map.on('rotatestart', () => { + t.equal(map.isRotating(), true); + }); + + map.on('rotateend', () => { + t.equal(map.isRotating(), false); + map.remove(); + t.end(); + }); + + map.rotateTo(5, { duration: 0 }); +}); + +test('Map#isRotating returns true when drag rotating', (t) => { + const map = createMap(); + + map.on('rotatestart', () => { + t.equal(map.isRotating(), true); + }); + + map.on('rotateend', () => { + t.equal(map.isRotating(), false); + map.remove(); + t.end(); + }); + + simulate.mousedown(map.getCanvas(), {buttons: 2, button: 2}); + map._updateCamera(); + + simulate.mousemove(map.getCanvas(), {buttons: 2}); + map._updateCamera(); + + simulate.mouseup(map.getCanvas(), {buttons: 0, button: 2}); + map._updateCamera(); +}); diff --git a/test/unit/ui/map/isZooming.test.js b/test/unit/ui/map/isZooming.test.js new file mode 100644 index 00000000000..f42683523ee --- /dev/null +++ b/test/unit/ui/map/isZooming.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const browser = require('../../../../src/util/browser'); +const window = require('../../../../src/util/window'); +const Map = require('../../../../src/ui/map'); +const DOM = require('../../../../src/util/dom'); +const simulate = require('mapbox-gl-js-test/simulate_interaction'); + +function createMap() { + return new Map({ container: DOM.create('div', '', window.document.body) }); +} + +test('Map#isZooming returns false by default', (t) => { + const map = createMap(); + t.equal(map.isZooming(), false); + map.remove(); + t.end(); +}); + +test('Map#isZooming returns true during a camera zoom animation', (t) => { + const map = createMap(); + + map.on('zoomstart', () => { + t.equal(map.isZooming(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isZooming(), false); + map.remove(); + t.end(); + }); + + map.zoomTo(5, { duration: 0 }); +}); + +test('Map#isZooming returns true when scroll zooming', (t) => { + const map = createMap(); + + map.on('zoomstart', () => { + t.equal(map.isZooming(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isZooming(), false); + map.remove(); + t.end(); + }); + + let now = 0; + t.stub(browser, 'now').callsFake(() => now); + + simulate.wheel(map.getCanvas(), {type: 'wheel', deltaY: -simulate.magicWheelZoomDelta}); + map._updateCamera(); + + now += 400; + map._updateCamera(); +}); + +test('Map#isZooming returns true when double-click zooming', (t) => { + const map = createMap(); + + map.on('zoomstart', () => { + t.equal(map.isZooming(), true); + }); + + map.on('zoomend', () => { + t.equal(map.isZooming(), false); + map.remove(); + t.end(); + }); + + let now = 0; + t.stub(browser, 'now').callsFake(() => now); + + simulate.dblclick(map.getCanvas()); + map._updateCamera(); + + now += 500; + map._updateCamera(); +});