From 162a06508ba04ffe24b72e6efbf395454a54d639 Mon Sep 17 00:00:00 2001 From: dburnsii Date: Tue, 29 Oct 2019 16:59:27 -0700 Subject: [PATCH] Add rotation options to markers (#8836) --- src/ui/marker.js | 87 +++++++++++++++++++++++++++++++++- test/unit/ui/marker.test.js | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/ui/marker.js b/src/ui/marker.js index 5df1db44ad6..3204e4eb0c4 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -19,7 +19,10 @@ type Options = { offset?: PointLike, anchor?: Anchor, color?: string, - draggable?: boolean + draggable?: boolean, + rotation?: number, + rotationAlignment?: string, + pitchAlignment?: string }; /** @@ -31,6 +34,9 @@ type Options = { * @param {PointLike} [options.offset] The offset in pixels as a {@link PointLike} object to apply relative to the element's center. Negatives indicate left and up. * @param {string} [options.color='#3FB1CE'] The color to use for the default marker if options.element is not provided. The default is light blue. * @param {boolean} [options.draggable=false] A boolean indicating whether or not a marker is able to be dragged to a new position on the map. + * @param {number} [options.rotation=0] The rotation angle of the marker in degrees, relative to its respective {@link Marker#rotationAlignment} setting. A positive value will rotate the marker clockwise. + * @param {string} [options.pitchAlignment='auto'] `map` aligns the `Marker` to the plane of the map. `viewport` aligns the `Marker` to the plane of the viewport. `auto` automatically matches the value of `rotationAlignment`. + * @param {string} [options.rotationAlignment='auto'] `map` aligns the `Marker`'s rotation relative to the map, maintaining a bearing as the map rotates. `viewport` aligns the `Marker`'s rotation relative to the viewport, agnostic to map rotations. `auto` is equivalent to `viewport`. * @example * var marker = new mapboxgl.Marker() * .setLngLat([30.5, 50.5]) @@ -51,6 +57,9 @@ export default class Marker extends Evented { _draggable: boolean; _state: 'inactive' | 'pending' | 'active'; // used for handling drag events _positionDelta: ?number; + _rotation: number; + _pitchAlignment: string; + _rotationAlignment: string; constructor(options?: Options, legacyOptions?: Options) { super(); @@ -72,6 +81,9 @@ export default class Marker extends Evented { this._color = options && options.color || '#3FB1CE'; this._draggable = options && options.draggable || false; this._state = 'inactive'; + this._rotation = options && options.rotation || 0; + this._rotationAlignment = options && options.rotationAlignment || 'auto'; + this._pitchAlignment = options && options.pitchAlignment && options.pitchAlignment !== 'auto' ? options.pitchAlignment : this._rotationAlignment; if (!options || !options.element) { this._defaultMarker = true; @@ -344,6 +356,20 @@ export default class Marker extends Evented { this._pos = this._map.project(this._lngLat)._add(this._offset); + let rotation = ""; + if (this._rotationAlignment === "viewport" || this._rotationAlignment === "auto") { + rotation = `rotateZ(${this._rotation}deg)`; + } else if (this._rotationAlignment === "map") { + rotation = `rotateZ(${this._rotation - this._map.getBearing()}deg)`; + } + + let pitch = ""; + if (this._pitchAlignment === "viewport" || this._pitchAlignment === "auto") { + pitch = "rotateX(0deg)"; + } else if (this._pitchAlignment === "map") { + pitch = `rotateX(${this._map.getPitch()}deg)`; + } + // because rounding the coordinates at every `move` event causes stuttered zooming // we only round them when _update is called with `moveend` or when its called with // no arguments (when the Marker is initialized or Marker#setLngLat is invoked). @@ -351,7 +377,7 @@ export default class Marker extends Evented { this._pos = this._pos.round(); } - DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px)`); + DOM.setTransform(this._element, `${anchorTranslate[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${pitch} ${rotation}`); } /** @@ -484,4 +510,61 @@ export default class Marker extends Evented { isDraggable() { return this._draggable; } + + /** + * Sets the `rotation` property of the marker. + * @param {number} [rotation=0] The rotation angle of the marker (clockwise, in degrees), relative to its respective {@link Marker#rotationAlignment} setting. + * @returns {Marker} `this` + */ + setRotation(rotation: number) { + this._rotation = rotation || 0; + this._update(); + return this; + } + + /** + * Returns the current rotation angle of the marker (in degrees). + * @returns {number} + */ + getRotation() { + return this._rotation; + } + + /** + * Sets the `rotationAlignment` property of the marker. + * @param {string} [alignment='auto'] Sets the `rotationAlignment` property of the marker. + * @returns {Marker} `this` + */ + setRotationAlignment(alignment: string) { + this._rotationAlignment = alignment || 'auto'; + this._update(); + return this; + } + + /** + * Returns the current `rotationAlignment` property of the marker. + * @returns {string} + */ + getRotationAlignment() { + return this._rotationAlignment; + } + + /** + * Sets the `pitchAlignment` property of the marker. + * @param {string} [alignment] Sets the `pitchAlignment` property of the marker. If alignment is 'auto', it will automatically match `rotationAlignment`. + * @returns {Marker} `this` + */ + setPitchAlignment(alignment: string) { + this._pitchAlignment = alignment && alignment !== 'auto' ? alignment : this._rotationAlignment; + this._update(); + return this; + } + + /** + * Returns the current `pitchAlignment` property of the marker. + * @returns {string} + */ + getPitchAlignment() { + return this._pitchAlignment; + } } diff --git a/test/unit/ui/marker.test.js b/test/unit/ui/marker.test.js index 99d2920e225..620411e7f34 100644 --- a/test/unit/ui/marker.test.js +++ b/test/unit/ui/marker.test.js @@ -465,3 +465,96 @@ test('Marker with draggable:true does not error if removed on mousedown', (t) => t.ok(map.fire('mouseup')); t.end(); }); + +test('Marker can set rotationAlignment and pitchAlignment', (t) => { + const map = createMap(t); + const marker = new Marker({rotationAlignment: 'map', pitchAlignment: 'map'}) + .setLngLat([0, 0]) + .addTo(map); + + t.equal(marker.getRotationAlignment(), 'map'); + t.equal(marker.getPitchAlignment(), 'map'); + + map.remove(); + t.end(); +}); + +test('Marker can set and update rotation', (t) => { + const map = createMap(t); + const marker = new Marker({rotation: 45}) + .setLngLat([0, 0]) + .addTo(map); + + t.equal(marker.getRotation(), 45); + + marker.setRotation(90); + t.equal(marker.getRotation(), 90); + + map.remove(); + t.end(); +}); + +test('Marker transforms rotation with the map', (t) => { + const map = createMap(t); + const marker = new Marker({rotationAlignment: 'map'}) + .setLngLat([0, 0]) + .addTo(map); + + const rotationRegex = /rotateZ\(-?([0-9]+)deg\)/; + const initialRotation = marker.getElement().style.transform.match(rotationRegex)[1]; + + map.setBearing(map.getBearing() + 180); + + const finalRotation = marker.getElement().style.transform.match(rotationRegex)[1]; + t.notEqual(initialRotation, finalRotation); + + map.remove(); + t.end(); +}); + +test('Marker transforms pitch with the map', (t) => { + const map = createMap(t); + const marker = new Marker({pitchAlignment: 'map'}) + .setLngLat([0, 0]) + .addTo(map); + + map.setPitch(0); + + const rotationRegex = /rotateX\(-?([0-9]+)deg\)/; + const initialPitch = marker.getElement().style.transform.match(rotationRegex)[1]; + + map.setPitch(45); + + const finalPitch = marker.getElement().style.transform.match(rotationRegex)[1]; + t.notEqual(initialPitch, finalPitch); + + map.remove(); + t.end(); +}); + +test('Marker pitchAlignment when set to auto defaults to rotationAlignment', (t) => { + const map = createMap(t); + const marker = new Marker({rotationAlignment: 'map', pitchAlignment: 'auto'}) + .setLngLat([0, 0]) + .addTo(map); + + t.equal(marker.getRotationAlignment(), marker.getPitchAlignment()); + + map.remove(); + t.end(); +}); + +test('Marker pitchAlignment when set to auto defaults to rotationAlignment (setter/getter)', (t) => { + const map = createMap(t); + const marker = new Marker({pitchAlignment: 'map'}) + .setLngLat([0, 0]) + .addTo(map); + + t.equal(marker.getPitchAlignment(), 'map'); + marker.setRotationAlignment('viewport'); + marker.setPitchAlignment('auto'); + t.equal(marker.getRotationAlignment(), marker.getPitchAlignment()); + + map.remove(); + t.end(); +});