diff --git a/CHANGELOG.md b/CHANGELOG.md index c5b30734da..ea938b0dd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # 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 diff --git a/examples/annotations/index.pug b/examples/annotations/index.pug index 1b4b14a96a..e07cb3882b 100644 --- a/examples/annotations/index.pug +++ b/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') diff --git a/examples/annotations/main.css b/examples/annotations/main.css index f1062e2f5c..b5579ac784 100644 --- a/examples/annotations/main.css +++ b/examples/annotations/main.css @@ -65,6 +65,19 @@ 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; } diff --git a/examples/annotations/main.js b/examples/annotations/main.js index 1f3c65d565..1320d16612 100644 --- a/examples/annotations/main.js +++ b/examples/annotations/main.js @@ -2,453 +2,575 @@ var annotationDebug = {}; -// Run after the DOM loads -$(function () { - 'use strict'; +var layer, fromButtonSelect, fromGeojsonUpdate; - var layer, fromButtonSelect, fromGeojsonUpdate; +// get the query parameters and set controls appropriately +var query = utils.getQuery(); +$('#clickmode').val(query.clickmode || 'edit'); +$('#keepadding').prop('checked', query.keepadding === 'true'); +$('#showLabels').prop('checked', query.labels !== 'false'); +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. +var initialGeoJSON = query.geojson; - // get the query parameters and set controls appropriately - var query = utils.getQuery(); - $('#clickmode').val(query.clickmode || 'edit'); - $('#keepadding').prop('checked', query.keepadding === 'true'); - $('#showLabels').prop('checked', query.labels !== 'false'); - if (query.lastannotation) { - $('.annotationtype button').removeClass('lastused'); - $('.annotationtype button#' + query.lastannotation).addClass('lastused'); +// respond to changes in our controls +$('#controls').on('change', change_controls); +$('#geojson[type=textarea]').on('input propertychange', change_geojson); +$('#controls').on('click', 'a', select_control); +$('.annotationtype button').on('click', select_annotation); +$('#editdialog').on('submit', edit_update); + +$('#controls').toggleClass('no-controls', query.controls === 'false'); + +// start the map near Fresno unless the query parameters say to do otherwise +var map = geo.map({ + node: '#map', + center: { + x: query.x ? +query.x : -119.150, + y: query.y ? +query.y : 36.712 + }, + zoom: query.zoom ? +query.zoom : 10, + rotation: query.rotation ? +query.rotation * Math.PI / 180 : 0 +}); +// allow some query parameters to specify what map we will show +if (query.map !== 'false') { + if (query.map !== 'satellite') { + annotationDebug.mapLayer = map.createLayer('osm'); + } + if (query.map === 'satellite' || query.map === 'dual') { + annotationDebug.satelliteLayer = map.createLayer('osm', {url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', opacity: query.map === 'dual' ? 0.25 : 1}); } - // 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. - var initialGeoJSON = query.geojson; +} +// create an annotation layer +layer = map.createLayer('annotation', { + renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, + annotations: query.renderer ? undefined : geo.listAnnotations(), + showLabels: query.labels !== 'false', + clickToEdit: !query.clickmode || query.clickmode === 'edit' +}); +// bind to the mouse click and annotation mode events +layer.geoOn(geo.event.mouseclick, mouseClickToStart); +layer.geoOn(geo.event.annotation.mode, handleModeChange); +layer.geoOn(geo.event.annotation.add, handleAnnotationChange); +layer.geoOn(geo.event.annotation.update, handleAnnotationChange); +layer.geoOn(geo.event.annotation.remove, handleAnnotationChange); +layer.geoOn(geo.event.annotation.state, handleAnnotationChange); - // respond to changes in our controls - $('#controls').on('change', change_controls); - $('#geojson[type=textarea]').on('input propertychange', change_geojson); - $('#controls').on('click', 'a', select_control); - $('.annotationtype button').on('click', select_annotation); - $('#editdialog').on('submit', edit_update); +let brushLayer; - $('#controls').toggleClass('no-controls', query.controls === 'false'); +map.draw(); - // start the map near Fresno unless the query parameters say to do otherwise - var map = geo.map({ - node: '#map', - center: { - x: query.x ? +query.x : -119.150, - y: query.y ? +query.y : 36.712 - }, - zoom: query.zoom ? +query.zoom : 10, - rotation: query.rotation ? +query.rotation * Math.PI / 180 : 0 - }); - // allow some query parameters to specify what map we will show - if (query.map !== 'false') { - if (query.map !== 'satellite') { - annotationDebug.mapLayer = map.createLayer('osm'); +// pick which button is initially highlighted based on query parameters. +if (query.lastused || query.active) { + if (query.active && query.clickmode !== 'brush') { + layer.mode(query.active); + } else { + $('.annotationtype button').removeClass('lastused active'); + $('.annotationtype button#' + (query.lastused || query.active)).addClass('lastused'); + } +} + +// if we have geojson as a query parameter, populate our annotations +if (initialGeoJSON) { + 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; +annotationDebug.query = query; + +/** + * When the mouse is clicked, switch to adding an annotation if appropriate. + * + * @param {geo.event} evt geojs event. + */ +function mouseClickToStart(evt) { + if (evt.handled || query.clickmode !== 'add') { + return; + } + if (evt.buttonsDown.left) { + if ($('.annotationtype button.lastused').hasClass('active') && query.keepadding === 'true') { + return; } - if (query.map === 'satellite' || query.map === 'dual') { - annotationDebug.satelliteLayer = map.createLayer('osm', {url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png', opacity: query.map === 'dual' ? 0.25 : 1}); + select_button('.annotationtype button.lastused'); + } else if (evt.buttonsDown.right) { + select_button('.annotationtype button#' + + $('.annotationtype button.lastused').attr('next')); + } +} + +/** + * 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; } - // create an annotation layer - layer = map.createLayer('annotation', { - renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, - annotations: query.renderer ? undefined : geo.listAnnotations(), - showLabels: query.labels !== 'false', - clickToEdit: !query.clickmode || query.clickmode === 'edit' - }); - // bind to the mouse click and annotation mode events - layer.geoOn(geo.event.mouseclick, mouseClickToStart); - layer.geoOn(geo.event.annotation.mode, handleModeChange); - layer.geoOn(geo.event.annotation.add, handleAnnotationChange); - layer.geoOn(geo.event.annotation.update, handleAnnotationChange); - layer.geoOn(geo.event.annotation.remove, handleAnnotationChange); - layer.geoOn(geo.event.annotation.state, handleAnnotationChange); + 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(); +} - // pick which button is initially highlighted based on query parameters. - if (query.lastused || query.active) { - if (query.active) { - layer.mode(query.active); - } else { - $('.annotationtype button').removeClass('lastused active'); - $('.annotationtype button#' + query.lastused).addClass('lastused'); - } +/** + * Handle changes to our controls. + * + * @param evt jquery evt that triggered this call. + */ +function change_controls(evt) { + var ctl = $(evt.target), + param = ctl.attr('param-name'), + value = ctl.val(); + if (ctl.is('[type="checkbox"]')) { + value = ctl.is(':checked') ? 'true' : 'false'; + } + if (value === '' && ctl.attr('placeholder')) { + value = ctl.attr('placeholder'); + } + if (!param || value === query[param]) { + return; } + switch (param) { + case 'labels': + layer.options('showLabels', '' + value !== 'false'); + layer.draw(); + break; + 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; + if (value === '' || (ctl.attr('placeholder') && + value === ctl.attr('placeholder'))) { + delete query[param]; + } + // 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(); + } +} - // if we have geojson as a query parameter, populate our annotations - if (initialGeoJSON) { - layer.geojson(initialGeoJSON, true); +/** + * Handle changes to the geojson. + * + * @param evt jquery evt that triggered this call. + */ +function change_geojson(evt) { + var ctl = $(evt.target), + value = ctl.val(); + // when we update the geojson from the textarea control, raise a flag so we + // (a) ignore bad geojson, and (b) don't replace the user's geojson with + // the auto-generated geojson + fromGeojsonUpdate = true; + var result = layer.geojson(value, 'update'); + if (query.save && result !== undefined) { + var geojson = layer.geojson(); + query.geojson = geojson ? JSON.stringify(geojson) : undefined; + utils.setQuery(query); } + fromGeojsonUpdate = false; +} - // expose some internal parameters so you can examine them from the console - annotationDebug.map = map; - annotationDebug.layer = layer; - annotationDebug.query = query; +/** + * Handle selecting an annotation button. + * + * @param evt jquery evt that triggered this call. + */ +function select_annotation(evt) { + select_button(evt.target); +} - /** - * When the mouse is clicked, switch to adding an annotation if appropriate. - * - * @param {geo.event} evt geojs event. - */ - function mouseClickToStart(evt) { - if (evt.handled || query.clickmode !== 'add') { - return; - } - if (evt.buttonsDown.left) { - if ($('.annotationtype button.lastused').hasClass('active') && query.keepadding === 'true') { - return; - } - select_button('.annotationtype button.lastused'); - } else if (evt.buttonsDown.right) { - select_button('.annotationtype button#' + - $('.annotationtype button.lastused').attr('next')); - } +/** + * Select an annotation button by jquery selector. + * + * @param {object} ctl a jquery selector or element. + */ +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; + layer.mode(wasactive ? null : id); + fromButtonSelect = false; +} - /** - * Handle changes to our controls. - * - * @param evt jquery evt that triggered this call. - */ - function change_controls(evt) { - var ctl = $(evt.target), - param = ctl.attr('param-name'), - value = ctl.val(); - if (ctl.is('[type="checkbox"]')) { - value = ctl.is(':checked') ? 'true' : 'false'; - } - if (value === '' && ctl.attr('placeholder')) { - value = ctl.attr('placeholder'); +/** + * When the annotation mode changes, update the controls to reflect it. + * + * @param {geo.event} evt a geojs mode change event. + */ +function handleModeChange(evt) { + // highlight the current buttons based on the current mode + var mode = layer.mode(); + $('.annotationtype button').removeClass('active'); + if (mode) { + $('.annotationtype button').removeClass('lastused active'); + $('.annotationtype button#' + mode).addClass('lastused active'); + } + $('#instructions').attr( + 'annotation', $('.annotationtype button.active').attr('id') || 'none'); + query.active = $('.annotationtype button.active').attr('id') || undefined; + query.lastused = query.active ? undefined : $('.annotationtype button.lastused').attr('id'); + 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' && query.clickmode !== 'brush') { + layer.mode($('.annotationtype button.lastused').attr('id')); + } +} + +/** + * When an annotation is created or removed, update our list of annotations. + * + * @param {geo.event} evt a geojs mode change event. + */ +function handleAnnotationChange(evt) { + var annotations = layer.annotations(); + var ids = annotations.map(function (annotation) { + return annotation.id(); + }); + var present = []; + $('#annotationlist .entry').each(function () { + var entry = $(this); + if (entry.attr('id') === 'sample') { + return; } - if (!param || value === query[param]) { + var id = entry.attr('annotation-id'); + // Remove deleted annotations + if ($.inArray(id, ids) < 0) { + entry.remove(); return; } - switch (param) { - case 'labels': - layer.options('showLabels', '' + value !== 'false'); - layer.draw(); - break; - case 'clickmode': - layer.options('clickToEdit', value === 'edit'); - layer.draw(); - break; + present.push(id); + // update existing elements + entry.find('.entry-name').text(layer.annotationById(id).name()); + }); + // Add if new and fully created + $.each(ids, function (idx, id) { + if ($.inArray(id, present) >= 0) { + return; } - query[param] = value; - if (value === '' || (ctl.attr('placeholder') && - value === ctl.attr('placeholder'))) { - delete query[param]; + var annotation = layer.annotationById(id); + if (annotation.state() === geo.annotation.state.create) { + return; } - // update our query parameters, os when you reload the page it is in the - // same state - utils.setQuery(query); - } - - /** - * Handle changes to the geojson. - * - * @param evt jquery evt that triggered this call. - */ - function change_geojson(evt) { - var ctl = $(evt.target), - value = ctl.val(); - // when we update the geojson from the textarea control, raise a flag so we - // (a) ignore bad geojson, and (b) don't replace the user's geojson with - // the auto-generated geojson - fromGeojsonUpdate = true; - var result = layer.geojson(value, 'update'); - if (query.save && result !== undefined) { - var geojson = layer.geojson(); + var entry = $('#annotationlist .entry#sample').clone(); + entry.attr({id: '', 'annotation-id': id}); + entry.find('.entry-name').text(annotation.name()); + $('#annotationlist').append(entry); + }); + $('#annotationheader').css( + 'display', $('#annotationlist .entry').length <= 1 ? 'none' : 'block'); + if (!fromGeojsonUpdate) { + // update the geojson textarea + var geojson = layer.geojson(); + $('#geojson').val(geojson ? JSON.stringify(geojson, undefined, 2) : ''); + if (query.save) { query.geojson = geojson ? JSON.stringify(geojson) : undefined; utils.setQuery(query); } - fromGeojsonUpdate = false; } +} - /** - * Handle selecting an annotation button. - * - * @param evt jquery evt that triggered this call. - */ - function select_annotation(evt) { - select_button(evt.target); +/** + * Handle selecting a control. + * + * @param evt jquery evt that triggered this call. + */ +function select_control(evt) { + var mode, + ctl = $(evt.target), + action = ctl.attr('action'), + id = ctl.closest('.entry').attr('annotation-id'), + annotation = layer.annotationById(id); + switch (action) { + case 'adjust': + layer.mode(layer.modes.edit, annotation); + layer.draw(); + break; + case 'edit': + show_edit_dialog(id); + break; + case 'remove': + layer.removeAnnotation(annotation); + break; + case 'remove-all': + fromButtonSelect = true; + mode = layer.mode(); + layer.mode(null); + layer.removeAllAnnotations(); + layer.mode(mode); + fromButtonSelect = false; + break; } +} - /** - * Select an annotation button by jquery selector. - * - * @param {object} ctl a jquery selector or element. - */ - function select_button(ctl) { - ctl = $(ctl); - var wasactive = ctl.hasClass('active'), - id = ctl.attr('id'); - fromButtonSelect = true; - layer.mode(wasactive ? null : id); - fromButtonSelect = false; - } +/** + * Show the edit dialog for a particular annotation. + * + * @param {number} id the annotation id to edit. + */ +function show_edit_dialog(id) { + var annotation = layer.annotationById(id), + type = annotation.type(), + typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'), + opt = annotation.options(), + dlg = $('#editdialog'); - /** - * When the annotation mode changes, update the controls to reflect it. - * - * @param {geo.event} evt a geojs mode change event. - */ - function handleModeChange(evt) { - // highlight the current buttons based on the current mode - var mode = layer.mode(); - $('.annotationtype button').removeClass('active'); - if (mode) { - $('.annotationtype button').removeClass('lastused active'); - $('.annotationtype button#' + mode).addClass('lastused active'); + $('#edit-validation-error', dlg).text(''); + dlg.attr('annotation-id', id); + dlg.attr('annotation-type', type); + $('[option="name"]', dlg).val(annotation.name()); + $('[option="label"]', dlg).val(annotation.label(undefined, true)); + $('[option="description"]', dlg).val(annotation.description()); + // populate each control with the current value of the annotation + $('.form-group[annotation-types]').each(function () { + var ctl = $(this), + key = $('[option]', ctl).attr('option'), + format = $('[option]', ctl).attr('format'), + value; + if (!ctl.attr('annotation-types').match(typeMatch)) { + // if a property doesn't exist for the current annotation's type, hide + // the control + ctl.hide(); + return; } - $('#instructions').attr( - 'annotation', $('.annotationtype button.active').attr('id') || 'none'); - query.active = $('.annotationtype button.active').attr('id') || undefined; - query.lastused = query.active ? undefined : $('.annotationtype button.lastused').attr('id'); - 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') { - layer.mode($('.annotationtype button.lastused').attr('id')); + ctl.show(); + switch ($('[option]', ctl).attr('optiontype')) { + case 'option': + value = opt[key]; + break; + case 'label': + value = (opt.labelStyle || {})[key]; + break; + default: + value = opt.style[key]; + break; } - } - - /** - * When an annotation is created or removed, update our list of annotations. - * - * @param {geo.event} evt a geojs mode change event. - */ - function handleAnnotationChange(evt) { - var annotations = layer.annotations(); - var ids = annotations.map(function (annotation) { - return annotation.id(); - }); - var present = []; - $('#annotationlist .entry').each(function () { - var entry = $(this); - if (entry.attr('id') === 'sample') { - return; - } - var id = entry.attr('annotation-id'); - // Remove deleted annotations - if ($.inArray(id, ids) < 0) { - entry.remove(); - return; - } - present.push(id); - // update existing elements - entry.find('.entry-name').text(layer.annotationById(id).name()); - }); - // Add if new and fully created - $.each(ids, function (idx, id) { - if ($.inArray(id, present) >= 0) { - return; - } - var annotation = layer.annotationById(id); - if (annotation.state() === geo.annotation.state.create) { - return; - } - var entry = $('#annotationlist .entry#sample').clone(); - entry.attr({id: '', 'annotation-id': id}); - entry.find('.entry-name').text(annotation.name()); - $('#annotationlist').append(entry); - }); - $('#annotationheader').css( - 'display', $('#annotationlist .entry').length <= 1 ? 'none' : 'block'); - if (!fromGeojsonUpdate) { - // update the geojson textarea - var geojson = layer.geojson(); - $('#geojson').val(geojson ? JSON.stringify(geojson, undefined, 2) : ''); - if (query.save) { - query.geojson = geojson ? JSON.stringify(geojson) : undefined; - utils.setQuery(query); - } + switch (format) { + case 'angle': + if (value !== undefined && value !== null && value !== '') { + value = '' + +(+value * 180.0 / Math.PI).toFixed(4) + ' deg'; + } + break; + case 'color': + // always show colors as hex values + value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}, 'needed'); + break; + case 'coordinate2': + if (value !== undefined && value !== null && value !== '') { + value = '' + value.x + ', ' + value.y; + } } - } + if ((value === undefined || value === '' || value === null) && $('[option]', ctl).is('select')) { + value = $('[option] option', ctl).eq(0).val(); + } + $('[option]', ctl).val(value === undefined ? '' : '' + value); + }); + dlg.one('shown.bs.modal', function () { + $('[option="name"]', dlg).focus(); + }); + dlg.modal(); +} - /** - * Handle selecting a control. - * - * @param evt jquery evt that triggered this call. - */ - function select_control(evt) { - var mode, - ctl = $(evt.target), - action = ctl.attr('action'), - id = ctl.closest('.entry').attr('annotation-id'), - annotation = layer.annotationById(id); - switch (action) { - case 'adjust': - layer.mode(layer.modes.edit, annotation); - layer.draw(); +/** + * Update an annotation from values in the edit dialog. + * + * @param evt jquery evt that triggered this call. + */ +function edit_update(evt) { + evt.preventDefault(); + var dlg = $('#editdialog'), + id = dlg.attr('annotation-id'), + annotation = layer.annotationById(id), + opt = annotation.options(), + type = annotation.type(), + typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'), + newopt = {style: {}, labelStyle: {}}, + error; + + // validate form values + $('.form-group[annotation-types]').each(function () { + var ctl = $(this), + key = $('[option]', ctl).attr('option'), + format = $('[option]', ctl).attr('format'), + value, oldvalue; + if (!ctl.attr('annotation-types').match(typeMatch)) { + return; + } + value = $('[option]', ctl).val(); + switch (format) { + case 'angle': + if (/^\s*[.0-9eE]+\s*$/.exec(value)) { + value += 'deg'; + } break; - case 'edit': - show_edit_dialog(id); + } + switch (key) { + case 'textScaled': + if (['true', 'on', 'yes'].indexOf(value.trim().toLowerCase()) >= 0) { + value = map.zoom(); + } break; - case 'remove': - layer.removeAnnotation(annotation); + } + value = layer.validateAttribute(value, format); + switch ($('[option]', ctl).attr('optiontype')) { + case 'option': + oldvalue = opt[key]; + break; + case 'label': + oldvalue = (opt.labelStyle || {})[key]; break; - case 'remove-all': - fromButtonSelect = true; - mode = layer.mode(); - layer.mode(null); - layer.removeAllAnnotations(); - layer.mode(mode); - fromButtonSelect = false; + default: + oldvalue = opt.style[key]; break; } - } - - /** - * Show the edit dialog for a particular annotation. - * - * @param {number} id the annotation id to edit. - */ - function show_edit_dialog(id) { - var annotation = layer.annotationById(id), - type = annotation.type(), - typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'), - opt = annotation.options(), - dlg = $('#editdialog'); - - $('#edit-validation-error', dlg).text(''); - dlg.attr('annotation-id', id); - dlg.attr('annotation-type', type); - $('[option="name"]', dlg).val(annotation.name()); - $('[option="label"]', dlg).val(annotation.label(undefined, true)); - $('[option="description"]', dlg).val(annotation.description()); - // populate each control with the current value of the annotation - $('.form-group[annotation-types]').each(function () { - var ctl = $(this), - key = $('[option]', ctl).attr('option'), - format = $('[option]', ctl).attr('format'), - value; - if (!ctl.attr('annotation-types').match(typeMatch)) { - // if a property doesn't exist for the current annotation's type, hide - // the control - ctl.hide(); - return; - } - ctl.show(); - switch ($('[option]', ctl).attr('optiontype')) { - case 'option': - value = opt[key]; - break; - case 'label': - value = (opt.labelStyle || {})[key]; - break; - default: - value = opt.style[key]; - break; - } - switch (format) { - case 'angle': - if (value !== undefined && value !== null && value !== '') { - value = '' + +(+value * 180.0 / Math.PI).toFixed(4) + ' deg'; - } - break; - case 'color': - // always show colors as hex values - value = geo.util.convertColorToHex(value || {r: 0, g: 0, b: 0}, 'needed'); - break; - case 'coordinate2': - if (value !== undefined && value !== null && value !== '') { - value = '' + value.x + ', ' + value.y; - } - } - if ((value === undefined || value === '' || value === null) && $('[option]', ctl).is('select')) { - value = $('[option] option', ctl).eq(0).val(); - } - $('[option]', ctl).val(value === undefined ? '' : '' + value); - }); - dlg.one('shown.bs.modal', function () { - $('[option="name"]', dlg).focus(); - }); - dlg.modal(); - } - - /** - * Update an annotation from values in the edit dialog. - * - * @param evt jquery evt that triggered this call. - */ - function edit_update(evt) { - evt.preventDefault(); - var dlg = $('#editdialog'), - id = dlg.attr('annotation-id'), - annotation = layer.annotationById(id), - opt = annotation.options(), - type = annotation.type(), - typeMatch = new RegExp('(^| )(' + type + '|all)( |$)'), - newopt = {style: {}, labelStyle: {}}, - error; - - // validate form values - $('.form-group[annotation-types]').each(function () { - var ctl = $(this), - key = $('[option]', ctl).attr('option'), - format = $('[option]', ctl).attr('format'), - value, oldvalue; - if (!ctl.attr('annotation-types').match(typeMatch)) { - return; - } - value = $('[option]', ctl).val(); - switch (format) { - case 'angle': - if (/^\s*[.0-9eE]+\s*$/.exec(value)) { - value += 'deg'; - } - break; - } - switch (key) { - case 'textScaled': - if (['true', 'on', 'yes'].indexOf(value.trim().toLowerCase()) >= 0) { - value = map.zoom(); - } - break; - } - value = layer.validateAttribute(value, format); + if (value === oldvalue || (oldvalue === undefined && value === '')) { + // don't change anything + } else if (value === undefined) { + error = $('label', ctl).text() + ' is not a valid value'; + } else { switch ($('[option]', ctl).attr('optiontype')) { case 'option': - oldvalue = opt[key]; + newopt[key] = value; break; case 'label': - oldvalue = (opt.labelStyle || {})[key]; + newopt.labelStyle[key] = value; break; default: - oldvalue = opt.style[key]; + newopt.style[key] = value; break; } - if (value === oldvalue || (oldvalue === undefined && value === '')) { - // don't change anything - } else if (value === undefined) { - error = $('label', ctl).text() + ' is not a valid value'; - } else { - switch ($('[option]', ctl).attr('optiontype')) { - case 'option': - newopt[key] = value; - break; - case 'label': - newopt.labelStyle[key] = value; - break; - default: - newopt.style[key] = value; - break; - } - } - }); - if (error) { - $('#edit-validation-error', dlg).text(error); - return; } - annotation.name($('[option="name"]', dlg).val()); - annotation.label($('[option="label"]', dlg).val() || null); - annotation.description($('[option="description"]', dlg).val() || ''); - annotation.options(newopt).draw(); - - dlg.modal('hide'); - // refresh the annotation list - handleAnnotationChange(); + }); + if (error) { + $('#edit-validation-error', dlg).text(error); + return; } -}); + annotation.name($('[option="name"]', dlg).val()); + annotation.label($('[option="label"]', dlg).val() || null); + annotation.description($('[option="description"]', dlg).val() || ''); + annotation.options(newopt).draw(); + + dlg.modal('hide'); + // refresh the annotation list + handleAnnotationChange(); +} diff --git a/examples/annotations/thumb.jpg b/examples/annotations/thumb.jpg index 9aed2edb92..73c1c6116e 100755 Binary files a/examples/annotations/thumb.jpg and b/examples/annotations/thumb.jpg differ diff --git a/src/action.js b/src/action.js index 2cf258a72f..2db9367b1d 100644 --- a/src/action.js +++ b/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; diff --git a/src/annotation/annotation.js b/src/annotation/annotation.js index 6c370ed05b..9b1d22537e 100644 --- a/src/annotation/annotation.js +++ b/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 */ - ['createStyle', 'editStyle', 'editHandleStyle', 'labelStyle', 'highlightStyle' + /** + * Calls {@link geo.annotation#style} with `styleType='cursorStyle'`. + * @function cursorStyle + * @memberof geo.annotation + * @instance + */ + ['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 || {}; }; diff --git a/src/annotation/polygonAnnotation.js b/src/annotation/polygonAnnotation.js index dde0234a43..bc3b1c1be3 100644 --- a/src/annotation/polygonAnnotation.js +++ b/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 }); diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 60119e1ff4..5f16a1a25b 100644 --- a/src/annotationLayer.js +++ b/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(); } } diff --git a/src/event.js b/src/event.js index 50a49286e4..e3142be560 100644 --- a/src/event.js +++ b/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; diff --git a/src/main.styl b/src/main.styl index 13ca13c989..b4639ba0cb 100644 --- a/src/main.styl +++ b/src/main.styl @@ -58,6 +58,17 @@ &.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 "" diff --git a/src/mapInteractor.js b/src/mapInteractor.js index 414651a1e8..133197ce91 100644 --- a/src/mapInteractor.js +++ b/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} }; diff --git a/tests/cases/annotation.js b/tests/cases/annotation.js index 41b997ca6e..b9b23a60ea 100644 --- a/tests/cases/annotation.js +++ b/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}), diff --git a/tests/cases/annotationLayer.js b/tests/cases/annotationLayer.js index 254bde1c3d..3005bd372d 100644 --- a/tests/cases/annotationLayer.js +++ b/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;