import ol_Collection from 'ol/Collection.js'
import ol_interaction_Interaction from 'ol/interaction/Interaction.js'
import ol_layer_Vector from 'ol/layer/Vector.js'
import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js'
import '../source/Vector.js'
/** Undo/redo interaction
* @constructor
* @extends {ol_interaction_Interaction}
* @fires undo
* @fires redo
* @fires change:add
* @fires change:remove
* @fires change:clear
* @param {Object} options
* @param {number=} options.maxLength max undo stack length (0=Infinity), default Infinity
* @param {Array
} options.layers array of layers to undo/redo
*/
var ol_interaction_UndoRedo = class olinteractionUndoRedo extends ol_interaction_Interaction {
constructor(options) {
options = options || {}
super({
handleEvent: function () {
return true
}
})
//array of layers to undo/redo
this._layers = options.layers
this._undoStack = new ol_Collection()
this._redoStack = new ol_Collection()
// Zero level stack
this._undo = []
this._redo = []
this._undoStack.on('add', function (e) {
if (e.element.level === undefined) {
e.element.level = this._level
if (!e.element.level) {
e.element.view = {
center: this.getMap().getView().getCenter(),
zoom: this.getMap().getView().getZoom()
}
this._undo.push(e.element)
}
} else {
if (!e.element.level)
this._undo.push(this._redo.shift())
}
if (!e.element.level) {
this.dispatchEvent({
type: 'stack:add',
action: e.element
})
}
this._reduce()
}.bind(this))
this._undoStack.on('remove', function (e) {
if (!e.element.level) {
if (this._doShift) {
this._undo.shift()
} else {
if (this._undo.length)
this._redo.push(this._undo.pop())
}
if (!this._doClear) {
this.dispatchEvent({
type: 'stack:remove',
action: e.element,
shift: this._doShift
})
}
}
}.bind(this))
// Block counter
this._block = 0
this._level = 0
// Shift an undo action ?
this._doShift = false
// Start recording
this._record = true
// Custom definitions
this._defs = {}
}
/** Add a custom undo/redo
* @param {string} action the action key name
* @param {function} undoFn function called when undoing
* @param {function} redoFn function called when redoing
* @api
*/
define(action, undoFn, redoFn) {
this._defs[action] = { undo: undoFn, redo: redoFn }
}
/** Get first level undo / redo length
* @param {string} [type] get redo stack length, default get undo
* @return {number}
*/
length(type) {
return (type === 'redo') ? this._redo.length : this._undo.length
}
/** Set undo stack max length
* @param {number} length
*/
setMaxLength(length) {
length = parseInt(length)
if (length && length < 0)
length = 0
this.set('maxLength', length)
this._reduce()
}
/** Get undo / redo size (includes all block levels)
* @param {string} [type] get redo stack length, default get undo
* @return {number}
*/
size(type) {
return (type === 'redo') ? this._redoStack.getLength() : this._undoStack.getLength()
}
/** Set undo stack max size
* @param {number} size
*/
setMaxSize(size) {
size = parseInt(size)
if (size && size < 0)
size = 0
this.set('maxSize', size)
this._reduce()
}
/** Reduce stack: shift undo to set size
* @private
*/
_reduce() {
if (this.get('maxLength')) {
while (this.length() > this.get('maxLength')) {
this.shift()
}
}
if (this.get('maxSize')) {
while (this.length() > 1 && this.size() > this.get('maxSize')) {
this.shift()
}
}
}
/** Get first level undo / redo first level stack
* @param {string} [type] get redo stack, default get undo
* @return {Array<*>}
*/
getStack(type) {
return (type === 'redo') ? this._redo : this._undo
}
/** Add a new custom undo/redo
* @param {string} action the action key name
* @param {any} prop an object that will be passed in the undo/redo functions of the action
* @param {string} name action name
* @return {boolean} true if the action is defined
*/
push(action, prop, name) {
if (this._defs[action]) {
this._undoStack.push({
type: action,
name: name,
custom: true,
prop: prop
})
return true
} else {
console.warn('[UndoRedoInteraction]: "' + action + '" is not defined.')
return false
}
}
/** Remove undo action from the beginning of the stack.
* The action is not returned.
*/
shift() {
this._doShift = true
var a = this._undoStack.removeAt(0)
this._doShift = false
// Remove all block
if (a.type === 'blockstart') {
a = this._undoStack.item(0)
while (this._undoStack.getLength() && a.level > 0) {
this._undoStack.removeAt(0)
a = this._undoStack.item(0)
}
}
}
/** Activate or deactivate the interaction, ie. records or not events on the map.
* @param {boolean} active
* @api stable
*/
setActive(active) {
super.setActive(active)
this._record = active
}
/**
* Remove the interaction from its current map, if any, and attach it to a new
* map, if any. Pass `null` to just remove the interaction from the current map.
* @param {ol.Map} map Map.
* @api stable
*/
setMap(map) {
if (this._mapListener) {
this._mapListener.forEach(function (l) { ol_Observable_unByKey(l) })
}
this._mapListener = []
super.setMap(map)
// Watch blocks
if (map) {
this._mapListener.push(map.on('undoblockstart', this.blockStart.bind(this)))
this._mapListener.push(map.on('undoblockend', this.blockEnd.bind(this)))
}
// Watch sources
this._watchSources()
this._watchInteractions()
}
/** Watch for changes in the map sources
* @private
*/
_watchSources() {
var map = this.getMap()
// Clear listeners
if (this._sourceListener) {
this._sourceListener.forEach(function (l) { ol_Observable_unByKey(l) })
}
this._sourceListener = []
var self = this
// Ges vector layers
function getVectorLayers(layers, init) {
if (!init)
init = []
layers.forEach(function (l) {
if (l instanceof ol_layer_Vector) {
if (!self._layers || self._layers.indexOf(l) >= 0) {
init.push(l)
}
} else if (l.getLayers) {
getVectorLayers(l.getLayers(), init)
}
})
return init
}
if (map) {
// Watch the vector sources in the map
var vectors = getVectorLayers(map.getLayers())
vectors.forEach((function (l) {
var s = l.getSource()
this._sourceListener.push(s.on(['addfeature', 'removefeature'], this._onAddRemove.bind(this)))
this._sourceListener.push(s.on('clearstart', function () {
this.blockStart('clear')
}.bind(this)))
this._sourceListener.push(s.on('clearend', this.blockEnd.bind(this)))
}).bind(this))
// Watch new inserted/removed
this._sourceListener.push(map.getLayers().on(['add', 'remove'], this._watchSources.bind(this)))
}
}
/** Watch for interactions
* @private
*/
_watchInteractions() {
var map = this.getMap()
// Clear listeners
if (this._interactionListener) {
this._interactionListener.forEach(function (l) { ol_Observable_unByKey(l) })
}
this._interactionListener = []
if (map) {
// Watch the interactions in the map
map.getInteractions().forEach((function (i) {
this._interactionListener.push(i.on(
['setattributestart', 'modifystart', 'rotatestart', 'translatestart', 'scalestart', 'deletestart', 'deleteend', 'beforesplit', 'aftersplit'],
this._onInteraction.bind(this)
))
}).bind(this))
// Watch new inserted / unwatch removed
this._interactionListener.push(map.getInteractions().on(
['add', 'remove'],
this._watchInteractions.bind(this)
))
}
}
/** A feature is added / removed
*/
_onAddRemove(e) {
if (this._record) {
this._redoStack.clear()
this._redo.length = 0
this._undoStack.push({
type: e.type,
source: e.target,
feature: e.feature
})
}
}
/** Perform an interaction
* @private
*/
_onInteraction(e) {
var fn = this._onInteraction[e.type]
if (fn)
fn.call(this, e)
}
/** Start an undo block
* @param {string} [name] name f the action
* @api
*/
blockStart(name) {
this._redoStack.clear()
this._redo.length = 0
this._undoStack.push({
type: 'blockstart',
name: name
})
this._level++
}
/** End an undo block
* @api
*/
blockEnd() {
this._undoStack.push({ type: 'blockend' })
this._level--
}
/** handle undo/redo
* @private
*/
_handleDo(e, undo) {
// Not active
if (!this.getActive())
return
// Stop recording while undoing
this._record = false
if (e.custom) {
if (this._defs[e.type]) {
if (undo)
this._defs[e.type].undo(e.prop)
else
this._defs[e.type].redo(e.prop)
} else {
console.warn('[UndoRedoInteraction]: "' + e.type + '" is not defined.')
}
} else {
switch (e.type) {
case 'addfeature': {
if (undo)
e.source.removeFeature(e.feature)
else
e.source.addFeature(e.feature)
break
}
case 'removefeature': {
if (undo)
e.source.addFeature(e.feature)
else
e.source.removeFeature(e.feature)
break
}
case 'changegeometry': {
var geom = e.feature.getGeometry()
e.feature.setGeometry(e.oldGeom)
e.oldGeom = geom
break
}
case 'changeattribute': {
var newp = e.newProperties
var oldp = e.oldProperties
for (var p in oldp) {
if (oldp === undefined)
e.feature.unset(p)
else
e.feature.set(p, oldp[p])
}
e.oldProperties = newp
e.newProperties = oldp
break
}
case 'blockstart': {
this._block += undo ? -1 : 1
break
}
case 'blockend': {
this._block += undo ? 1 : -1
break
}
default: {
console.warn('[UndoRedoInteraction]: "' + e.type + '" is not defined.')
}
}
}
// Handle block
if (this._block < 0)
this._block = 0
if (this._block) {
if (undo)
this.undo()
else
this.redo()
}
this._record = true
// Dispatch event
this.dispatchEvent({
type: undo ? 'undo' : 'redo',
action: e
})
}
/** Undo last operation
* @api
*/
undo() {
var e = this._undoStack.item(this._undoStack.getLength() - 1)
if (!e)
return
this._redoStack.push(e)
this._undoStack.pop()
this._handleDo(e, true)
}
/** Redo last operation
* @api
*/
redo() {
var e = this._redoStack.item(this._redoStack.getLength() - 1)
if (!e)
return
this._undoStack.push(e)
this._redoStack.pop()
this._handleDo(e, false)
}
/** Clear undo stack
* @api
*/
clear() {
this._doClear = true
this._undo.length = this._redo.length = 0
this._undoStack.clear()
this._redoStack.clear()
this._doClear = false
this.dispatchEvent({ type: 'stack:clear' })
}
/** Check if undo is avaliable
* @return {number} the number of undo
* @api
*/
hasUndo() {
return this._undoStack.getLength()
}
/** Check if redo is avaliable
* @return {number} the number of redo
* @api
*/
hasRedo() {
return this._redoStack.getLength()
}
}
/** Set attribute
* @private
*/
ol_interaction_UndoRedo.prototype._onInteraction.setattributestart = function(e) {
this.blockStart(e.target.get('name') || 'setattribute');
var newp = Object.assign({}, e.properties);
e.features.forEach(function(f) {
var oldp = {};
for (var p in newp) {
oldp[p] = f.get(p);
}
this._undoStack.push({
type: 'changeattribute',
feature: f,
newProperties: newp,
oldProperties: oldp
});
}.bind(this));
this.blockEnd();
};
ol_interaction_UndoRedo.prototype._onInteraction.rotatestart =
ol_interaction_UndoRedo.prototype._onInteraction.translatestart =
ol_interaction_UndoRedo.prototype._onInteraction.scalestart =
ol_interaction_UndoRedo.prototype._onInteraction.modifystart = function (e) {
this.blockStart(e.type.replace(/start$/,''));
e.features.forEach(function(m) {
this._undoStack.push({
type: 'changegeometry',
feature: m,
oldGeom: m.getGeometry().clone()
});
}.bind(this));
this.blockEnd();
};
/** @private
*/
ol_interaction_UndoRedo.prototype._onInteraction.beforesplit = function() {
// Check modify before split
var l = this._undoStack.getLength();
if (l>2
&& this._undoStack.item(l-1).type === 'blockend'
&& this._undoStack.item(l-2).type === 'changegeometry') {
this._undoStack.pop();
} else {
this.blockStart('split');
}
};
ol_interaction_UndoRedo.prototype._onInteraction.deletestart = function() {
this.blockStart('delete');
}
/** @private
*/
ol_interaction_UndoRedo.prototype._onInteraction.aftersplit =
ol_interaction_UndoRedo.prototype._onInteraction.deleteend =
ol_interaction_UndoRedo.prototype.blockEnd;
export default ol_interaction_UndoRedo