/* * This is part of Geomajas, a GIS framework, http://www.geomajas.org/. * * Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium. * * The program is available in open source according to the GNU Affero * General Public License. All contributions in this program are covered * by the Geomajas Contributors License Agreement. For full licensing * details, see LICENSE.txt in the project root. */ package org.geomajas.plugin.editing.jsapi.client.service; import com.google.gwt.core.client.JsArray; import com.google.gwt.event.shared.HandlerRegistration; import org.geomajas.annotation.Api; import org.geomajas.geometry.Coordinate; import org.geomajas.geometry.Geometry; import org.geomajas.plugin.editing.client.event.GeometryEditChangeStateEvent; import org.geomajas.plugin.editing.client.event.GeometryEditInsertEvent; import org.geomajas.plugin.editing.client.event.GeometryEditMoveEvent; import org.geomajas.plugin.editing.client.event.GeometryEditRemoveEvent; import org.geomajas.plugin.editing.client.event.GeometryEditResumeEvent; import org.geomajas.plugin.editing.client.event.GeometryEditShapeChangedEvent; import org.geomajas.plugin.editing.client.event.GeometryEditStartEvent; import org.geomajas.plugin.editing.client.event.GeometryEditStopEvent; import org.geomajas.plugin.editing.client.event.GeometryEditSuspendEvent; import org.geomajas.plugin.editing.client.event.GeometryEditTentativeMoveEvent; import org.geomajas.plugin.editing.client.operation.GeometryOperationFailedException; import org.geomajas.plugin.editing.client.service.GeometryEditService; import org.geomajas.plugin.editing.client.service.GeometryEditServiceImpl; import org.geomajas.plugin.editing.client.service.GeometryEditState; import org.geomajas.plugin.editing.client.service.GeometryIndex; import org.geomajas.plugin.editing.client.service.GeometryIndexService; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditChangeStateHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditInsertHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditMoveHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditRemoveHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditResumeHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditShapeChangedHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditStartHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditStopHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditSuspendHandler; import org.geomajas.plugin.editing.jsapi.client.event.GeometryEditTentativeMoveHandler; import org.geomajas.plugin.jsapi.client.event.JsHandlerRegistration; import org.timepedia.exporter.client.Export; import org.timepedia.exporter.client.ExportPackage; import org.timepedia.exporter.client.Exportable; import org.timepedia.exporter.client.ExporterBaseActual.JsArrayObject; import org.timepedia.exporter.client.ExporterUtil; import org.timepedia.exporter.client.NoExport; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * <p> * Central service for all operations concerning the geometry editing process. This process should work together with a * set of controllers on the map that execute methods from this service after which events are fired for a renderer to * act upon. Note that this service uses the {@link GeometryIndexService} to identify sub-geometries, vertices and * edges, and that all operations work on a set of such indices. This allows for great flexibility in the operations * that can be performed on geometries. * </p> * <p> * This service also extends the {@link org.geomajas.plugin.editing.client.service.GeometryIndexOperationService} * service, which defines possible operations on geometries during the editing process. Operations can be stand alone or * can be part of an operation sequence. Using an operations sequence wherein multiple operations are executed will be * regarded as a single operation unit for the undo and redo methods. * </p> * <p> * Take for example the moving of a vertex on the map. The user might drag a vertex over a lot of pixels, but every * intermediary change is executed as an operation (otherwise no events would be thrown and the renderer on the map * wouldn't know there was a change). When the user finally releases the vertex, dozens of move operations might already * have been executed. If the user would now have to click an undo button dozens of times to get the vertex back to it's * original position, that would not be very user-friendly.<br/> * On such occasions, an operation sequence would be used so that those dozens move operations are regarded as a single * unit, undone with a single call to undo. * </p> * <p> * Know also that operations onto the geometry really do apply on the same geometry that was passed with the * <code>start</code> method. In other words, this service does change the original geometry. If you want to support * some roll-back functionality within your code, make sure to create a clone of the geometry before starting this edit * service. * </p> * * @author Pieter De Graef * @since 1.0.0 */ @Export("GeometryEditService") @ExportPackage("org.geomajas.plugin.editing.service") @Api(allMethods = true) public class JsGeometryEditService implements Exportable { private GeometryEditService delegate; private JsGeometryIndexStateService stateService; /** * Default constructor. */ public JsGeometryEditService() { delegate = new GeometryEditServiceImpl(); stateService = new JsGeometryIndexStateService(delegate.getIndexStateService()); } /** * Constructor with a {@link GeometryEditService} delegate. * @param delegate delegate */ @NoExport public JsGeometryEditService(GeometryEditService delegate) { this.delegate = delegate; stateService = new JsGeometryIndexStateService(delegate.getIndexStateService()); } /** * Get the delegating {@link GeometryEditService}. * @return delegate delegate */ @NoExport public GeometryEditService getDelegate() { return delegate; } // Registering event handlers: /** * Register a {@link GeometryEditStartHandler} that catches events that signal the editing process has started. * * @param handler * The {@link GeometryEditStartHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditStartHandler(final GeometryEditStartHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditStartHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditStartHandler() { public void onGeometryEditStart(GeometryEditStartEvent event) { handler.onGeometryEditStart(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditStartEvent( event.getGeometry())); } }; return new JsHandlerRegistration(new HandlerRegistration[] { delegate.addGeometryEditStartHandler(h) }); } /** * Register a {@link GeometryEditStopHandler} that catches events that signal the editing process has ended. * * @param handler * The {@link GeometryEditStopHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditStopHandler(final GeometryEditStopHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditStopHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditStopHandler() { public void onGeometryEditStop(GeometryEditStopEvent event) { handler.onGeometryEditStop(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditStopEvent( event.getGeometry())); } }; return new JsHandlerRegistration(new HandlerRegistration[] { delegate.addGeometryEditStopHandler(h) }); } /** * Register a {@link GeometryEditSuspendHandler} that catches events that signal the editing process was suspended. * * @param handler * The {@link GeometryEditSuspendHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditSuspendHandler(final GeometryEditSuspendHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditSuspendHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditSuspendHandler() { public void onGeometryEditSuspend(GeometryEditSuspendEvent event) { handler.onGeometryEditSuspend( new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditSuspendEvent( event.getGeometry())); } }; return new JsHandlerRegistration(new HandlerRegistration[] { delegate.addGeometryEditSuspendHandler(h) }); } /** * Register a {@link GeometryEditResumeHandler} that catches events that signal the editing process was resumed. * * @param handler * The {@link GeometryEditResumeHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditResumeHandler(final GeometryEditResumeHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditResumeHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditResumeHandler() { public void onGeometryEditResume(GeometryEditResumeEvent event) { handler.onGeometryEditResume(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditResumeEvent( event.getGeometry())); } }; return new JsHandlerRegistration(new HandlerRegistration[] { delegate.addGeometryEditResumeHandler(h) }); } /** * Register a {@link GeometryEditChangeStateHandler} to listen to events the mark changes in the general editing * state. This general state can say we're busy idling, dragging vertices/edges/geometries or inserting. The reason * why this exists is for the general editing controllers to know how to behave. * * @param handler * The {@link GeometryEditChangeStateHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditChangeStateHandler(final GeometryEditChangeStateHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditChangeStateHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditChangeStateHandler() { public void onChangeEditingState(GeometryEditChangeStateEvent event) { String state = "idle"; switch (event.getEditingState()) { case INSERTING: state = "inserting"; break; case DRAGGING: state = "dragging"; } org.geomajas.plugin.editing.jsapi.client.event.GeometryEditChangeStateEvent e; e = new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditChangeStateEvent(state); handler.onChangeEditingState(e); } }; HandlerRegistration registration = delegate.addGeometryEditChangeStateHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } /** * Register a {@link GeometryEditTentativeMoveHandler} to listen to mouse move events that point to tentative * moving. These move events don't have to commit to anything. They may result in operations being executed, they * may not. This is meant mainly for the renderers to respond quickly to user interaction. (user friendliness and * all that jazz). * * @param handler * The {@link GeometryEditTentativeMoveHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditTentativeMoveHandler(final GeometryEditTentativeMoveHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditTentativeMoveHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditTentativeMoveHandler() { public void onTentativeMove(GeometryEditTentativeMoveEvent event) { handler.onInsertMove(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditTentativeMoveEvent( delegate.getTentativeMoveOrigin(), delegate.getTentativeMoveLocation())); } }; HandlerRegistration registration = delegate.addGeometryEditTentativeMoveHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } /** * Register a {@link GeometryEditInsertHandler} to listen to insert events of sub-geometries, vertices and edges. * * @param handler * The {@link GeometryEditInsertHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditInsertHandler(final GeometryEditInsertHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditInsertHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditInsertHandler() { public void onGeometryEditInsert(GeometryEditInsertEvent event) { org.geomajas.plugin.editing.jsapi.client.event.GeometryEditInsertEvent e; List<GeometryIndex> indices = event.getIndices(); e = new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditInsertEvent(event.getGeometry(), indices.toArray(new GeometryIndex[indices.size()])); handler.onGeometryEditInsert(e); } }; HandlerRegistration registration = delegate.addGeometryEditInsertHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } /** * Register a {@link GeometryEditMoveHandler} to listen to move(translate) events of sub-geometries, vertices and * edges. * * @param handler * The {@link GeometryEditMoveHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditMoveHandler(final GeometryEditMoveHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditMoveHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditMoveHandler() { public void onGeometryEditMove(GeometryEditMoveEvent event) { List<GeometryIndex> indices = event.getIndices(); handler.onGeometryEditMove(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditMoveEvent( event.getGeometry(), indices.toArray(new GeometryIndex[indices.size()]))); } }; HandlerRegistration registration = delegate.addGeometryEditMoveHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } /** * Register a {@link GeometryEditRemoveHandler} to listen to delete events of sub-geometries, vertices and edges. * * @param handler * The {@link GeometryEditRemoveHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditRemoveHandler(final GeometryEditRemoveHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditRemoveHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditRemoveHandler() { public void onGeometryEditRemove(GeometryEditRemoveEvent event) { List<GeometryIndex> indices = event.getIndices(); handler.onGeometryEditRemove(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditRemoveEvent( event.getGeometry(), indices.toArray(new GeometryIndex[indices.size()]))); } }; HandlerRegistration registration = delegate.addGeometryEditRemoveHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } /** * Register a {@link GeometryEditShapeChangedHandler} to listen to operation events (moving, inserting, deleting, * ...) of sub-geometries, vertices and edges but also listens to undo/redo events. Anything the changes the * geometry shape basically. * * @param handler * The {@link GeometryEditShapeChangedHandler} to add as listener. * @return The registration of the handler. */ public JsHandlerRegistration addGeometryEditShapeChangedHandler(final GeometryEditShapeChangedHandler handler) { org.geomajas.plugin.editing.client.event.GeometryEditShapeChangedHandler h; h = new org.geomajas.plugin.editing.client.event.GeometryEditShapeChangedHandler() { public void onGeometryShapeChanged(GeometryEditShapeChangedEvent event) { handler.onShapeChanged(new org.geomajas.plugin.editing.jsapi.client.event.GeometryEditShapeChangedEvent( getGeometry())); } }; HandlerRegistration registration = delegate.addGeometryEditShapeChangedHandler(h); return new JsHandlerRegistration(new HandlerRegistration[] { registration }); } // ------------------------------------------------------------------------ // Methods concerning "UNDO/REDO": // ------------------------------------------------------------------------ /** * Undo the last operation (or operation sequence) that was executed in the editing process, thereby restoring the * previous state. * * @throws GeometryOperationFailedException * In case the inverse operation could not be executed. */ public void undo() throws GeometryOperationFailedException { delegate.undo(); } /** * Can an undo actually be executed? You can't execute more calls to undo than there are operations in the undo * queue. * * @return True or false. */ public boolean canUndo() { return delegate.canUndo(); } /** * Redo an operation again, after it was undone with the undo method. * * @throws GeometryOperationFailedException * In case the operation failed. */ public void redo() throws GeometryOperationFailedException { delegate.redo(); } /** * Can a redo operation be executed? This can only be done after some undo. * * @return True or false. */ public boolean canRedo() { return delegate.canRedo(); } // ------------------------------------------------------------------------ // Operation sequence manipulation: // ------------------------------------------------------------------------ /** * Starts an operation sequence. All operations called after this method will be regarded as a single unit, which * can be very useful for undo/redo operations. This state is active until <code>stopOperationSequence</code> is * called. * * @throws GeometryOperationFailedException * Thrown in case an operation sequence has already been started. Call * <code>stopOperationSequence</code> first. */ public void startOperationSequence() throws GeometryOperationFailedException { delegate.startOperationSequence(); } /** * Stops the current operation sequence (if there is one active). From this point on, all operations * (move/insert/delete/...) will again be regarded as separate operations. */ public void stopOperationSequence() { delegate.stopOperationSequence(); } /** * Is there currently an operation sequence being build or not? * * @return Checks if an operation sequence has been started. */ public boolean isOperationSequenceActive() { return delegate.isOperationSequenceActive(); } // ------------------------------------------------------------------------ // Supported operations: // ------------------------------------------------------------------------ /** * Move a set of indices to new locations. These indices can point to vertices, edges or sub-geometries. For each * index, a list of new coordinates is provided. * * @param indices * The list of indices to move. * @param coordinates * The coordinates to move the indices to. Must be a nested array of coordinates. In other words, for * each index an array of coordinates must be supplied. * @throws GeometryOperationFailedException * In case one of the indices could not be found. No changes will have been performed. */ public void move(GeometryIndex[] indices, JsArray<JsArrayObject> coordinates) { List<List<Coordinate>> coords = new ArrayList<List<Coordinate>>(coordinates.length()); for (int i = 0; i < coordinates.length(); i++) { JsArrayObject jsObj = coordinates.get(i); coords.add(Arrays.asList(ExporterUtil.toArrObject(jsObj, new Coordinate[jsObj.length()]))); } try { delegate.move(Arrays.asList(indices), coords); } catch (GeometryOperationFailedException e) { throw new RuntimeException(e.getMessage()); } } /** * Insert lists of coordinates at the provided indices. These indices can point to vertices, edges or * sub-geometries. For each index, a list of coordinates is provided to be inserted after that index. * * @param indices * The list of indices after which to insert coordinates. * @param coordinates * The coordinates to be inserted after each index. Must be a nested array of coordinates. In other * words, for each index an array of coordinates must be supplied. * @throws GeometryOperationFailedException * In case one of the indices could not be found. No changes will have been performed. */ public void insert(GeometryIndex[] indices, JsArray<JsArrayObject> coordinates) { List<List<Coordinate>> coords = new ArrayList<List<Coordinate>>(coordinates.length()); for (int i = 0; i < coordinates.length(); i++) { JsArrayObject jsObj = coordinates.get(i); coords.add(Arrays.asList(ExporterUtil.toArrObject(jsObj, new Coordinate[jsObj.length()]))); } try { delegate.insert(Arrays.asList(indices), coords); } catch (GeometryOperationFailedException e) { throw new RuntimeException(e.getMessage()); } } /** * Delete vertices, edges or sub-geometries at the given indices. * * @param indices * The list of indices that point to the vertices/edges/sub-geometries that should be deleted. * @throws GeometryOperationFailedException * In case one of the indices could not be found. No changes will have been performed. */ public void remove(GeometryIndex[] indices) { try { delegate.remove(Arrays.asList(indices)); } catch (GeometryOperationFailedException e) { throw new RuntimeException(e.getMessage()); } } /** * Add an empty child at the lowest sub-geometry level. * * @return The index that points to the empty child within the geometry. * @throws GeometryOperationFailedException */ public GeometryIndex addEmptyChild() throws GeometryOperationFailedException { return delegate.addEmptyChild(); } // ------------------------------------------------------------------------ // Methods concerning Workflow: // ------------------------------------------------------------------------ /** * Start the geometry editing process. * * @param geometry * The geometry that needs to be edited. */ public void start(Geometry geometry) { delegate.start(geometry); } /** * Stop the geometry editing process. * * @return Returns the current state of the geometry. */ public Geometry stop() { return delegate.stop(); } /** * Suspend the geometry editing process. * */ public void suspend() { delegate.suspend(); } /** * Resume the geometry editing process. * */ public void resume() { delegate.resume(); } /** * Is the editing process started ? * @return true if started */ public boolean isStarted() { return delegate.isStarted(); } /** * Is the editing process suspended ? * @return true if suspended */ public boolean isSuspended() { return delegate.isSuspended(); } // ------------------------------------------------------------------------ // Methods concerning editing state: // ------------------------------------------------------------------------ /** * Change the general editing state of this service (idle, dragging, inserting, ...). * * @param state * The new editing state. */ public void setEditingState(String state) { if ("idle".equalsIgnoreCase(state)) { delegate.setEditingState(GeometryEditState.IDLE); } else if ("dragging".equalsIgnoreCase(state)) { delegate.setEditingState(GeometryEditState.DRAGGING); } else if ("inserting".equalsIgnoreCase(state)) { delegate.setEditingState(GeometryEditState.INSERTING); } } /** * Get the current editing state of this service (idle, dragging, inserting, ...). * * @return The current editing state. */ public String getEditingState() { switch (delegate.getEditingState()) { case IDLE: return "idle"; case DRAGGING: return "dragging"; case INSERTING: return "inserting"; default: return "unknown"; } } // ------------------------------------------------------------------------ // Methods regarding the tentative move events: // ------------------------------------------------------------------------ /** * Set the origin for a line to a possible next vertex position. This is regarded as tentative, because no * commitment is taken yet. * * @param origin * The origin for the tentative move event. */ public void setTentativeMoveOrigin(Coordinate origin) { delegate.setTentativeMoveOrigin(origin); } /** * Set the end-point for a possible next vertex position. This is regarded as tentative, because no commitment is * taken yet. An event is thrown though, containing both the tentative origin and end-point. * * @param location * The end-point for the tentative move event. */ public void setTentativeMoveLocation(Coordinate location) { delegate.setTentativeMoveLocation(location); } /** * Get the origin for the tentative move event. * * @return The origin for the tentative move event. */ public Coordinate getTentativeMoveOrigin() { return delegate.getTentativeMoveOrigin(); } /** * Get the end-point for the tentative move event. * * @return The end-point for the tentative move event. */ public Coordinate getTentativeMoveLocation() { return delegate.getTentativeMoveLocation(); } // ------------------------------------------------------------------------ // Getters: // ------------------------------------------------------------------------ /** * Get the index where the next insert operation should take place. * * @return The index where the next insert operation should take place. */ public GeometryIndex getInsertIndex() { return delegate.getInsertIndex(); } /** * Set the index where the insert operation should take place. This is mainly used as a helper method. The insert * operation can actually insert wherever it wants, but this is often used to keep track of the latest inserts and * extrapolate where to insert next. * * @param insertIndex * The vertex/edge/sub-geometry where to insert on the next insert statement. */ public void setInsertIndex(GeometryIndex insertIndex) { delegate.setInsertIndex(insertIndex); } /** * Get the current geometry. This geometry may change shape during the editing process. * * @return The current geometry that is being edited. */ public Geometry getGeometry() { return delegate.getGeometry(); } /** * Return the indexing service the is being used to identify vertices/edges/sub-geometries. * * @return The geometry indexing service. */ public GeometryIndexService getIndexService() { return delegate.getIndexService(); } /** * Return the service that keeps track of the changes in state of the individual vertices/edges/sub-geometries * during editing. * * @return The geometry-index-state-change service. */ public JsGeometryIndexStateService getIndexStateService() { return stateService; } }