Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

Commit

Permalink
HARP-10889: Fix extrusion animation in polygons example.
Browse files Browse the repository at this point in the history
Extrusion animation across multiple data sources was not supported by
extrusion animation handler after latest refactoring.
  • Loading branch information
atomicsulfate committed Aug 18, 2020
1 parent 6d2ab44 commit d4275c6
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 60 deletions.
79 changes: 59 additions & 20 deletions @here/harp-mapview/lib/AnimatedExtrusionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { TileKey } from "@here/harp-geoutils";
import { ExtrusionFeature, ExtrusionFeatureDefs } from "@here/harp-materials";
import { MathUtils } from "@here/harp-utils";
import { DataSource } from "./DataSource";
import { MapView } from "./MapView";
import { Tile } from "./Tile";

Expand All @@ -26,13 +27,16 @@ export enum AnimatedExtrusionState {
}

const DEFAULT_EXTRUSION_DURATION = 750; // milliseconds
const DEFAULT_MIN_ZOOM_LEVEL = 16;
const DEFAULT_MIN_ZOOM_LEVEL = 1;

interface TileExtrusionState {
materials: ExtrusionFeature[];
animated: boolean;
}

// key is tile's morton code.
type TileMap = Map<number, TileExtrusionState>;

/**
* Handles animated extrusion effect of the buildings in {@link MapView}.
*/
Expand All @@ -49,8 +53,7 @@ export class AnimatedExtrusionHandler {
private m_minZoomLevel: number = DEFAULT_MIN_ZOOM_LEVEL;
private m_forceEnabled: boolean = false;

// key is tile's morton code.
private readonly m_tileMap: Map<number, TileExtrusionState> = new Map();
private readonly m_dataSourceMap: Map<DataSource, TileMap> = new Map();
private m_state: AnimatedExtrusionState = AnimatedExtrusionState.None;
private m_startTime: number = -1;

Expand Down Expand Up @@ -98,8 +101,7 @@ export class AnimatedExtrusionHandler {
return false;
}

const techniqueHasMinZl = technique.hasOwnProperty("minZoomLevel");
if (techniqueHasMinZl) {
if (technique.hasOwnProperty("minZoomLevel")) {
this.m_minZoomLevel = (technique as any).minZoomLevel;
}

Expand All @@ -113,7 +115,7 @@ export class AnimatedExtrusionHandler {

const animateExtrusionValue = getPropertyValue(technique.animateExtrusion, env);

if (animateExtrusionValue === null && techniqueHasMinZl) {
if (animateExtrusionValue === null) {
return this.enabled;
}

Expand All @@ -129,7 +131,7 @@ export class AnimatedExtrusionHandler {
* @internal
*/
update(zoomLevel: number) {
const extrusionVisible = this.m_tileMap.size > 0 && zoomLevel >= this.m_minZoomLevel;
const extrusionVisible = this.m_dataSourceMap.size > 0 && zoomLevel >= this.m_minZoomLevel;

if (this.m_state === AnimatedExtrusionState.None && extrusionVisible) {
this.m_state = AnimatedExtrusionState.Started;
Expand Down Expand Up @@ -161,7 +163,10 @@ export class AnimatedExtrusionHandler {
this.resetAnimation(false);
}
}
this.m_tileMap.set(tile.tileKey.mortonCode(), { materials, animated });
this.getOrCreateTileMap(tile.dataSource).set(tile.tileKey.mortonCode(), {
materials,
animated
});
}

/**
Expand All @@ -174,6 +179,19 @@ export class AnimatedExtrusionHandler {
);
}

private getTileMap(dataSource: DataSource, create: boolean = false): TileMap | undefined {
return this.m_dataSourceMap.get(dataSource);
}

private getOrCreateTileMap(dataSource: DataSource): TileMap {
let tileMap = this.m_dataSourceMap.get(dataSource);
if (!tileMap) {
tileMap = new Map();
this.m_dataSourceMap.set(dataSource, tileMap);
}
return tileMap;
}

private skipAnimation(tile: Tile): boolean {
return this.wasAnyAncestorAnimated(tile) || this.wasAnyDescendantAnimated(tile);
}
Expand All @@ -185,11 +203,14 @@ export class AnimatedExtrusionHandler {
distanceToMinLevel,
this.m_mapView.visibleTileSet.options.quadTreeSearchDistanceUp
);

const tileMap = this.getTileMap(tile.dataSource);
if (!tileMap) {
return false;
}
let lastTileKey = tile.tileKey;
for (let deltaUp = 1; deltaUp <= levelsUp; ++deltaUp) {
lastTileKey = lastTileKey.parent();
if (this.m_tileMap.get(lastTileKey.mortonCode())?.animated ?? false) {
if (tileMap.get(lastTileKey.mortonCode())?.animated ?? false) {
return true;
}
}
Expand All @@ -203,14 +224,18 @@ export class AnimatedExtrusionHandler {
this.m_mapView.visibleTileSet.options.quadTreeSearchDistanceDown
);

const tileMap = this.getTileMap(tile.dataSource);
if (!tileMap) {
return false;
}
const tilingScheme = tile.dataSource.getTilingScheme();
let nextTileKeys = [tile.tileKey];
let childTileKeys: TileKey[] = [];
for (let deltaDown = 1; deltaDown <= levelsDown; ++deltaDown) {
childTileKeys.length = 0;
for (const tileKey of nextTileKeys) {
for (const childTileKey of tilingScheme.getSubTileKeys(tileKey)) {
if (this.m_tileMap.get(childTileKey.mortonCode())?.animated ?? false) {
if (tileMap.get(childTileKey.mortonCode())?.animated ?? false) {
return true;
}
childTileKeys.push(childTileKey);
Expand All @@ -224,7 +249,17 @@ export class AnimatedExtrusionHandler {
}

private removeTile(tile: Tile): void {
this.m_tileMap.delete(tile.tileKey.mortonCode());
const tileMap = this.getTileMap(tile.dataSource);
if (!tileMap) {
return;
}
tileMap.delete(tile.tileKey.mortonCode());

// Remove tile map if it's empty. That way, counting the number of data sources in the
// map is enough to know if there's any tile.
if (tileMap.size === 0) {
this.m_dataSourceMap.delete(tile.dataSource);
}
}

private animateExtrusion() {
Expand Down Expand Up @@ -258,19 +293,23 @@ export class AnimatedExtrusionHandler {
this.m_state = AnimatedExtrusionState.None;
this.m_startTime = -1;
if (resetTiles) {
this.m_tileMap.forEach(state => {
state.animated = false;
this.m_dataSourceMap.forEach(tileMap => {
tileMap.forEach(state => {
state.animated = false;
});
});
}
}
private setExtrusionRatio(value: number) {
this.m_tileMap.forEach(state => {
if (!state.animated) {
this.setTileExtrusionRatio(state.materials, value);
if (value >= 1) {
state.animated = true;
this.m_dataSourceMap.forEach(tileMap => {
tileMap.forEach(state => {
if (!state.animated) {
this.setTileExtrusionRatio(state.materials, value);
if (value >= 1) {
state.animated = true;
}
}
}
});
});
}

Expand Down
71 changes: 31 additions & 40 deletions @here/harp-mapview/test/AnimatedExtrusionHandlerTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ExtrusionFeature } from "@here/harp-materials";
import { expect } from "chai";
import * as sinon from "sinon";
import { AnimatedExtrusionHandler } from "../lib/AnimatedExtrusionHandler";
import { DataSource } from "../lib/DataSource";
import { MapView } from "../lib/MapView";
import { Tile } from "../lib/Tile";

Expand All @@ -37,28 +38,29 @@ class FakeDataSource {
}
}

class FakeTile {
removeTileCallback?: (tile: Tile) => void;
readonly dataSource = new FakeDataSource();

constructor(readonly tileKey: TileKey) {}

addDisposeCallback(callback: (tile: Tile) => void) {
this.removeTileCallback = callback;
}
}

describe("AnimatedExtrusionHandler", function() {
const minZoomLevel = 17;
let mapView: MapView;
let defaultDataSource: DataSource;
let handler: AnimatedExtrusionHandler;
let technique: Technique;
const env = new MapEnv({});
let clock: sinon.SinonFakeTimers;

class FakeTile {
removeTileCallback?: (tile: Tile) => void;

constructor(readonly tileKey: TileKey, readonly dataSource = defaultDataSource) {}

addDisposeCallback(callback: (tile: Tile) => void) {
this.removeTileCallback = callback;
}
}

beforeEach(() => {
technique = { name: "extruded-polygon", minZoomLevel } as any;
mapView = new FakeMapView() as any;
defaultDataSource = new FakeDataSource() as any;
handler = new AnimatedExtrusionHandler(mapView);
clock = sinon.useFakeTimers();
});
Expand All @@ -84,35 +86,13 @@ describe("AnimatedExtrusionHandler", function() {
});

// tslint:disable-next-line: max-line-length
it("returns forced enabled if technique has minZoomLevel but not animateExtrusion", function() {
{
const enabled = handler.setAnimationProperties(
{
name: "extruded-polygon",
minZoomLevel: 5
} as any,
env
);
expect(enabled).to.be.true;
}

{
handler.enabled = false;
const enabled = handler.setAnimationProperties(
{ name: "extruded-polygon", minZoomLevel: 5 } as any,
env
);
expect(enabled).to.be.false;
}
});

it("returns false if technique has neither minZoomLevel nor animateExtrusion", function() {
it("returns forced enabled if technique does not define animateExtrusion", function() {
{
const enabled = handler.setAnimationProperties(
{ name: "extruded-polygon" } as any,
env
);
expect(enabled).to.be.false;
expect(enabled).to.be.true;
}

{
Expand Down Expand Up @@ -231,23 +211,34 @@ describe("AnimatedExtrusionHandler", function() {
});

it("updates extrusion ratio", function() {
const material: ExtrusionFeature = { extrusionRatio: 0 };
const material1: ExtrusionFeature = { extrusionRatio: 0 };
const material2: ExtrusionFeature = { extrusionRatio: 0 };
const duration = handler.duration;
const tile = new FakeTile(new TileKey(1, 2, minZoomLevel));
// two tiles with same key but different data source.
const tile1 = new FakeTile(new TileKey(1, 2, minZoomLevel));
const tile2 = new FakeTile(
new TileKey(1, 2, minZoomLevel),
new FakeDataSource() as any
);

handler.setAnimationProperties(technique, env);
handler.add(tile as any, [material]);
handler.add(tile1 as any, [material1]);
handler.add(tile2 as any, [material2]);
handler.update(minZoomLevel);

clock.tick(duration / 2);
handler.update(minZoomLevel);
expect(material.extrusionRatio)
expect(material1.extrusionRatio)
.to.be.greaterThan(0)
.and.lessThan(1);
expect(material2.extrusionRatio)
.to.be.greaterThan(0)
.and.lessThan(1);

clock.tick(duration / 2);
handler.update(minZoomLevel);
expect(material.extrusionRatio).to.be.equal(1);
expect(material1.extrusionRatio).to.be.equal(1);
expect(material2.extrusionRatio).to.be.equal(1);
expect(handler.isAnimating).to.be.false;
});
});
Expand Down

0 comments on commit d4275c6

Please sign in to comment.