feat: Add a cursor annotation mode.
See the annotation example for the utility of this mode.
MODIFY
CHANGELOG.md
CHANGELOG.md
# GeoJS Change Log
+## Version 1.10.0
+
+### Features
+
+- Add a cursor annotation mode ([#1224](../../pull/1224))
+
+## Version 1.9.4
+
+### Improvements
+
+- Update on meta key changes ([#1221](../../pull/1221))
+
+### Bug Fixes
+
+- Fix determining annotation correspondence ([#1223](../../pull/1223))
+
+## Version 1.9.3
+
+### Improvements
+
+- Speed up track feature ([#1220](../../pull/1220))
+
## Version 1.9.2
### Bug Fixes

MODIFY
examples/annotations/index.pug
examples/annotations/index.pug
@@ -5,7 +5,7 @@ block append mainContent
.form-group.annotationtype(title='Select the type of annotation to add.')
.shortlabel Add
button#rectangle.lastused(next='square') Rectangle
- button#square.lastused(next='ellipse') Square
+ button#square(next='ellipse') Square
button#ellipse(next='circle') Ellipse
button#circle(next='polygon') Circle
button#polygon(next='point') Polygon
@@ -21,12 +21,21 @@ block append mainContent
.annotation.circle Left click-and-drag or left click opposite corners to draw a circle.
.annotation.point Left click to create a point.
.annotation.line Left-click points in the line. Double click, right click, or click the starting point to end the line.
- .form-group(title='If enabled, left-click to add another annotation, and right-click to switch annotation type. Otherwise, you must click a button above.')
+ .form-group(title='If "Add New", left-click to add another annotation, and right-click to switch annotation type. Otherwise, you must click a button above.')
label(for='clickmode') Click mode
select#clickmode(param-name='clickmode', placeholder='edit')
option(value='edit') Select and Edit
option(value='add') Add New
option(value='none') None
+ option(value='brush') Brush Mode
+
+ .form-group.compact(title='In Brush Mode, specify the brush shape and size in meters.')
+ label(for='brushshape') Brush Shape
+ select#brushshape(param-name='brushshape', placeholder='square')
+ option(value='square') Square
+ option(value='circle') Circle
+ label(for='brushsize') Size (m)
+ input#brushsize(param-name='brushsize', type='range', min=10, max=100000, value=5000)
.form-group(title='If enabled, immediately after adding one annotation, you can add another without either left-clicking or selecting a button.')
label(for='keepadding') Keep adding annotations
input#keepadding(param-name='keepadding', type='checkbox', placeholder='false')

MODIFY
examples/annotations/main.css
examples/annotations/main.css
color: #333;
cursor: pointer;
}
+#controls .form-group.compact label {
+ min-width: 0px;
+ padding-right: 5px;
+}
+#controls .form-group.compact select {
+ padding-right: 10px;
+}
+#controls .form-group.compact input[type="range"] {
+ width: 120px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
.entry span, .entry a {
display: inline-block;
}

MODIFY
examples/annotations/main.js
examples/annotations/main.js
var annotationDebug = {};
-// Run after the DOM loads
-$(function () {
- 'use strict';
-
var layer, fromButtonSelect, fromGeojsonUpdate;
// get the query parameters and set controls appropriately
@@ -13,10 +9,14 @@ $(function () {
$('#clickmode').val(query.clickmode || 'edit');
$('#keepadding').prop('checked', query.keepadding === 'true');
$('#showLabels').prop('checked', query.labels !== 'false');
- if (query.lastannotation) {
+if (query.lastannotation && query.clickmode !== 'brush') {
$('.annotationtype button').removeClass('lastused');
$('.annotationtype button#' + query.lastannotation).addClass('lastused');
}
+$('#brushshape').val(query.brushshape || 'square');
+if (query.brushsize) {
+ $('#brushsize').val(query.brushsize);
+}
// You can set the initial annotations via a query parameter. If the query
// parameter 'save=true' is specified, the query will be updated with the
// geojson. This can become too long for some browsers.
@@ -65,15 +65,17 @@ $(function () {
layer.geoOn(geo.event.annotation.remove, handleAnnotationChange);
layer.geoOn(geo.event.annotation.state, handleAnnotationChange);
+let brushLayer;
+
map.draw();
// pick which button is initially highlighted based on query parameters.
if (query.lastused || query.active) {
- if (query.active) {
+ if (query.active && query.clickmode !== 'brush') {
layer.mode(query.active);
} else {
$('.annotationtype button').removeClass('lastused active');
- $('.annotationtype button#' + query.lastused).addClass('lastused');
+ $('.annotationtype button#' + (query.lastused || query.active)).addClass('lastused');
}
}
@@ -82,6 +84,10 @@ $(function () {
layer.geojson(initialGeoJSON, true);
}
+if (query.clickmode === 'brush') {
+ setBrushMode();
+}
+
// expose some internal parameters so you can examine them from the console
annotationDebug.map = map;
annotationDebug.layer = layer;
@@ -107,6 +113,109 @@ $(function () {
}
}
+/**
+ * Handle a click or drag with a brush.
+ *
+ * @param {object} evt The event with the activity.
+ * */
+let lastState;
+function brushAction(evt) {
+ let source;
+ if (evt.event === geo.event.annotation.cursor_action) {
+ if (evt.operation && evt.operation !== 'union' && evt.operation !== 'difference') {
+ return;
+ }
+ // if this is the same action as the previous one, "blur" the brush shapes
+ // along the direction of travel
+ if (lastState && lastState.stateId && lastState.stateId === evt.evt.state.stateId) {
+ const shape = $('#brushshape').val();
+ const size = parseInt($('#brushsize').val());
+ source = brushLayer.toPolygonList();
+ const bbox1 = brushLayer.annotations()[0]._coordinates();
+ const bbox2 = lastState.bbox;
+ if (bbox1[0].x !== bbox2[0].x || bbox1[0].y !== bbox2[0].y) {
+ if (shape === 'square') {
+ const order = (bbox1[0].x - bbox2[0].x) * (bbox1[0].y - bbox2[0].y) < 0 ? 0 : 1;
+ source.push([[
+ [bbox1[order].x, bbox1[order].y],
+ [bbox1[order + 2].x, bbox1[order + 2].y],
+ [bbox2[order + 2].x, bbox2[order + 2].y],
+ [bbox2[order].x, bbox2[order].y]
+ ]]);
+ } else {
+ const c1x = (bbox1[0].x + bbox1[2].x) * 0.5;
+ const c1y = (bbox1[0].y + bbox1[2].y) * 0.5;
+ const c2x = (bbox2[0].x + bbox2[2].x) * 0.5;
+ const c2y = (bbox2[0].y + bbox2[2].y) * 0.5;
+ const ang = Math.atan2(c2y - c1y, c2x - c1x) + Math.PI / 2;
+ source.push([[
+ [c1x + size / 2 * Math.cos(ang), c1y + size / 2 * Math.sin(ang)],
+ [c1x - size / 2 * Math.cos(ang), c1y - size / 2 * Math.sin(ang)],
+ [c2x - size / 2 * Math.cos(ang), c2y - size / 2 * Math.sin(ang)],
+ [c2x + size / 2 * Math.cos(ang), c2y + size / 2 * Math.sin(ang)]
+ ]]);
+ }
+ }
+ }
+ lastState = evt.evt.state;
+ lastState.bbox = brushLayer.annotations()[0]._coordinates();
+ } else {
+ lastState = null;
+ }
+ geo.util.polyops[evt.operation || 'union'](layer, source || brushLayer, {correspond: {}, keepAnnotations: 'exact', style: layer});
+}
+
+/**
+ * If the brush mode ends but we are supposed to be in brush mode, reset it.
+ */
+var inUpdateBrushMode;
+function updateBrushMode() {
+ if (query.clickmode !== 'brush') {
+ return;
+ }
+ if (!inUpdateBrushMode) {
+ inUpdateBrushMode = true;
+ window.setTimeout(() => {
+ setBrushMode();
+ inUpdateBrushMode = false;
+ }, 1);
+ }
+}
+
+/**
+ * If we are switching to brush mode, create an annotation that will be used
+ * and hook to annotation cursor events. If switching away, remove such an
+ * annotation.
+ */
+function setBrushMode(mode) {
+ if (brushLayer) {
+ brushLayer.mode(null);
+ brushLayer.removeAllAnnotations();
+ }
+ if (query.clickmode !== 'brush') {
+ return;
+ }
+ layer.mode(null);
+ if (!brushLayer) {
+ brushLayer = map.createLayer('annotation', {
+ renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined,
+ showLabels: false
+ });
+ brushLayer.geoOn(geo.event.annotation.cursor_click, brushAction);
+ brushLayer.geoOn(geo.event.annotation.cursor_action, brushAction);
+ brushLayer.geoOn(geo.event.annotation.mode, updateBrushMode);
+ brushLayer.geoOn(geo.event.annotation.state, updateBrushMode);
+ }
+ annotationDebug.brushLayer = brushLayer;
+ const shape = $('#brushshape').val();
+ const size = parseInt($('#brushsize').val());
+ const annot = geo.registries.annotations[shape].func({layer: layer});
+ brushLayer.addAnnotation(annot);
+ annot._coordinates([{x: 0, y: 0}, {x: size, y: 0}, {x: size, y: size}, {y: size, x: 0}]);
+ brushLayer.mode('cursor', annot);
+ map.draw();
+}
+
/**
* Handle changes to our controls.
*
@@ -133,6 +242,11 @@ $(function () {
case 'clickmode':
layer.options('clickToEdit', value === 'edit');
layer.draw();
+ if (value === 'brush') {
+ $('.annotationtype button').removeClass('lastused active');
+ query.lastused = query.active ? query.active : query.lastused;
+ query.active = undefined;
+ }
break;
}
query[param] = value;
@@ -143,6 +257,9 @@ $(function () {
// update our query parameters, os when you reload the page it is in the
// same state
utils.setQuery(query);
+ if (['clickmode', 'brushshape', 'brushsize'].indexOf(param) >= 0) {
+ setBrushMode();
+ }
}
/**
@@ -182,6 +299,12 @@ $(function () {
*/
function select_button(ctl) {
ctl = $(ctl);
+ if (query.clickmode === 'brush') {
+ query.clickmode = 'edit';
+ $('#clickmode').val(query.clickmode);
+ utils.setQuery(query);
+ setBrushMode();
+ }
var wasactive = ctl.hasClass('active'),
id = ctl.attr('id');
fromButtonSelect = true;
@@ -209,7 +332,7 @@ $(function () {
utils.setQuery(query);
// if we are in keep-adding mode, and the mode changed to null, and that
// wasn't caused by clicking the button, reenable the annotation mode.
- if (!mode && !fromButtonSelect && query.keepadding === 'true') {
+ if (!mode && !fromButtonSelect && query.keepadding === 'true' && query.clickmode !== 'brush') {
layer.mode($('.annotationtype button.lastused').attr('id'));
}
}
@@ -451,4 +574,3 @@ $(function () {
// refresh the annotation list
handleAnnotationChange();
}
-});

MODIFY
examples/annotations/thumb.jpg
examples/annotations/thumb.jpg
Binary files a/examples/annotations/thumb.jpg and b/examples/annotations/thumb.jpg differ

MODIFY
src/action.js
src/action.js
@@ -17,7 +17,8 @@ var geo_action = {
zoomselect: 'geo_action_zoomselect',
// annotation actions -- some are also added by the registry
- annotation_edit_handle: 'geo_annotation_edit_handle'
+ annotation_edit_handle: 'geo_annotation_edit_handle',
+ annotation_cursor: 'geo_annotation_cursor'
};
module.exports = geo_action;

MODIFY
src/annotation/annotation.js
src/annotation/annotation.js
@@ -15,7 +15,8 @@ var annotationState = {
create: 'create',
done: 'done',
highlight: 'highlight',
- edit: 'edit'
+ edit: 'edit',
+ cursor: 'cursor'
};
var annotationActionOwner = 'annotationAction';
@@ -153,10 +154,13 @@ var annotation = function (type, args) {
/**
* Assign a new id to this annotation.
+ *
+ * @returns {this}
*/
this.newId = function () {
annotationId += 1;
m_id = annotationId;
+ return m_this;
};
/**
@@ -308,6 +312,27 @@ var annotation = function (type, args) {
return m_this;
};
+ this._cursorHandleMousemove = function (evt) {
+ m_this.layer()._handleMouseMoveModifiers(evt);
+ const center = m_this._cursorCenter;
+ const delta = {
+ x: evt.mapgcs.x - center.x,
+ y: evt.mapgcs.y - center.y
+ };
+ if (delta.x || delta.y) {
+ const curPts = m_this._coordinates();
+ var pts = m_this._coordinatesMapFunc(curPts, function (elem) {
+ return {x: elem.x + delta.x, y: elem.y + delta.y};
+ });
+ m_this._coordinates(pts);
+ m_this._cursorCenter = evt.mapgcs;
+ m_this.modified();
+ m_this.draw();
+ return true;
+ }
+ return false;
+ };
+
/**
* Get or set the state of this annotation.
*
@@ -328,6 +353,17 @@ var annotation = function (type, args) {
annotation: m_this
});
}
+ if (m_this.layer()) {
+ m_this.layer().geoOff(geo_event.mousemove, m_this._cursorHandleMousemove);
+ }
+ switch (m_state) {
+ case annotationState.cursor:
+ m_this._cursorCenter = util.centerFromPerimeter(m_this._coordinates());
+ if (m_this.layer()) {
+ m_this.layer().geoOn(geo_event.mousemove, m_this._cursorHandleMousemove);
+ }
+ break;
+ }
}
return m_this;
};
@@ -356,6 +392,18 @@ var annotation = function (type, args) {
owner: annotationActionOwner,
input: 'pan'
}];
+ case annotationState.cursor:
+ return [{
+ action: geo_action.annotation_cursor,
+ name: 'annotation cursor',
+ owner: annotationActionOwner,
+ input: 'pan'
+ }, {
+ action: geo_action.annotation_cursor,
+ name: 'annotation cursor',
+ owner: annotationActionOwner,
+ input: 'left'
+ }];
default:
return [];
}
@@ -479,7 +527,7 @@ var annotation = function (type, args) {
/* For style objects, re-extend them without recursion. This allows
* setting colors without an opacity field, for instance. */
['style', 'createStyle', 'editStyle', 'editHandleStyle', 'labelStyle',
- 'highlightStyle'
+ 'highlightStyle', 'cursorStyle'
].forEach(function (key) {
if (arg1[key] !== undefined) {
$.extend(m_options[key], arg1[key]);
@@ -528,8 +576,8 @@ var annotation = function (type, args) {
* current style with the values in the specified object.
* @param {*} [arg2] If `arg1` is a string, the new value for that style.
* @param {string} [styleType='style'] The name of the style type, such as
- * `createStyle`, `editStyle`, `editHandleStyle`, `labelStyle`, or
- * `highlightStyle`.
+ * `createStyle`, `editStyle`, `editHandleStyle`, `labelStyle`,
+ * `highlightStyle`, or `cursorStyle`.
* @returns {object|this} Either the entire style object, the value of a
* specific style, or the current class instance.
*/
@@ -578,12 +626,18 @@ var annotation = function (type, args) {
* @instance
*/
/**
- * Calls {@link geo.annotation#style} with `styleType='highlightHandleStyle'`.
- * @function highlightHandleStyle
+ * Calls {@link geo.annotation#style} with `styleType='highlightStyle'`.
+ * @function highlightStyle
+ * @memberof geo.annotation
+ * @instance
+ */
+ /**
+ * Calls {@link geo.annotation#style} with `styleType='cursorStyle'`.
+ * @function cursorStyle
* @memberof geo.annotation
* @instance
*/
- ['createStyle', 'editStyle', 'editHandleStyle', 'labelStyle', 'highlightStyle'
+ ['createStyle', 'editStyle', 'editHandleStyle', 'labelStyle', 'highlightStyle', 'cursorStyle'
].forEach(function (styleType) {
m_this[styleType] = function (arg1, arg2) {
return m_this.style(arg1, arg2, styleType);
@@ -608,6 +662,10 @@ var annotation = function (type, args) {
return $.extend({}, m_options.style, m_options.editStyle,
m_options[state + 'Style']);
}
+ if (state === annotationState.cursor) {
+ return $.extend({}, m_options.style, m_options.editStyle,
+ m_options.createStyle, m_options[state + 'Style']);
+ }
return m_options[state + 'Style'] || m_options.style || {};
};

MODIFY
src/annotation/polygonAnnotation.js
src/annotation/polygonAnnotation.js
@@ -3,6 +3,7 @@ const inherit = require('../inherit');
const registerAnnotation = require('../registry').registerAnnotation;
const lineFeature = require('../lineFeature');
const polygonFeature = require('../polygonFeature');
+const util = require('../util');
const annotation = require('./annotation').annotation;
const annotationState = require('./annotation').state;
@@ -61,6 +62,9 @@ var polygonAnnotation = function (args) {
}
return m_this.options('vertices')[i];
}
+ },
+ cursorStyle: {
+ position: util.identityFunction
}
}, args);
args.vertices = args.vertices || args.coordinates || [];
@@ -332,6 +336,13 @@ polygonAnnotation.defaults = $.extend({}, annotation.defaults, {
stroke: false,
strokeColor: {r: 0, g: 0, b: 1}
},
+ cursorStyle: {
+ closed: true,
+ fillColor: {r: 0.3, g: 0.3, b: 0.3},
+ fillOpacity: 0.25,
+ stroke: true,
+ strokeColor: {r: 0, g: 0, b: 1}
+ },
allowBooleanOperations: true
});

MODIFY
src/annotationLayer.js
src/annotationLayer.js
@@ -184,6 +184,15 @@ var annotationLayer = function (arg) {
}
}
break;
+ case m_this.modes.cursor:
+ m_this.currentAnnotation._cursorHandleMousemove(evt.mouse);
+ update = m_this.currentAnnotation.processAction(evt);
+ m_this.geoTrigger(geo_event.annotation.cursor_action, {
+ annotation: m_this.currentAnnotation,
+ operation: m_this.currentBooleanOperation(),
+ evt: evt
+ });
+ break;
default:
update = m_this.currentAnnotation.processAction(evt);
break;
@@ -261,7 +270,7 @@ var annotationLayer = function (arg) {
* @param {geo.event} evt The mouse move or click event.
*/
this._handleMouseMoveModifiers = function (evt) {
- if (m_this.mode() !== m_this.modes.edit && m_this.currentAnnotation.options('allowBooleanOperations') && m_this.currentAnnotation._coordinates().length < 2) {
+ if (m_this.mode() !== m_this.modes.edit && m_this.currentAnnotation.options('allowBooleanOperations') && (m_this.currentAnnotation._coordinates().length < 2 || m_this.mode() === m_this.modes.cursor)) {
if (evt.modifiers) {
const mod = (evt.modifiers.shift ? 's' : '') + (evt.modifiers.ctrl ? 'c' : '') + (evt.modifiers.meta || evt.modifiers.alt ? 'a' : '');
if (m_this._currentBooleanClass === m_this._booleanClasses[mod]) {
@@ -392,6 +401,13 @@ var annotationLayer = function (arg) {
update = m_this.currentAnnotation.mouseClick(evt);
m_this._updateFromEvent(update);
retrigger = !m_this.mode();
+ if (m_this.mode() === m_this.modes.cursor) {
+ m_this.geoTrigger(geo_event.annotation.cursor_click, {
+ annotation: m_this.currentAnnotation,
+ operation: m_this.currentBooleanOperation(),
+ evt: evt
+ });
+ }
} else if (!m_this.mode() && !m_this.currentAnnotation && m_this.options('clickToEdit')) {
var highlighted = m_this.annotations().filter(function (ann) {
return ann.state() === geo_annotation.state.highlight;
@@ -591,7 +607,8 @@ var annotationLayer = function (arg) {
/* A list of special modes */
this.modes = {
- edit: 'edit'
+ edit: 'edit',
+ cursor: 'cursor'
};
/* Keys are short-hand for preferred event modifiers. Values are classes to
@@ -608,11 +625,13 @@ var annotationLayer = function (arg) {
*
* @param {string|null} [arg] `undefined` to get the current mode, `null` to
* stop creating/editing, `this.modes.edit` (`'edit'`) plus an annotation
- * to switch to edit mode, or the name of the type of annotation to
- * create. Available annotations can listed via
+ * to switch to edit mode, `this.modes.cursor` plus an annotation to
+ * switch to using the annotation as a cursor, or the name of the type of
+ * annotation to create. Available annotations can listed via
* {@link geo.listAnnotations}.
- * @param {geo.annotation} [editAnnotation] If `arg === this.modes.edit`,
- * this is the annotation that should be edited.
+ * @param {geo.annotation} [editAnnotation] If `arg === this.modes.edit` or
+ * `arg === this.modes.cursor`, this is the annotation that should be
+ * edited or used.
* @param {object} [options] Additional options to pass when creating an
* annotation.
* @returns {string|null|this} The current mode or the layer.
@@ -622,11 +641,14 @@ var annotationLayer = function (arg) {
if (arg === undefined) {
return m_mode;
}
- if (arg !== m_mode || (arg === m_this.modes.edit && editAnnotation !== m_this.editAnnotation) || (arg !== m_this.modes.edit && arg && !m_this.currentAnnotation)) {
+ if (arg !== m_mode ||
+ ((arg === m_this.modes.edit || arg === m_this.modes.cursor) && editAnnotation !== m_this.editAnnotation) ||
+ (arg !== m_this.modes.edit && arg !== m_this.modes.cursor && arg && !m_this.currentAnnotation)
+ ) {
var createAnnotation, actions,
mapNode = m_this.map().node(), oldMode = m_mode;
m_mode = arg;
- mapNode.toggleClass('annotation-input', !!(m_mode && m_mode !== m_this.modes.edit));
+ mapNode.toggleClass('annotation-input', !!(m_mode && m_mode !== m_this.modes.edit && m_mode !== m_this.modes.cursor));
if (!m_mode || m_mode === m_this.modes.edit) {
Object.values(m_this._booleanClasses).forEach((c) => mapNode.toggleClass(c, false));
m_this._currentBooleanClass = undefined;
@@ -649,6 +671,12 @@ var annotationLayer = function (arg) {
m_this.modified();
m_this.draw();
break;
+ case geo_annotation.state.cursor:
+ m_this.currentAnnotation.state(geo_annotation.state.done);
+ m_this.modified();
+ m_this.draw();
+ m_this.map().node().toggleClass('annotation-cursor', false);
+ break;
}
m_this.currentAnnotation = null;
}
@@ -656,6 +684,12 @@ var annotationLayer = function (arg) {
m_this.currentAnnotation = editAnnotation;
m_this.currentAnnotation.state(geo_annotation.state.edit);
m_this.modified();
+ } else if (m_mode === m_this.modes.cursor) {
+ m_this.currentAnnotation = editAnnotation;
+ m_this.currentAnnotation.state(geo_annotation.state.cursor);
+ m_this.modified();
+ m_this.map().node().toggleClass('annotation-cursor', true);
+ actions = m_this.currentAnnotation.actions(geo_annotation.state.cursor);
} else if (registry.registries.annotations[m_mode]) {
createAnnotation = registry.registries.annotations[m_mode].func;
}
@@ -669,13 +703,15 @@ var annotationLayer = function (arg) {
m_this.currentAnnotation = createAnnotation(options);
m_this.addAnnotation(m_this.currentAnnotation, null);
actions = m_this.currentAnnotation.actions(geo_annotation.state.create);
+ }
+ if (actions) {
$.each(actions, function (idx, action) {
m_this.map().interactor().addAction(action);
});
}
m_this.geoTrigger(geo_event.annotation.mode, {
mode: m_mode, oldMode: oldMode});
- if (oldMode === m_this.modes.edit) {
+ if (oldMode === m_this.modes.edit || oldMode === m_this.modes.cursor) {
m_this.modified();
}
}

MODIFY
src/event.js
src/event.js
@@ -706,4 +706,32 @@ geo_event.annotation.mode = 'geo_annotation_mode';
*/
geo_event.annotation.boolean = 'geo_annotation_boolean';
+/**
+ * Triggered when an annotation is in cursor mode and the mouse is clicked.
+ *
+ * @event geo.event.annotation.cursor_click
+ * @type {geo.event.base}
+ * @property {geo.annotation} annotation The annotation that is being operated
+ * on.
+ * @property {string} operation The operation being performed.
+ * @property {boolean} [cancel] If the handle sets this to false, don't apply
+ * the operation to the annotation layer.
+ * @property {object} event The triggering event.
+ */
+geo_event.annotation.cursor_click = 'geo_annotation_cursor_click';
+
+/**
+ * Triggered when an annotation is in cursor mode and an action occurs.
+ *
+ * @event geo.event.annotation.cursor_action
+ * @type {geo.event.base}
+ * @property {geo.annotation} annotation The annotation that is being operated
+ * on.
+ * @property {string} operation The operation being performed.
+ * @property {boolean} [cancel] If the handle sets this to false, don't apply
+ * the operation to the annotation layer.
+ * @property {object} event The triggering event.
+ */
+geo_event.annotation.cursor_action = 'geo_annotation_cursor_action';
+
module.exports = geo_event;

MODIFY
src/main.styl
src/main.styl
&.annotation-xor
cursor embedurl("./css/cursor-crosshair-xor.svg") 12 12,crosshair
+ &.annotation-cursor
+ cursor crosshair
+ &.annotation-intersect
+ cursor embedurl("./css/cursor-crosshair-intersect.svg") 12 12,crosshair
+ &.annotation-difference
+ cursor embedurl("./css/cursor-crosshair-difference.svg") 12 12,crosshair
+ &.annotation-union
+ cursor embedurl("./css/cursor-crosshair-union.svg") 12 12,crosshair
+ &.annotation-xor
+ cursor embedurl("./css/cursor-crosshair-xor.svg") 12 12,crosshair
+
&.highlight-focus
&:after
content ""

MODIFY
src/mapInteractor.js
src/mapInteractor.js
@@ -134,6 +134,7 @@ var mapInteractor = function (args) {
m_boundKeys,
m_touchHandler,
m_state,
+ m_nextStateId = 0,
m_queue,
$node,
m_selectionLayer = null,
@@ -1117,6 +1118,7 @@ var mapInteractor = function (args) {
};
// store the state object
+ m_nextStateId += 1;
m_state = {
action: action,
actionRecord: actionRecord,
@@ -1124,6 +1126,7 @@ var mapInteractor = function (args) {
initialZoom: map.zoom(),
initialRotation: map.rotation(),
initialEventRotation: evt.rotation,
+ stateId: m_nextStateId,
delta: {x: 0, y: 0}
};

MODIFY
tests/cases/annotation.js
tests/cases/annotation.js
@@ -150,6 +150,13 @@ describe('geo.annotation', function () {
expect(ann.layer(layer)).toBe(ann);
expect(ann.layer()).toBe(layer);
});
+ it('id and new id', function () {
+ var ann = geo.annotation.annotation('test');
+ expect(ann.id()).not.toBe(undefined);
+ var id = ann.id();
+ expect(ann.newId()).toBe(ann);
+ expect(ann.id()).not.toEqual(id);
+ });
it('state', function () {
map = createMap();
layer = map.createLayer('annotation', {
@@ -376,7 +383,8 @@ describe('geo.annotation', function () {
style: {strokeWidth: 1},
createStyle: {strokeWidth: 2, fill: false},
editStyle: {strokeWidth: 3, strokeOpacity: 0.5},
- highlightStyle: {strokeWidth: 4, fillOpacity: 0.5}
+ highlightStyle: {strokeWidth: 4, fillOpacity: 0.5},
+ cursorStyle: {strokeWidth: 5, fillOpacity: 0.5}
};
var ann = geo.annotation.annotation('test', testStyles);
expect(ann.styleForState()).toEqual(testStyles.style);
@@ -390,6 +398,10 @@ describe('geo.annotation', function () {
expect(ann.styleForState(geo.annotation.state.highlight)).toEqual(testStyles.highlightStyle);
ann.state(geo.annotation.state.create);
expect(ann.styleForState()).toEqual(ann.styleForState(geo.annotation.state.create));
+ // cursor extends create, so it is special
+ expect(ann.styleForState(geo.annotation.state.cursor)).toEqual({
+ strokeWidth: 5, fill: false, strokeOpacity: 0.5, fillOpacity: 0.5
+ });
});
it('_addEditHandles', function () {
var ann = geo.annotation.annotation('test', {layer: layer}),

MODIFY
tests/cases/annotationLayer.js
tests/cases/annotationLayer.js
@@ -350,6 +350,33 @@ describe('geo.annotationLayer', function () {
expect(layer.displayDistance(c3, null, c1, 'display')).toBeCloseTo(10.63, 2);
expect(layer.displayDistance(c3, null, c2)).toBeCloseTo(4.47, 2);
});
+ it('cursor mode', function () {
+ expect(layer.mode()).toBe(null);
+ var rect = geo.annotation.rectangleAnnotation({
+ layer: layer,
+ corners: [{x: 0, y: 0}, {x: 1, y: 0}, {x: 1, y: 1}, {x: 0, y: 1}]});
+ const events = {};
+ layer.addAnnotation(rect, map.gcs());
+ expect(layer.mode(layer.modes.cursor, rect)).toBe(layer);
+ expect(layer.mode()).toBe(layer.modes.cursor);
+ layer.geoOn(geo.event.annotation.cursor_action, function (evt) { events.action = evt; });
+ layer.geoOn(geo.event.annotation.cursor_click, function (evt) { events.click = evt; });
+ map.interactor().simulateEvent('mousemove', {map: {x: 20, y: 20}});
+ expect(events.action).toBe(undefined);
+ expect(events.click).toBe(undefined);
+ map.interactor().simulateEvent('mousedown', {map: {x: 30, y: 30}, button: 'left'});
+ map.interactor().simulateEvent('mousemove', {map: {x: 40, y: 30}, button: 'left'});
+ expect(events.action).not.toBe(undefined);
+ expect(events.click).toBe(undefined);
+ map.interactor().simulateEvent('mouseup', {map: {x: 40, y: 30}, button: 'left'});
+ expect(events.action).not.toBe(undefined);
+ expect(events.click).toBe(undefined);
+ map.interactor().simulateEvent('mousedown', {map: {x: 50, y: 30}, button: 'left'});
+ map.interactor().simulateEvent('mouseup', {map: {x: 50, y: 30}, button: 'left'});
+ expect(events.click).not.toBe(undefined);
+ expect(layer.mode(null)).toBe(layer);
+ expect(layer.mode()).toBe(null);
+ });
});
describe('Private utility functions', function () {
var map, layer, point, rect, rect2, editActionEvent = 0;

feat: Add a cursor annotation mode.

See the annotation example for the utility of this mode.