Merge pull request #10632 from jahow/draw-interaction-append-coords
Draw interaction: Append coordinates to polygons and lines (reworked)
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
layout: example.html
|
||||||
|
title: Tracing around a polygon
|
||||||
|
shortdesc: Example of setting up a draw interaction to easily snap to an existing feature.
|
||||||
|
docs: >
|
||||||
|
This example showcases how the draw interaction API can be set up to make snapping along
|
||||||
|
an existing geometry easier while preserving topology, which is sometimes called "tracing".
|
||||||
|
When the user clicks on two different points on the Idaho state border,
|
||||||
|
the part of the border comprised between these two points is added to
|
||||||
|
the currently drawn feature.
|
||||||
|
This leverages the `appendCoordinates` method of the `ol/interaction/Draw` interaction.
|
||||||
|
tags: "draw, trace, snap, vector, topology"
|
||||||
|
---
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
<form class="form-inline">
|
||||||
|
<label>Geometry type </label>
|
||||||
|
<select id="type">
|
||||||
|
<option value="Polygon">Polygon</option>
|
||||||
|
<option value="LineString">LineString</option>
|
||||||
|
<option value="None">None</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import Map from '../src/ol/Map.js';
|
||||||
|
import View from '../src/ol/View.js';
|
||||||
|
import Draw from '../src/ol/interaction/Draw.js';
|
||||||
|
import Snap from '../src/ol/interaction/Snap.js';
|
||||||
|
import Style from '../src/ol/style/Style.js';
|
||||||
|
import Stroke from '../src/ol/style/Stroke.js';
|
||||||
|
import Fill from '../src/ol/style/Fill.js';
|
||||||
|
import GeoJSON from '../src/ol/format/GeoJSON.js';
|
||||||
|
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||||
|
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
|
||||||
|
import LineString from '../src/ol/geom/LineString.js';
|
||||||
|
import Feature from '../src/ol/Feature.js';
|
||||||
|
|
||||||
|
// math utilities
|
||||||
|
|
||||||
|
// coordinates; will return the length of the [a, b] segment
|
||||||
|
function length(a, b) {
|
||||||
|
return Math.sqrt((b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// coordinates; will return true if c is on the [a, b] segment
|
||||||
|
function isOnSegment(c, a, b) {
|
||||||
|
const lengthAc = length(a, c);
|
||||||
|
const lengthAb = length(a, b);
|
||||||
|
const dot = ((c[0] - a[0]) * (b[0] - a[0]) + (c[1] - a[1]) * (b[1] - a[1])) / lengthAb;
|
||||||
|
return Math.abs(lengthAc - dot) < 1e-6 && lengthAc < lengthAb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// modulo for negative values, eg: mod(-1, 4) returns 3
|
||||||
|
function mod(a, b) {
|
||||||
|
return ((a % b) + b) % b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a coordinates array which contains the segments of the feature's
|
||||||
|
// outer ring between the start and end points
|
||||||
|
// Note: this assumes the base feature is a single polygon
|
||||||
|
function getPartialRingCoords(feature, startPoint, endPoint) {
|
||||||
|
let polygon = feature.getGeometry();
|
||||||
|
if (polygon.getType() === 'MultiPolygon') {
|
||||||
|
polygon = polygon.getPolygon(0);
|
||||||
|
}
|
||||||
|
const ringCoords = polygon.getLinearRing().getCoordinates();
|
||||||
|
|
||||||
|
let i, pointA, pointB, startSegmentIndex = -1;
|
||||||
|
for (i = 0; i < ringCoords.length; i++) {
|
||||||
|
pointA = ringCoords[i];
|
||||||
|
pointB = ringCoords[mod(i + 1, ringCoords.length)];
|
||||||
|
|
||||||
|
// check if this is the start segment dot product
|
||||||
|
if (isOnSegment(startPoint, pointA, pointB)) {
|
||||||
|
startSegmentIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwCoordinates = [];
|
||||||
|
let cwLength = 0;
|
||||||
|
const ccwCoordinates = [];
|
||||||
|
let ccwLength = 0;
|
||||||
|
|
||||||
|
// build clockwise coordinates
|
||||||
|
for (i = 0; i < ringCoords.length; i++) {
|
||||||
|
pointA = i === 0 ? startPoint : ringCoords[mod(i + startSegmentIndex, ringCoords.length)];
|
||||||
|
pointB = ringCoords[mod(i + startSegmentIndex + 1, ringCoords.length)];
|
||||||
|
cwCoordinates.push(pointA);
|
||||||
|
|
||||||
|
if (isOnSegment(endPoint, pointA, pointB)) {
|
||||||
|
cwCoordinates.push(endPoint);
|
||||||
|
cwLength += length(pointA, endPoint);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
cwLength += length(pointA, pointB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build counter-clockwise coordinates
|
||||||
|
for (i = 0; i < ringCoords.length; i++) {
|
||||||
|
pointA = ringCoords[mod(startSegmentIndex - i, ringCoords.length)];
|
||||||
|
pointB = i === 0 ? startPoint : ringCoords[mod(startSegmentIndex - i + 1, ringCoords.length)];
|
||||||
|
ccwCoordinates.push(pointB);
|
||||||
|
|
||||||
|
if (isOnSegment(endPoint, pointA, pointB)) {
|
||||||
|
ccwCoordinates.push(endPoint);
|
||||||
|
ccwLength += length(endPoint, pointB);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
ccwLength += length(pointA, pointB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the shortest path
|
||||||
|
return ccwLength < cwLength ? ccwCoordinates : cwCoordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// layers definition
|
||||||
|
|
||||||
|
const raster = new TileLayer({
|
||||||
|
source: new OSM()
|
||||||
|
});
|
||||||
|
|
||||||
|
// features in this layer will be snapped to
|
||||||
|
const baseVector = new VectorLayer({
|
||||||
|
source: new VectorSource({
|
||||||
|
format: new GeoJSON(),
|
||||||
|
url: 'https://ahocevar.com/geoserver/wfs?service=wfs&request=getfeature&typename=topp:states&cql_filter=STATE_NAME=\'Idaho\'&outputformat=application/json'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is were the drawn features go
|
||||||
|
const drawVector = new VectorLayer({
|
||||||
|
source: new VectorSource(),
|
||||||
|
style: new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'rgba(100, 255, 0, 1)',
|
||||||
|
width: 2
|
||||||
|
}),
|
||||||
|
fill: new Fill({
|
||||||
|
color: 'rgba(100, 255, 0, 0.3)'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// this line only appears when we're tracing a feature outer ring
|
||||||
|
const previewLine = new Feature({
|
||||||
|
geometry: new LineString([])
|
||||||
|
});
|
||||||
|
const previewVector = new VectorLayer({
|
||||||
|
source: new VectorSource({
|
||||||
|
features: [previewLine]
|
||||||
|
}),
|
||||||
|
style: new Style({
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: 'rgba(255, 0, 0, 1)',
|
||||||
|
width: 2
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = new Map({
|
||||||
|
layers: [raster, baseVector, drawVector, previewVector],
|
||||||
|
target: 'map',
|
||||||
|
view: new View({
|
||||||
|
center: [-12986427, 5678422],
|
||||||
|
zoom: 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let drawInteraction, tracingFeature, startPoint, endPoint;
|
||||||
|
let drawing = false;
|
||||||
|
|
||||||
|
const getFeatureOptions = {
|
||||||
|
hitTolerance: 10,
|
||||||
|
layerFilter: (layer) => {
|
||||||
|
return layer === baseVector;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// the click event is used to start/end tracing around a feature
|
||||||
|
map.on('click', (event) => {
|
||||||
|
if (!drawing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hit = false;
|
||||||
|
map.forEachFeatureAtPixel(
|
||||||
|
event.pixel,
|
||||||
|
(feature) => {
|
||||||
|
if (tracingFeature && feature !== tracingFeature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hit = true;
|
||||||
|
const coord = map.getCoordinateFromPixel(event.pixel);
|
||||||
|
|
||||||
|
// second click on the tracing feature: append the ring coordinates
|
||||||
|
if (feature === tracingFeature) {
|
||||||
|
endPoint = tracingFeature.getGeometry().getClosestPoint(coord);
|
||||||
|
const appendCoords = getPartialRingCoords(tracingFeature, startPoint, endPoint);
|
||||||
|
drawInteraction.removeLastPoint();
|
||||||
|
drawInteraction.appendCoordinates(appendCoords);
|
||||||
|
tracingFeature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start tracing on the feature ring
|
||||||
|
tracingFeature = feature;
|
||||||
|
startPoint = tracingFeature.getGeometry().getClosestPoint(coord);
|
||||||
|
},
|
||||||
|
getFeatureOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hit) {
|
||||||
|
// clear current tracing feature & preview
|
||||||
|
previewLine.getGeometry().setCoordinates([]);
|
||||||
|
tracingFeature = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// the pointermove event is used to show a preview of the result of the tracing
|
||||||
|
map.on('pointermove', (event) => {
|
||||||
|
if (tracingFeature && drawing) {
|
||||||
|
let coord = null;
|
||||||
|
map.forEachFeatureAtPixel(
|
||||||
|
event.pixel,
|
||||||
|
(feature) => {
|
||||||
|
if (tracingFeature === feature) {
|
||||||
|
coord = map.getCoordinateFromPixel(event.pixel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFeatureOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
let previewCoords = [];
|
||||||
|
if (coord) {
|
||||||
|
endPoint = tracingFeature.getGeometry().getClosestPoint(coord);
|
||||||
|
previewCoords = getPartialRingCoords(tracingFeature, startPoint, endPoint);
|
||||||
|
}
|
||||||
|
previewLine.getGeometry().setCoordinates(previewCoords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapInteraction = new Snap({
|
||||||
|
source: baseVector.getSource()
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeSelect = document.getElementById('type');
|
||||||
|
|
||||||
|
function addInteraction() {
|
||||||
|
const value = typeSelect.value;
|
||||||
|
if (value !== 'None') {
|
||||||
|
drawInteraction = new Draw({
|
||||||
|
source: drawVector.getSource(),
|
||||||
|
type: typeSelect.value
|
||||||
|
});
|
||||||
|
drawInteraction.on('drawstart', () => {
|
||||||
|
drawing = true;
|
||||||
|
});
|
||||||
|
drawInteraction.on('drawend', () => {
|
||||||
|
drawing = false;
|
||||||
|
previewLine.getGeometry().setCoordinates([]);
|
||||||
|
tracingFeature = null;
|
||||||
|
});
|
||||||
|
map.addInteraction(drawInteraction);
|
||||||
|
map.addInteraction(snapInteraction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeSelect.onchange = function() {
|
||||||
|
map.removeInteraction(drawInteraction);
|
||||||
|
map.removeInteraction(snapInteraction);
|
||||||
|
addInteraction();
|
||||||
|
};
|
||||||
|
addInteraction();
|
||||||
@@ -505,7 +505,7 @@ class Draw extends PointerInteraction {
|
|||||||
if (this.freehand_ &&
|
if (this.freehand_ &&
|
||||||
event.type === MapBrowserEventType.POINTERDRAG &&
|
event.type === MapBrowserEventType.POINTERDRAG &&
|
||||||
this.sketchFeature_ !== null) {
|
this.sketchFeature_ !== null) {
|
||||||
this.addToDrawing_(event);
|
this.addToDrawing_(event.coordinate);
|
||||||
pass = false;
|
pass = false;
|
||||||
} else if (this.freehand_ &&
|
} else if (this.freehand_ &&
|
||||||
event.type === MapBrowserEventType.POINTERDOWN) {
|
event.type === MapBrowserEventType.POINTERDOWN) {
|
||||||
@@ -580,7 +580,7 @@ class Draw extends PointerInteraction {
|
|||||||
this.finishDrawing();
|
this.finishDrawing();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.addToDrawing_(event);
|
this.addToDrawing_(event.coordinate);
|
||||||
}
|
}
|
||||||
pass = false;
|
pass = false;
|
||||||
} else if (this.freehand_) {
|
} else if (this.freehand_) {
|
||||||
@@ -764,13 +764,12 @@ class Draw extends PointerInteraction {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new coordinate to the drawing.
|
* Add a new coordinate to the drawing.
|
||||||
* @param {import("../MapBrowserEvent.js").default} event Event.
|
* @param {!PointCoordType} coordinate Coordinate
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
addToDrawing_(event) {
|
addToDrawing_(coordinate) {
|
||||||
const coordinate = event.coordinate;
|
|
||||||
const geometry = this.sketchFeature_.getGeometry();
|
const geometry = this.sketchFeature_.getGeometry();
|
||||||
const projection = event.map.getView().getProjection();
|
const projection = this.getMap().getView().getProjection();
|
||||||
let done;
|
let done;
|
||||||
let coordinates;
|
let coordinates;
|
||||||
if (this.mode_ === Mode.LINE_STRING) {
|
if (this.mode_ === Mode.LINE_STRING) {
|
||||||
@@ -903,9 +902,44 @@ class Draw extends PointerInteraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend an existing geometry by adding additional points. This only works
|
* Append coordinates to the end of the geometry that is currently being drawn.
|
||||||
* on features with `LineString` geometries, where the interaction will
|
* This can be used when drawing LineStrings or Polygons. Coordinates will
|
||||||
* extend lines by adding points to the end of the coordinates array.
|
* either be appended to the current LineString or the outer ring of the current
|
||||||
|
* Polygon.
|
||||||
|
* @param {!LineCoordType} coordinates Linear coordinates to be appended into
|
||||||
|
* the coordinate array.
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
appendCoordinates(coordinates) {
|
||||||
|
const mode = this.mode_;
|
||||||
|
let sketchCoords = [];
|
||||||
|
if (mode === Mode.LINE_STRING) {
|
||||||
|
sketchCoords = /** @type {LineCoordType} */ this.sketchCoords_;
|
||||||
|
} else if (mode === Mode.POLYGON) {
|
||||||
|
sketchCoords = this.sketchCoords_ && this.sketchCoords_.length ? /** @type {PolyCoordType} */ (this.sketchCoords_)[0] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove last coordinate from sketch drawing (this coordinate follows cursor position)
|
||||||
|
const ending = sketchCoords.pop();
|
||||||
|
|
||||||
|
// Append coordinate list
|
||||||
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
this.addToDrawing_(coordinates[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate last coordinate for sketch drawing
|
||||||
|
this.addToDrawing_(ending);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate draw mode by starting from an existing geometry which will
|
||||||
|
* receive new additional points. This only works on features with
|
||||||
|
* `LineString` geometries, where the interaction will extend lines by adding
|
||||||
|
* points to the end of the coordinates array.
|
||||||
|
* This will change the original feature, instead of drawing a copy.
|
||||||
|
*
|
||||||
|
* The function will dispatch a `drawstart` event.
|
||||||
|
*
|
||||||
* @param {!Feature<LineString>} feature Feature to be extended.
|
* @param {!Feature<LineString>} feature Feature to be extended.
|
||||||
* @api
|
* @api
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1399,4 +1399,129 @@ describe('ol.interaction.Draw', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('append coordinates when drawing a Polygon feature', function() {
|
||||||
|
let draw;
|
||||||
|
let coordinates;
|
||||||
|
let coordinates2;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
draw = new Draw({
|
||||||
|
source: source,
|
||||||
|
type: 'Polygon'
|
||||||
|
});
|
||||||
|
map.addInteraction(draw);
|
||||||
|
coordinates = [[0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4]];
|
||||||
|
coordinates2 = [[10, 10], [11, 11], [12, 10], [10, 13], [13, 12], [14, 14]];
|
||||||
|
});
|
||||||
|
|
||||||
|
function isClosed(polygon) {
|
||||||
|
const first = polygon.getFirstCoordinate();
|
||||||
|
const last = polygon.getLastCoordinate();
|
||||||
|
expect(first).to.eql(last);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('draws polygon with clicks, adds coordinates to drawing, finishing on first point', function() {
|
||||||
|
// first point
|
||||||
|
simulateEvent('pointermove', 10, 20);
|
||||||
|
simulateEvent('pointerdown', 10, 20);
|
||||||
|
simulateEvent('pointerup', 10, 20);
|
||||||
|
isClosed(draw.sketchFeature_.getGeometry());
|
||||||
|
|
||||||
|
// add coordinates
|
||||||
|
draw.appendCoordinates(coordinates);
|
||||||
|
|
||||||
|
// finish on first point
|
||||||
|
simulateEvent('pointermove', 10, 20);
|
||||||
|
simulateEvent('pointerdown', 10, 20);
|
||||||
|
simulateEvent('pointerup', 10, 20);
|
||||||
|
|
||||||
|
const features = source.getFeatures();
|
||||||
|
expect(features).to.have.length(1);
|
||||||
|
const geometry = features[0].getGeometry();
|
||||||
|
expect(geometry).to.be.a(Polygon);
|
||||||
|
|
||||||
|
expect(geometry.getCoordinates()).to.eql([
|
||||||
|
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [10, -20]]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds coordinates to empty drawing', function() {
|
||||||
|
// first point
|
||||||
|
simulateEvent('pointermove', 0, 0);
|
||||||
|
simulateEvent('pointerdown', 0, 0);
|
||||||
|
simulateEvent('pointerup', 0, 0);
|
||||||
|
draw.removeLastPoint();
|
||||||
|
draw.appendCoordinates(coordinates);
|
||||||
|
isClosed(draw.sketchFeature_.getGeometry());
|
||||||
|
|
||||||
|
// finish drawing
|
||||||
|
simulateEvent('pointerdown', 0, 0);
|
||||||
|
simulateEvent('pointerup', 0, 0);
|
||||||
|
|
||||||
|
const features = source.getFeatures();
|
||||||
|
expect(features).to.have.length(1);
|
||||||
|
const geometry = features[0].getGeometry();
|
||||||
|
expect(geometry).to.be.a(Polygon);
|
||||||
|
|
||||||
|
expect(geometry.getCoordinates()).to.eql([
|
||||||
|
[[0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [0, 0]]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps updating the sketch feature after appending coordinates', function() {
|
||||||
|
// first point
|
||||||
|
simulateEvent('pointermove', 10, 20);
|
||||||
|
simulateEvent('pointerdown', 10, 20);
|
||||||
|
simulateEvent('pointerup', 10, 20);
|
||||||
|
isClosed(draw.sketchFeature_.getGeometry());
|
||||||
|
|
||||||
|
// add coordinates
|
||||||
|
draw.appendCoordinates(coordinates);
|
||||||
|
|
||||||
|
// add another point
|
||||||
|
simulateEvent('pointermove', 30, 20);
|
||||||
|
simulateEvent('pointerdown', 30, 20);
|
||||||
|
simulateEvent('pointerup', 30, 20);
|
||||||
|
|
||||||
|
// sketchGeom should have a complete ring, with a double coordinate for cursor
|
||||||
|
const sketchGeom = draw.sketchFeature_.getGeometry();
|
||||||
|
expect(sketchGeom.getCoordinates()).to.eql([
|
||||||
|
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [30, -20], [30, -20], [10, -20]]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps updating the sketch feature after multiple appendiges', function() {
|
||||||
|
// first point
|
||||||
|
simulateEvent('pointermove', 10, 20);
|
||||||
|
simulateEvent('pointerdown', 10, 20);
|
||||||
|
simulateEvent('pointerup', 10, 20);
|
||||||
|
isClosed(draw.sketchFeature_.getGeometry());
|
||||||
|
|
||||||
|
// add coordinates
|
||||||
|
draw.appendCoordinates(coordinates);
|
||||||
|
|
||||||
|
// another point
|
||||||
|
simulateEvent('pointermove', 100, 100);
|
||||||
|
simulateEvent('pointerdown', 100, 100);
|
||||||
|
simulateEvent('pointerup', 100, 100);
|
||||||
|
|
||||||
|
// add another array of coordinates
|
||||||
|
draw.appendCoordinates(coordinates2);
|
||||||
|
|
||||||
|
// finish on first point
|
||||||
|
simulateEvent('pointermove', 10, 20);
|
||||||
|
simulateEvent('pointerdown', 10, 20);
|
||||||
|
simulateEvent('pointerup', 10, 20);
|
||||||
|
|
||||||
|
const features = source.getFeatures();
|
||||||
|
expect(features).to.have.length(1);
|
||||||
|
const geometry = features[0].getGeometry();
|
||||||
|
expect(geometry).to.be.a(Polygon);
|
||||||
|
|
||||||
|
expect(geometry.getCoordinates()).to.eql([
|
||||||
|
[[10, -20], [0, 0], [1, 1], [2, 0], [0, 3], [3, 2], [4, 4], [100, -100], [10, 10], [11, 11], [12, 10], [10, 13], [13, 12], [14, 14], [10, -20]]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user