Skip to content

Commit

Permalink
added(editor): Introduced batch editing to group multiple feature mod…
Browse files Browse the repository at this point in the history
  • Loading branch information
TerminalTim committed Nov 25, 2024
1 parent bd18b18 commit b697ae4
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 15 deletions.
19 changes: 19 additions & 0 deletions packages/core/src/features/Feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ export class Feature<GeometryType = string> implements GeoJSONFeature<GeometryTy
// need for quick data merge across multiple tile searches
private _m: number = 0;

/**
* pointerenter event listener for internal use only
* @hidden
* @internal
*/
pointerenter: () => void;
/**
* pointerleave event listener for internal use only
* @hidden
* @internal
*/
pointerleave: () => void;
/**
* pointerup event listener for internal use only
* @hidden
* @internal
*/
pointerup: () => void;

constructor(feature: GeoJSONFeature<GeometryType>, prov?: FeatureProvider) {
this.id = feature.id;

Expand Down
99 changes: 98 additions & 1 deletion packages/editor/src/API/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,103 @@ export default class Editor {
return this._i().hooks.add(type, hook, provider);
};

/**
* Batches multiple feature edits into a single history entry.
* This allows combining multiple actions, such as modifying properties or coordinates,
* so that only a single history step is created for undo/redo purposes.
*
* Use this method when you want to group multiple feature modifications (such as setting properties or coordinates)
* into a single history step for easier undo/redo management.
*
* @param action A function that contains one or more feature modification actions. All edits within this function
* will be grouped together as a single history step.
*
* @example
* editor.batch(() => {
* feature.prop("name", "newName"); // Modify feature property
* feature.coord(newCoordinate); // Modify feature coordinates
* });
*
* @remarks
* This method is useful when you want to execute multiple edits in sequence but treat them as a single operation
* for undo/redo. The changes will be bundled into one history entry, simplifying the undo/redo process.
*
* @see {@link editor.undo} for undoing the last action.
* @see {@link editor.redo} for redoing the last undone action.
* @see {@link editor.beginBatch} for starting a batch of edits manually.
* @see {@link editor.endBatch} for finalizing a batch of edits manually.
*/
batch(action: () => void): void {
if (typeof action === 'function') {
this.beginBatch();
try {
action();
} finally {
this.endBatch();
}
}
}

/**
* Begins a new batch operation for multiple feature edits.
*
* Call this function before making a series of edits to a feature. All changes made between `beginBatch` and
* `endBatch` will be grouped together as a single history entry. This allows more control over when to
* create a history step for a series of edits.
*
* @example
* editor.startBatch(); // Start a batch operation
* feature.prop("name", "newName"); // Modify feature property
* feature.coord(newCoordinate); // Modify feature coordinates
* editor.endBatch(); // Finish the batch and commit changes as a single history entry
*
* @remarks
* This method is helpful when you want to make multiple edits and control when the changes are committed to history.
* The edits made within the `startBatch`/`endBatch` block are treated as a single operation.
*
* @see {@link editor.endBatch} for finalizing a batch operation.
* @see {@link editor.undo} for undoing the last action.
* @see {@link editor.redo} for redoing the last undone action.
* @see {@link editor.batch} for an alternative method to group edits without manually starting and ending a batch.
*/
beginBatch(): void {
this._b++;
this._i().objects.history.active(false);
}

// counter for "nested batch" handling
private _b: number = 0;

/**
* Ends the current batch operation and creates a single history entry for all changes made since `startBatch`.
*
* This function should be called after making all desired edits within a `startBatch` block. Once called,
* all changes will be committed as a single entry in the local history, enabling easy undo/redo of the entire batch.
*
* @example
* editor.startBatch(); // Start a batch operation
* feature.prop("name", "newName"); // Modify feature property
* feature.coord(newCoordinate); // Modify feature coordinates
* editor.endBatch(); // Finalize the batch and create a single history entry
*
* @remarks
* The `endBatch` method ensures that all modifications made within the batch are recorded as a single step in the local history.
* After calling this, you can undo or redo the entire set of changes together.
*
* @see {@link editor.startBatch} for beginning a batch operation.
* @see {@link editor.undo} for undoing the last action.
* @see {@link editor.redo} for redoing the last undone action.
* @see {@link editor.batch} for an alternative method to group feature edits into a single history step without manually starting and ending a batch.
*/
endBatch(): void {
const history = this._i().objects.history;
if (this._b > 0 && --this._b === 0) {
history.active(true);
history.saveChanges();
}
}


/**
* Remove a specific hook for the desired editing operation.
*
Expand Down Expand Up @@ -543,7 +640,7 @@ export default class Editor {
*
* @returns feature container
*/
createFeatureContainer(...features: (Feature|Feature[])[]): FeatureContainer {
createFeatureContainer(...features: (Feature | Feature[])[]): FeatureContainer {
const container = new Container(this._i());
container.push(features.flat());
return container;
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/features/History.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,10 @@ class History {
return this._a;
};

ignore(operation: () => void) {
let a = this.active();
batch(action: () => void) {
const a = this.active();
this.active(false);
operation();
action();
this.active(a);
};
}
Expand Down
8 changes: 4 additions & 4 deletions packages/editor/src/features/ObjectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const getOverlayIndex = (layers) => {
};

class ObjectManager {
private iEdit;
private iEdit: InternalEditor;
private display;
private listen: boolean = false;
private layers: Map<TileLayer>;
Expand Down Expand Up @@ -388,7 +388,7 @@ class ObjectManager {
featureHistory.origin(feature, true);

if (zLevels) {
featureHistory.ignore(() => {
featureHistory.batch(() => {
provider.writeZLevels(feature, zLevels);
});
}
Expand Down Expand Up @@ -445,7 +445,7 @@ class ObjectManager {
const provider = locationTools.getRoutingProvider(feature);
const routingLink = provider && provider.search(idMapping[cLinkId]);

featureHistory.ignore(() => {
featureHistory.batch(() => {
feature.getProvider().writeRoutingPoint(feature, routingLink, routingPoint.position);
});
}
Expand Down Expand Up @@ -510,7 +510,7 @@ class ObjectManager {
bbox[0], bbox[2], bbox[1], bbox[3]
)
) {
const crossing = iEdit.map.searchPointOnLine(line.geometry.coordinates, position, -1, UNDEF, UNDEF, options.ignoreZ);
const crossing = iEdit.map.searchPointOnLine(line.geometry.coordinates as GeoJSONCoordinate[], position, -1, UNDEF, UNDEF, options.ignoreZ);

if (crossing?.distance < Math.min(RESULT.distance, options.maxDistance)) {
RESULT.point = crossing.point;
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/features/feature/Feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class Feature extends GeoJSONFeature {
}

if (state != 'hovered' && state != 'selected') {
feature._e().objects.history.ignore(() => {
feature._e().objects.history.batch(() => {
feature._esu = true;
(<EditableProvider>feature.getProvider()).writeEditState(feature, state);
feature._esu = UNDEF;
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/features/link/Navlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class Navlink extends Feature {
}

if (updated) {
history.ignore(() => {
history.batch(() => {
link.getProvider().writeZLevels(link, zLevels);
});
// update zlevel visuals
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/src/features/link/NavlinkTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ var tools = {
// update/remove zlevels
const zLevels = <number[]>navlink.getZLevels();
zLevels.splice(index, 1);
navlink._e().objects.history.ignore(() => {
navlink._e().objects.history.batch(() => {
navlink.getProvider().writeZLevels(navlink, zLevels);
});

Expand Down Expand Up @@ -698,7 +698,7 @@ var tools = {
// fallback for old non 3d zlevel passed in via coordinates z position
let zLevel = Math.round(pos[2] < 10 && pos[2]) || 0;
zLevels.splice(index, 0, zLevel);
EDITOR.objects.history.ignore(() => {
EDITOR.objects.history.batch(() => {
link.getProvider().writeZLevels(link, zLevels);
});
// link.setZLevels(zLevels)
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/features/location/LocationTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ const tools = {
newRP[0] != round(curRP[0]) ||
newRP[1] != round(curRP[1])
) {
feature._e().objects.history.ignore(() => {
feature._e().objects.history.batch(() => {
// prevents infinity loop if rp writer is using .prop() function to set rp value
// because .prop() will call connect to make sure
// the connection gets created in case of rp val is changed
Expand Down
5 changes: 3 additions & 2 deletions packages/editor/src/tools/turnrestriction/TurnRestriction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import linkTools from '../../features/link/NavlinkTools';
import {isTurnAllowed, isPedestrianOnly, getProperty, setProperty} from './utils';
import Overlay from '../../features/Overlay';
import {Navlink} from '../../features/link/Navlink';
import InternalEditor from '../../IEditor';

const DISTANCE_METER = 8 * 1e-5;

Expand All @@ -49,7 +50,7 @@ class TurnRestriction {
from: Navlink;
to: Navlink;

constructor(HERE_WIKI, fromLink, fromShape, toLink, toShape, carPosition) {
constructor(HERE_WIKI: InternalEditor, fromLink, fromShape, toLink, toShape, carPosition) {
const overlay = HERE_WIKI.objects.overlay;
const nextShape = toShape == 0 ? 1 : toLink.coord().length - 2;
const path = toLink.coord();
Expand Down Expand Up @@ -95,7 +96,7 @@ class TurnRestriction {

sign.pointerup = () => {
if (isTurnAllowed(fromLink, fromShape, toLink, toShape) && !isPedestrianOnly(toLink)) {
HERE_WIKI.objects.history.ignore(() => {
HERE_WIKI.objects.history.batch(() => {
setProperty(curSign == 'ALLOWED', fromLink, fromShape, toLink, toShape);
});

Expand Down
48 changes: 48 additions & 0 deletions packages/playground/examples/editor/batch_changes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>XYZ Maps Example: Batch Changes</title>
<style type="text/css">
#map {
position: absolute;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
pre div, span div {
position: relative;
display: inline-block;
left: 20px;
background-color: #333;
color: #fff;
padding: 10px;
font-family: 'LFira Sans', Arial, Helvetica, sans-serif;
}
a.button {
position: relative;
display: block;
color: #FFFFFF;
cursor: pointer;
left: 20px;
height: 23px;
padding: 5px 6px 0px;
width: 150px;
text-align: center;
margin: 12px 0 0 0;
background-color: #333;
}
a.button:hover{
background-color: #555;
}
</style>
</head>
<body>
<div id="map"></div>
<a id="change" class="button">Apply Batched Changes</a>
<a id="undo" class="button">Undo</a>
<a id="redo" class="button">Redo</a>
</body>
</html>
79 changes: 79 additions & 0 deletions packages/playground/examples/editor/batch_changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {MVTLayer, TileLayer, SpaceProvider, GeoJSONCoordinate} from '@here/xyz-maps-core';
import {Map} from '@here/xyz-maps-display';
import {Editor, Marker} from '@here/xyz-maps-editor';

/** setup the Map **/
let backgroundLayer = new MVTLayer({
min: 1, max: 20,
remote: {
url: 'https://vector.hereapi.com/v2/vectortiles/base/mc/{z}/{x}/{y}/omv?apikey=' + YOUR_API_KEY
}
});
let myLayer = new TileLayer({
min: 14, max: 20,
provider: new SpaceProvider({
space: '6CkeaGLg',
credentials: {
access_token: YOUR_ACCESS_TOKEN
},
level: 14
}),
style: {
styleGroups: {
Point: [{
type: 'Circle', zIndex: 0, radius: 20, fill: '#ff7220', stroke: '#ff1d00aa', strokeWidth: 5
}, {
type: 'Text', zIndex: 1, text: ['get', 'counter'], fill: 'black', stroke: 'white', strokeWidth: 5, font: '22px sans-serif'
}]
},
assign() {
return 'Point';
}
}
});

myLayer.addFeature({
id: 'myFeature',
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [-116.85755, 33.03607]
},
properties: {
counter: 0
}
});

// setup the Map Display
const display = new Map(document.getElementById('map'), {
zoomlevel: 17,
center: {longitude: -116.85755, latitude: 33.03607},

// add layers to display
layers: [backgroundLayer, myLayer]
});

// setup the editor
const editor = new Editor(display);

// add the layer to the editor to enable editing of the layer
editor.addLayer(myLayer);
/** **/

document.querySelector<HTMLButtonElement>('#change').onclick = function() {
// Batch multiple changes to create a single history step
editor.batch(()=>{
const myFeature =<Marker>myLayer.search({id: 'myFeature'});
// First change: Increment the 'counter' property of the feature by
myFeature.prop('counter', myFeature.prop('counter') + 1);
// Second change: Adjust the feature's position slightly to the right
const currentPosition = <GeoJSONCoordinate>myFeature.coord();
myFeature.coord([currentPosition[0] + 0.0001, currentPosition[1]]);
});
};

document.querySelector<HTMLButtonElement>('#undo').onclick = () => editor.undo();

document.querySelector<HTMLButtonElement>('#redo').onclick = () => editor.redo();


4 changes: 4 additions & 0 deletions packages/playground/examples/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
"file": "./editor/observe_change_history.html",
"docs": "classes/editor.editor-1.html#addobserver"
},
{
"file": "./editor/batch_changes.html",
"docs": "classes/editor.editor-1.html#batch"
},
{
"file": "./editor/pointer_listener.html",
"docs": "classes/editor.editor-1.html#addeventlistener"
Expand Down

0 comments on commit b697ae4

Please sign in to comment.