From 160380025f513f11955882fd5a70af006a1600b6 Mon Sep 17 00:00:00 2001 From: Kyle Hensel Date: Wed, 5 Aug 2026 22:55:06 +1000 Subject: [PATCH] convert coreHistory to typescript --- modules/core/context.ts | 10 +- modules/core/graph.ts | 2 +- modules/core/{history.js => history.ts} | 625 +++++++++++++----------- modules/ui/loading.js | 1 + modules/util/class.ts | 16 + 5 files changed, 358 insertions(+), 296 deletions(-) rename modules/core/{history.js => history.ts} (51%) create mode 100644 modules/util/class.ts diff --git a/modules/core/context.ts b/modules/core/context.ts index 4087192bace..4438d26d3bc 100644 --- a/modules/core/context.ts +++ b/modules/core/context.ts @@ -165,7 +165,7 @@ export interface coreContext extends Pick, 'on'> { graph(): coreGraph; pauseChangeDispatch(): void; resumeChangeDispatch(): void; - perform: any; + perform: coreHistory['perform']; replace: coreHistory['replace']; pop: coreHistory['pop']; undo: coreHistory['undo']; @@ -692,10 +692,10 @@ export function coreContext(this: object): coreContext { // of instantiation shouldn't matter. function instantiateInternal() { - _history = coreHistory(context); - context.graph = _history.graph; - context.pauseChangeDispatch = _history.pauseChangeDispatch; - context.resumeChangeDispatch = _history.resumeChangeDispatch; + _history = new coreHistory(context); + context.graph = () => _history.graph(); + context.pauseChangeDispatch = () => _history.pauseChangeDispatch(); + context.resumeChangeDispatch = () => _history.resumeChangeDispatch(); context.perform = withDebouncedSave(_history.perform); context.replace = withDebouncedSave(_history.replace); context.pop = withDebouncedSave(_history.pop); diff --git a/modules/core/graph.ts b/modules/core/graph.ts index 58c96e5dac8..b56730dd7e0 100644 --- a/modules/core/graph.ts +++ b/modules/core/graph.ts @@ -315,7 +315,7 @@ export class coreGraph { } // Obliterates any existing entities - load(entities: { [key: EntityId | number]: iD.OsmEntity }) { + load(entities: { [key: EntityId | number]: iD.OsmEntity | undefined }) { var base = this.base(); this.entities = Object.create(base.entities); diff --git a/modules/core/history.js b/modules/core/history.ts similarity index 51% rename from modules/core/history.js rename to modules/core/history.ts index c99ff709cc1..2a335d3f0d6 100644 --- a/modules/core/history.js +++ b/modules/core/history.ts @@ -1,26 +1,26 @@ -import { dispatch as d3_dispatch } from 'd3-dispatch'; import { easeLinear as d3_easeLinear } from 'd3-ease'; import { select as d3_select } from 'd3-selection'; +import type { ZoomTransform } from 'd3-zoom'; import { asyncPrefs, prefs } from './preferences'; import { coreDifference } from './difference'; import { coreGraph } from './graph'; import { coreTree } from './tree'; -import { createEntity } from '../osm/create-entity'; +import type { EntityId, NodeId, OsmEntity, RelationId, WayId } from '../osm'; +import { createEntity, osmIdManager } from '../osm'; +import type { RelationMember } from '../osm/relation'; import { uiLoading } from '../ui/loading'; import { utilArrayDifference, utilArrayGroupBy, utilArrayUnion, - utilObjectOmit, utilRebind, utilSessionMutex + utilObjectOmit, utilSessionMutex } from '../util'; -import { osmIdManager } from '../osm'; - -/** - * @import { WayId, OsmEntity, EntityId } from '../osm'; - * @import { coreGraph } from './graph'; - * @import { LocalizedTextRenderer } from './localizer'; - * @import { Vec2 } from '../geo/vector'; - * @template [T = never] - * @typedef {{ +import type { geoExtent } from '../geo'; +import { EventDispatcher } from '../util/class'; +import type { Vec2 } from '../geo/vector'; +import type { coreContext } from './context'; +import type { LocalizedTextRenderer } from './localizer'; + +export interface Action { (graph: coreGraph, t?: number | null, extraData?: T): coreGraph id?: string; getWayId?(): WayId; @@ -30,9 +30,9 @@ import { osmIdManager } from '../osm'; copies?(): Record; useLongAxis?: GetSet; getReflectAxis?(graph: coreGraph): Vec2[]; - }} Action */ +} -/** @typedef {{ +export interface Operation { (event?: KeyboardEvent): void; available(situation: string): boolean | string | undefined; disabled(): false | string; @@ -49,46 +49,95 @@ import { osmIdManager } from '../osm'; point?(point: Vec2): unknown; getAuxiliaryGeometry?(): { id: string; - path: string; + path: string | null; klass: string; }[]; interrupts?: { [interruptId: string]: () => Promise; }; -}} Operation */ +} -/** @typedef {(context: iD.Context, selectedIDs: EntityId[]) => Operation} CreateOperation */ +export type CreateOperation = (context: iD.Context, selectedIDs: EntityId[]) => Operation; -/** @typedef {ReturnType} coreHistory */ +interface Stack { + graph: coreGraph; + annotation?: string; + imageryUsed?: string[]; + photoOverlaysUsed?: string[]; + transform?: ZoomTransform; + selectedIDs?: EntityId[]; +} -export function coreHistory(context) { - var dispatch = d3_dispatch('reset', 'change', 'merge', 'restore', 'undone', 'redone', 'storage_error'); - var lock = utilSessionMutex('lock'); +interface SerialisedStack extends Omit { + modified?: EntityId[]; + deleted?: EntityId[]; +} - // restorable if iD not open in another window/tab and a saved history exists in localStorage - var _hasUnresolvedRestorableChanges = lock.lock() && !!prefs('has_saved_history'); +interface IntroGraphEntity { + id: EntityId; + tags?: Tags; + loc?: Vec2; + nodes?: NodeId[]; + members?: RelationMember[]; +} - var duration = 150; - var _imageryUsed = []; - var _photoOverlaysUsed = []; - var _checkpoints = {}; - var _pausedGraph; - var _stack; - var _index; - /** @type {coreTree} */ - var _tree; +interface SerialisedHistory { + version: 2 | 3; + entities: OsmEntity[]; + baseEntities: OsmEntity[]; + stack: SerialisedStack[]; + nextIDs: typeof osmIdManager.next; + index: number; + timestamp: number; +} +type EventMap = { + reset: []; + change: [difference?: coreDifference] + merge: [entities: OsmEntity[]]; + restore: []; + undone: [stack: Stack, previousStack: Stack]; + redone: [stack: Stack, previousStack: Stack]; + storage_error: []; +} + +/** the last 'action' can optionally be a string annotation */ +export type ActionList = Action[] | [...actions: Action[], annotation: string] + +export class coreHistory extends EventDispatcher { + private context: coreContext; + + private _lock = utilSessionMutex('lock'); + + // restorable if iD not open in another window/tab and a saved history exists in localStorage + private _hasUnresolvedRestorableChanges = this._lock.lock() && !!prefs('has_saved_history'); + + private duration = 150; + private _imageryUsed: string[] = []; + private _photoOverlaysUsed: string[] = []; + private _checkpoints: { [key: string]: { stack: Stack[]; index: number; } } = {}; + private _pausedGraph: coreGraph | undefined | null; + + private _stack!: Stack[]; + private _index!: number; + private _tree!: coreTree; + + constructor(context: coreContext) { + super('reset', 'change', 'merge', 'restore', 'undone', 'redone', 'storage_error'); + this.context = context; + this.reset(); + } // internal _act, accepts list of actions and eased time - function _act(actions, t) { + private _act(actions: ActionList, t?: number): Stack { actions = Array.prototype.slice.call(actions); var annotation; if (typeof actions[actions.length - 1] !== 'function') { - annotation = actions.pop(); + annotation = actions.pop() as unknown as string; } - var graph = _stack[_index].graph; + var graph = this._stack[this._index].graph; for (var i = 0; i < actions.length; i++) { graph = actions[i](graph, t); } @@ -96,87 +145,86 @@ export function coreHistory(context) { return { graph: graph, annotation: annotation, - imageryUsed: _imageryUsed, - photoOverlaysUsed: _photoOverlaysUsed, - transform: context.projection.transform(), - selectedIDs: context.selectedIDs() + imageryUsed: this._imageryUsed, + photoOverlaysUsed: this._photoOverlaysUsed, + transform: this.context.projection.transform(), + selectedIDs: this.context.selectedIDs() }; } // internal _perform with eased time - function _perform(args, t) { - var previous = _stack[_index].graph; - _stack = _stack.slice(0, _index + 1); - var actionResult = _act(args, t); - _stack.push(actionResult); - _index++; - return change(previous); + private _perform(args: ActionList, t?: number): coreDifference { + var previous = this._stack[this._index].graph; + this._stack = this._stack.slice(0, this._index + 1); + var actionResult = this._act(args, t); + this._stack.push(actionResult); + this._index++; + return this.change(previous); } // internal _replace with eased time - function _replace(args, t) { - var previous = _stack[_index].graph; + private _replace(args: ActionList, t: number): coreDifference { + var previous = this._stack[this._index].graph; // assert(_index == _stack.length - 1) - var actionResult = _act(args, t); - _stack[_index] = actionResult; - return change(previous); + var actionResult = this._act(args, t); + this._stack[this._index] = actionResult; + return this.change(previous); } // internal _overwrite with eased time - function _overwrite(args, t) { - var previous = _stack[_index].graph; - if (_index > 0) { - _index--; - _stack.pop(); - } - _stack = _stack.slice(0, _index + 1); - var actionResult = _act(args, t); - _stack.push(actionResult); - _index++; - return change(previous); + private _overwrite(args: ActionList, t: number): coreDifference { + var previous = this._stack[this._index].graph; + if (this._index > 0) { + this._index--; + this._stack.pop(); + } + this._stack = this._stack.slice(0, this._index + 1); + var actionResult = this._act(args, t); + this._stack.push(actionResult); + this._index++; + return this.change(previous); } // determine difference and dispatch a change event - function change(previous) { - var difference = coreDifference(previous, history.graph()); - if (!_pausedGraph) { - dispatch.call('change', this, difference); + private change(previous: coreGraph): coreDifference { + var difference = coreDifference(previous, this.graph()); + if (!this._pausedGraph) { + this.dispatch.call('change', this, difference); } return difference; } - var history = { - - graph: function() { - return _stack[_index].graph; - }, + graph() { + return this._stack[this._index].graph; + } - tree: function() { - return _tree; - }, + tree() { + return this._tree; + } - base: function() { - return _stack[0].graph; - }, + base() { + return this._stack[0].graph; + } - merge: function(entities/*, extent*/) { - var stack = _stack.map(function(state) { return state.graph; }); - _stack[0].graph.rebase(entities, stack, false); - _tree.rebase(entities, false); + merge(entities: OsmEntity[]) { + var stack = this._stack.map(function(state) { return state.graph; }); + this._stack[0].graph.rebase(entities, stack, false); + this._tree.rebase(entities, false); - dispatch.call('merge', this, entities); - }, + this.dispatch.call('merge', this, entities); + } - perform: function() { + perform(...args: ActionList): coreDifference | Promise; + perform() { // complete any transition already in progress d3_select(document).interrupt('history.perform'); @@ -190,138 +238,138 @@ export function coreHistory(context) { if (transitionable) { var origArguments = arguments; - return new Promise(resolve => { + return new Promise(resolve => { d3_select(document) .transition('history.perform') - .duration(duration) + .duration(this.duration) .ease(d3_easeLinear) - .tween('history.tween', function() { - return function(t) { - if (t < 1) _overwrite([action0], t); + .tween('history.tween', () => { + return (t) => { + if (t < 1) this._overwrite([action0], t); }; }) - .on('start', function() { - resolve(_perform([action0], 0)); + .on('start', () => { + resolve(this._perform([action0], 0)); }) - .on('end interrupt', function() { - resolve(_overwrite(origArguments, 1)); + .on('end interrupt', () => { + resolve(this._overwrite(origArguments as unknown as Action[], 1)); }); }); } else { - return _perform(arguments); + return this._perform(arguments as unknown as Action[]); } - }, + } - replace: function() { + replace(...args: ActionList) { d3_select(document).interrupt('history.perform'); - return _replace(arguments); - }, + return this._replace(args, 1); + } - pop: function(n) { + pop(n?: number) { d3_select(document).interrupt('history.perform'); - var previous = _stack[_index].graph; - if (isNaN(+n) || +n < 0) { + var previous = this._stack[this._index].graph; + if (typeof n === 'undefined' || isNaN(+n) || +n < 0) { n = 1; } - while (n-- > 0 && _index > 0) { - _index--; - _stack.pop(); + while (n-- > 0 && this._index > 0) { + this._index--; + this._stack.pop(); } - return change(previous); - }, + return this.change(previous); + } // Back to the previous annotated state or _index = 0. - undo: function() { + undo() { d3_select(document).interrupt('history.perform'); - var previousStack = _stack[_index]; + var previousStack = this._stack[this._index]; var previous = previousStack.graph; - while (_index > 0) { - _index--; - if (_stack[_index].annotation) break; + while (this._index > 0) { + this._index--; + if (this._stack[this._index].annotation) break; } - dispatch.call('undone', this, _stack[_index], previousStack); - return change(previous); - }, + this.dispatch.call('undone', this, this._stack[this._index], previousStack); + return this.change(previous); + } // Forward to the next annotated state. - redo: function() { + redo() { d3_select(document).interrupt('history.perform'); - var previousStack = _stack[_index]; + var previousStack = this._stack[this._index]; var previous = previousStack.graph; - var tryIndex = _index; - while (tryIndex < _stack.length - 1) { + var tryIndex = this._index; + while (tryIndex < this._stack.length - 1) { tryIndex++; - if (_stack[tryIndex].annotation) { - _index = tryIndex; - dispatch.call('redone', this, _stack[_index], previousStack); + if (this._stack[tryIndex].annotation) { + this._index = tryIndex; + this.dispatch.call('redone', this, this._stack[this._index], previousStack); break; } } - return change(previous); - }, + return this.change(previous); + } - pauseChangeDispatch: function() { - if (!_pausedGraph) { - _pausedGraph = _stack[_index].graph; + pauseChangeDispatch() { + if (!this._pausedGraph) { + this._pausedGraph = this._stack[this._index].graph; } - }, + } - resumeChangeDispatch: function() { - if (_pausedGraph) { - var previous = _pausedGraph; - _pausedGraph = null; - return change(previous); + resumeChangeDispatch() { + if (this._pausedGraph) { + var previous = this._pausedGraph; + this._pausedGraph = null; + return this.change(previous); } - }, + } - undoAnnotation: function() { - var i = _index; + undoAnnotation() { + var i = this._index; while (i >= 0) { - if (_stack[i].annotation) return _stack[i].annotation; + if (this._stack[i].annotation) return this._stack[i].annotation; i--; } - }, + } - redoAnnotation: function() { - var i = _index + 1; - while (i <= _stack.length - 1) { - if (_stack[i].annotation) return _stack[i].annotation; + redoAnnotation() { + var i = this._index + 1; + while (i <= this._stack.length - 1) { + if (this._stack[i].annotation) return this._stack[i].annotation; i++; } - }, + } // Returns the entities from the active graph with bounding boxes // overlapping the given `extent`. - intersects: function(extent) { - return _tree.intersects(extent, _stack[_index].graph); - }, + intersects(extent: geoExtent) { + return this._tree.intersects(extent, this._stack[this._index].graph); + } - difference: function() { - var base = _stack[0].graph; - var head = _stack[_index].graph; + difference() { + var base = this._stack[0].graph; + var head = this._stack[this._index].graph; return coreDifference(base, head); - }, + } - changes: function(action) { - var base = _stack[0].graph; - var head = _stack[_index].graph; + changes(action?: Action) { + var base = this._stack[0].graph; + var head = this._stack[this._index].graph; if (action) { head = action(head); @@ -334,27 +382,27 @@ export function coreHistory(context) { created: difference.created(), deleted: difference.deleted() }; - }, + } changesCount() { return Object.values(this.changes()).flat().length; - }, + } - hasChanges: function() { + hasChanges() { return this.difference().length() > 0; - }, + } - imageryUsed: function(sources) { + imageryUsed(sources?: string[]) { if (sources) { - _imageryUsed = sources; - return history; + this._imageryUsed = sources; + return this; } else { var s = new Set(); - _stack.slice(1, _index + 1).forEach(function(state) { - state.imageryUsed.forEach(function(source) { + this._stack.slice(1, this._index + 1).forEach(function(state) { + state.imageryUsed!.forEach(function(source) { if (source !== 'Custom') { s.add(source); } @@ -362,16 +410,16 @@ export function coreHistory(context) { }); return Array.from(s); } - }, + } - photoOverlaysUsed: function(sources) { + photoOverlaysUsed(sources?: string[]) { if (sources) { - _photoOverlaysUsed = sources; - return history; + this._photoOverlaysUsed = sources; + return this; } else { var s = new Set(); - _stack.slice(1, _index + 1).forEach(function(state) { + this._stack.slice(1, this._index + 1).forEach(function(state) { if (state.photoOverlaysUsed && Array.isArray(state.photoOverlaysUsed)) { state.photoOverlaysUsed.forEach(function(photoOverlay) { s.add(photoOverlay); @@ -380,35 +428,35 @@ export function coreHistory(context) { }); return Array.from(s); } - }, + } // save the current history state - checkpoint: function(key) { - _checkpoints[key] = { - stack: _stack, - index: _index + checkpoint(key: string) { + this._checkpoints[key] = { + stack: this._stack, + index: this._index }; - return history; - }, + return this; + } // restore history state to a given checkpoint or reset completely - reset: function(key) { - if (key !== undefined && _checkpoints.hasOwnProperty(key)) { - _stack = _checkpoints[key].stack; - _index = _checkpoints[key].index; + reset(key?: string) { + if (key !== undefined && this._checkpoints.hasOwnProperty(key)) { + this._stack = this._checkpoints[key].stack; + this._index = this._checkpoints[key].index; } else { - _stack = [{graph: new coreGraph()}]; - _index = 0; - _tree = coreTree(_stack[0].graph); - _checkpoints = {}; + this._stack = [{graph: new coreGraph()}]; + this._index = 0; + this._tree = coreTree(this._stack[0].graph); + this._checkpoints = {}; } - _pausedGraph = null; - dispatch.call('reset'); - dispatch.call('change'); - return history; - }, + this._pausedGraph = null; + this.dispatch.call('reset'); + this.dispatch.call('change'); + return this; + } // `toIntroGraph()` is used to export the intro graph used by the walkthrough. @@ -421,15 +469,15 @@ export function coreHistory(context) { // `id.history().toIntroGraph()` // 5. This outputs stringified JSON to the browser console // 6. Copy it to `data/intro_graph.json` and prettify it in your code editor - toIntroGraph: function() { + toIntroGraph() { var nextID = { n: 0, r: 0, w: 0 }; - var permIDs = {}; + var permIDs: { [T in EntityId]: T } = {}; var graph = this.graph(); - var baseEntities = {}; + var baseEntities: { [entityId: EntityId]: IntroGraphEntity } = {}; // clone base entities.. Object.values(graph.base().entities).forEach(function(entity) { - var copy = copyIntroEntity(entity); + var copy = copyIntroEntity(entity!); baseEntities[copy.id] = copy; }); @@ -462,8 +510,8 @@ export function coreHistory(context) { return JSON.stringify({ dataIntroGraph: baseEntities }); - function copyIntroEntity(source) { - var copy = utilObjectOmit(source, ['type', 'user', 'v', 'version', 'visible']); + function copyIntroEntity(source: OsmEntity) { + var copy = utilObjectOmit(source, ['type', 'user', 'v', 'version', 'visible']) as IntroGraphEntity; // Note: the copy is no longer an osmEntity, so it might not have `tags` if (copy.tags && !Object.keys(copy.tags)) { @@ -477,9 +525,9 @@ export function coreHistory(context) { var match = source.id.match(/([nrw])-\d*/); // temporary id if (match !== null) { - var nrw = match[1]; + var nrw = match[1] as 'n' | 'w' | 'r'; var permID; - do { permID = nrw + (++nextID[nrw]); } + do { permID = (nrw + (++nextID[nrw])) as NodeId & WayId & RelationId; } while (baseEntities.hasOwnProperty(permID)); copy.id = permID; @@ -487,19 +535,19 @@ export function coreHistory(context) { } return copy; } - }, + } - toJSON: function() { + toJSON() { if (!this.hasChanges()) return; - var allEntities = {}; - var baseEntities = {}; - var base = _stack[0]; + var allEntities: { [key: EntityId]: OsmEntity } = {}; + var baseEntities: { [id: EntityId]: OsmEntity } = {}; + var base = this._stack[0]; - var s = _stack.map(function(i) { - var modified = []; - var deleted = []; + var s = this._stack.map(function(i) { + var modified: EntityId[] = []; + var deleted: EntityId[] = []; Object.keys(i.graph.entities).forEach(function(id) { var entity = i.graph.entities[id]; @@ -514,13 +562,13 @@ export function coreHistory(context) { // make sure that the originals of changed or deleted entities get merged // into the base of the _stack after restoring the data from JSON. if (id in base.graph.entities) { - baseEntities[id] = base.graph.entities[id]; + baseEntities[id] = base.graph.entities[id]!; } - if (entity && entity.nodes) { + if (entity?.type === 'way' && entity.nodes) { // get originals of pre-existing child nodes entity.nodes.forEach(function(nodeID) { if (nodeID in base.graph.entities) { - baseEntities[nodeID] = base.graph.entities[nodeID]; + baseEntities[nodeID] = base.graph.entities[nodeID]!; } }); } @@ -529,13 +577,13 @@ export function coreHistory(context) { if (baseParents) { baseParents.forEach(function(parentID) { if (parentID in base.graph.entities) { - baseEntities[parentID] = base.graph.entities[parentID]; + baseEntities[parentID] = base.graph.entities[parentID]!; } }); } }); - var x = {}; + var x: SerialisedStack = {}; if (modified.length) x.modified = modified; if (deleted.length) x.deleted = deleted; @@ -548,30 +596,31 @@ export function coreHistory(context) { return x; }); - return { + const serialised: SerialisedHistory = { version: 3, entities: Object.values(allEntities), baseEntities: Object.values(baseEntities), stack: s, nextIDs: osmIdManager.next, - index: _index, + index: this._index, // note the time the changes were saved timestamp: (new Date()).getTime() }; - }, + return serialised; + } - fromJSON: function(h, loadChildNodes) { + fromJSON(h: SerialisedHistory, loadChildNodes?: boolean) { var loadComplete = true; osmIdManager.next = h.nextIDs; - _index = h.index; + this._index = h.index; if (h.version === 2 || h.version === 3) { - var allEntities = {}; + var allEntities: { [key: string]: OsmEntity } = {}; - h.entities.forEach(function(rawEntity) { - const entity = createEntity(rawEntity); + h.entities.forEach(function(entityObject) { + const entity = createEntity(entityObject) as OsmEntity; allEntities[osmIdManager.key(entity)] = entity; }); @@ -579,30 +628,30 @@ export function coreHistory(context) { // This merges originals for changed entities into the base of // the _stack even if the current _stack doesn't have them (for // example when iD has been restarted in a different region) - var baseEntities = h.baseEntities.map(function(d) { return createEntity(d); }); - var stack = _stack.map(function(state) { return state.graph; }); - _stack[0].graph.rebase(baseEntities, stack, true); - _tree.rebase(baseEntities, true); + var baseEntities = h.baseEntities.map(function(d) { return createEntity(d) as OsmEntity; }); + var stack = this._stack.map(function(state) { return state.graph; }); + this._stack[0].graph.rebase(baseEntities, stack, true); + this._tree.rebase(baseEntities, true); // When we restore a modified way, we also need to fetch any missing // childnodes that would normally have been downloaded with it.. #2142 if (loadChildNodes) { - var osm = context.connection(); + var osm = this.context.connection(); var baseWays = baseEntities .filter(function(e) { return e.type === 'way'; }); var nodeIDs = baseWays - .reduce(function(acc, way) { return utilArrayUnion(acc, way.nodes); }, []); + .reduce(function(acc, way) { return utilArrayUnion(acc, way.nodes); }, []); var missing = nodeIDs - .filter(function(n) { return !_stack[0].graph.hasEntity(n); }); + .filter((n) => { return !this._stack[0].graph.hasEntity(n); }); if (missing.length && osm) { loadComplete = false; - context.map().redrawEnable(false); + this.context.map().redrawEnable(false); - var loading = uiLoading(context).blocking(true); - context.container().call(loading); + var loading = uiLoading(this.context).blocking(true); + this.context.container().call(loading); - var childNodesLoaded = function(err, result) { + var childNodesLoaded = (err: Error, result: { data: OsmEntity[] }) => { if (!err) { var visibleGroups = utilArrayGroupBy(result.data, 'visible'); var visibles = visibleGroups.true || []; // alive nodes @@ -610,23 +659,23 @@ export function coreHistory(context) { if (visibles.length) { var visibleIDs = visibles.map(function(entity) { return entity.id; }); - var stack = _stack.map(function(state) { return state.graph; }); + var stack = this._stack.map(function(state) { return state.graph; }); missing = utilArrayDifference(missing, visibleIDs); - _stack[0].graph.rebase(visibles, stack, true); - _tree.rebase(visibles, true); + this._stack[0].graph.rebase(visibles, stack, true); + this._tree.rebase(visibles, true); } // fetch older versions of nodes that were deleted.. invisibles.forEach(function(entity) { - osm.loadEntityVersion(entity.id, +entity.version - 1, childNodesLoaded); + osm.loadEntityVersion(entity.id, +entity.version! - 1, childNodesLoaded); }); } if (err || !missing.length) { loading.close(); - context.map().redrawEnable(true); - dispatch.call('change'); - dispatch.call('restore', this); + this.context.map().redrawEnable(true); + this.dispatch.call('change'); + this.dispatch.call('restore', this); } }; @@ -635,8 +684,8 @@ export function coreHistory(context) { } } - _stack = h.stack.map(function(d) { - var entities = {}, entity; + this._stack = h.stack.map((d) => { + var entities: { [key: EntityId]: OsmEntity | undefined } = {}, entity; if (d.modified) { d.modified.forEach(function(key) { @@ -652,7 +701,7 @@ export function coreHistory(context) { } return { - graph: new coreGraph(_stack[0].graph).load(entities), + graph: new coreGraph(this._stack[0].graph).load(entities), annotation: d.annotation, imageryUsed: d.imageryUsed, photoOverlaysUsed: d.photoOverlaysUsed, @@ -662,68 +711,71 @@ export function coreHistory(context) { }); } else { // original version - _stack = h.stack.map(function(d) { - var entities = {}; - - for (var i in d.entities) { - var entity = d.entities[i]; - entities[i] = entity === 'undefined' ? undefined : createEntity(entity); + this._stack = h.stack.map((d) => { + var entities: { [key: EntityId]: OsmEntity | undefined } = {}; + + // @ts-expect-error -- intentional, this is the legacy format from 13 years ago + const legacy = d.entities; + for (var _i in legacy) { + const i = _i; + var entity = legacy[i]; + entities[i] = (entity === 'undefined' ? undefined : createEntity(entity) as OsmEntity); } - d.graph = new coreGraph(_stack[0].graph).load(entities); - return d; + return { ...d, graph: new coreGraph(this._stack[0].graph).load(entities) }; }); } - var transform = _stack[_index].transform; + var transform = this._stack[this._index].transform; if (transform) { - context.map().transformEase(transform, 0); // 0 = immediate, no easing + this.context.map().transformEase(transform, 0); // 0 = immediate, no easing } if (loadComplete) { - dispatch.call('change'); - dispatch.call('restore', this); + this.dispatch.call('change'); + this.dispatch.call('restore', this); } - return history; - }, + return this; + } - lock: function() { - return lock.lock(); - }, + lock() { + return this._lock.lock(); + } - unlock: function() { - lock.unlock(); - }, + unlock() { + this._lock.unlock(); + } - save: function() { - if (lock.locked() && + save() { + if (this._lock.locked() && // don't overwrite existing, unresolved changes - !_hasUnresolvedRestorableChanges) { + !this._hasUnresolvedRestorableChanges) { - const historyData = history.toJSON(); + const historyData = this.toJSON(); if (!historyData) { asyncPrefs.del('saved_history') .then(() => prefs('has_saved_history', null)) - .catch(() => dispatch.call('storage_error')); + .catch(() => this.dispatch.call('storage_error')); } else { asyncPrefs.set('saved_history', historyData) .then(() => prefs('has_saved_history', true)) - .catch(() => dispatch.call('storage_error')); + .catch(() => this.dispatch.call('storage_error')); } } - return history; - }, + return this; + } // delete the history version saved in IndexedDB - clearSaved: function() { - context.debouncedSave.cancel(); - if (lock.locked()) { - _hasUnresolvedRestorableChanges = false; + clearSaved() { + this.context.debouncedSave.cancel(); + if (this._lock.locked()) { + + this._hasUnresolvedRestorableChanges = false; asyncPrefs.del('saved_history') .then(() => prefs('has_saved_history', null)); @@ -733,41 +785,34 @@ export function coreHistory(context) { prefs('hashtags', null); prefs('source', null); } - return history; - }, - + return this; + } - hasRestorableChanges: function() { - return _hasUnresolvedRestorableChanges; - }, + hasRestorableChanges() { + return this._hasUnresolvedRestorableChanges; + } - restore: async function() { - if (lock.locked()) { - _hasUnresolvedRestorableChanges = false; - var json = await asyncPrefs.get('saved_history'); - if (json) history.fromJSON(json, true); + async restore() { + if (this._lock.locked()) { + this._hasUnresolvedRestorableChanges = false; + var json = await asyncPrefs.get('saved_history'); + if (json) this.fromJSON(json, true); } - }, - + } - migrateHistoryData: async function() { - const value = JSON.parse(prefs(this._getLegacyKey('saved_history'))); + async migrateHistoryData() { + const value = JSON.parse(prefs(this._getLegacyKey('saved_history'))!); if (value !== null) { await asyncPrefs.set('saved_history', value); prefs('has_saved_history', true); prefs(this._getLegacyKey('saved_history'), null); } - }, + } // (legacy, was used for local-storage based history) // iD uses namespaced keys so multiple installations do not conflict - _getLegacyKey: n => 'iD_' + window.location.origin + '_' + n, - }; - - history.reset(); - - return utilRebind(history, dispatch, 'on'); + _getLegacyKey = (n: string) => 'iD_' + window.location.origin + '_' + n; } diff --git a/modules/ui/loading.js b/modules/ui/loading.js index 06527b68d8e..1dfbc77b952 100644 --- a/modules/ui/loading.js +++ b/modules/ui/loading.js @@ -45,6 +45,7 @@ export function uiLoading(context) { }; + /** @type {GetSet} */ loading.blocking = function(val) { if (!arguments.length) return _blocking; _blocking = val; diff --git a/modules/util/class.ts b/modules/util/class.ts new file mode 100644 index 00000000000..c0af02a7f6f --- /dev/null +++ b/modules/util/class.ts @@ -0,0 +1,16 @@ +import { dispatch, type Dispatch } from 'd3-dispatch'; +import { utilRebind } from './rebind'; + +/** + * This is a replacement for `utilRebind(this, dispatch, 'on')`, + * modern classes just need to extend this class, and call `super(…)` + */ +export abstract class EventDispatcher { + protected dispatch: Dispatch; + declare on: typeof this.dispatch.on; + + constructor(...args: (keyof EventMap)[]) { + this.dispatch = dispatch(...args); + utilRebind(this, this.dispatch, 'on'); + } +}