Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add checks for (non-)simple polygons #634

Merged
merged 40 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0d05256
Add check for invalid shapes
lehecht Aug 3, 2023
a821c82
Show message for invalid shapes
lehecht Aug 3, 2023
92ff004
Rename method for more clarity
lehecht Aug 3, 2023
f65c261
Remove case that is not reachable
lehecht Aug 9, 2023
b5078d1
Add event listener only once
lehecht Aug 9, 2023
af7dfc3
Simplify event listener method
lehecht Aug 9, 2023
2fdf82a
Concatenate x,y coordinates before size check
lehecht Aug 9, 2023
50c15ae
Remove duplicated coordinates
lehecht Aug 10, 2023
e9ac4f6
Remove superfluous checks for rectangles, ellipses and lines
lehecht Sep 22, 2023
66f4040
Move magic wand methods to MagicWandInteraction.js
lehecht Oct 2, 2023
0940d4c
Add jsts library
lehecht Oct 2, 2023
b164600
Disallow self intersecting polygons
lehecht Oct 11, 2023
0c7a70d
Remove unused imports
lehecht Oct 12, 2023
1c2ba0b
Move polygon validation methods to own class
lehecht Oct 13, 2023
1894844
Rename event for more clarity
lehecht Oct 13, 2023
ca61495
Add polygon validation for video annotations
lehecht Oct 13, 2023
1457ae2
Fix bug with nested polygon parts
lehecht Oct 16, 2023
0316882
Format code
lehecht Oct 16, 2023
7781541
Use value instead of variable
lehecht Oct 17, 2023
43f564d
Remove superfluous magicwand polygon validation
lehecht Oct 17, 2023
0c8b012
Move polygon validation call to drawInteractions.vue
lehecht Oct 17, 2023
669e94c
Transform self-intersecting polygons to simple ones
lehecht Oct 27, 2023
34f3884
Fix missing/unused imports
lehecht Oct 27, 2023
a8ddfd1
Remove unused method
lehecht Oct 27, 2023
3f41193
Remove unused methods
lehecht Oct 27, 2023
66f2fe0
Make polygonValiator a static class
lehecht Oct 30, 2023
398a5ae
Remove redundant return statement
lehecht Nov 2, 2023
4abce21
Replace class export by methods export
lehecht Nov 2, 2023
4996b71
Apply changes from code review
mzur Nov 7, 2023
27a7055
Convert JSTS Monkey import to side-effect import
mzur Nov 7, 2023
86dfe03
Remove unnecessary if-else condition
lehecht Nov 8, 2023
99758f2
Fix bug with intersecting polygons after modification
lehecht Nov 10, 2023
bcefe6d
Rename method to shorten name
lehecht Nov 14, 2023
bdeb64c
Add comments
lehecht Nov 14, 2023
f1e7730
Add comments
lehecht Nov 16, 2023
86b5cea
Make code more readable
lehecht Nov 16, 2023
7a340c6
Add Polygon check after modifying video polygons
lehecht Nov 16, 2023
4fc7573
Edit comments
lehecht Nov 16, 2023
9db8277
Implement alternative handling of polygons with holes
mzur Nov 17, 2023
fabfa21
Merge pull request #700 from biigle/non-simple-polygon-bug-patch-1
mzur Nov 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@fortawesome/fontawesome-free": "^5.2.0",
"bootstrap-sass": "^3.3.7",
"echarts": "^5.3.2",
"jsts": "^2.11.0",
"magic-wand-tool": "^1.1.4",
"polymorph-js": "^0.2.4",
"uiv": "^1.2.4",
Expand Down
3 changes: 3 additions & 0 deletions resources/assets/js/annotations/annotatorContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,9 @@ export default {
dismissCrossOriginError() {
this.crossOriginError = false;
},
handleInvalidPolygon(){
Messages.danger(`Invalid shape. Polygon needs at least 3 non-overlapping vertices.`);
},
},
watch: {
async imageId(id) {
Expand Down
25 changes: 20 additions & 5 deletions resources/assets/js/annotations/components/annotationCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ import ZoomifySource from '@biigle/ol/source/Zoomify';
import ZoomLevel from './annotationCanvas/zoomLevel';
import ZoomToExtentControl from '@biigle/ol/control/ZoomToExtent';
import ZoomToNativeControl from '../ol/ZoomToNativeControl';
import {click as clickCondition} from '@biigle/ol/events/condition';
import {defaults as defaultInteractions} from '@biigle/ol/interaction'
import {getCenter} from '@biigle/ol/extent';
import {shiftKeyOnly as shiftKeyOnlyCondition} from '@biigle/ol/events/condition';
import {singleClick as singleClickCondition} from '@biigle/ol/events/condition';
import * as PolygonValidator from '../ol/PolygonValidator';
import { click as clickCondition } from '@biigle/ol/events/condition';
import { defaults as defaultInteractions } from '@biigle/ol/interaction'
import { getCenter } from '@biigle/ol/extent';
import { shiftKeyOnly as shiftKeyOnlyCondition } from '@biigle/ol/events/condition';
import { singleClick as singleClickCondition } from '@biigle/ol/events/condition';


/**
Expand Down Expand Up @@ -512,6 +513,20 @@ export default {
handleNewFeature(e) {
if (this.hasSelectedLabel) {
let geometry = e.feature.getGeometry();
if (geometry.getType() === 'Polygon') {
if (PolygonValidator.isInvalidPolygon(e.feature)) {
this.$emit('is-invalid-polygon');
this.annotationSource.once('change', () => {
mzur marked this conversation as resolved.
Show resolved Hide resolved
if (this.annotationSource.hasFeature(e.feature)) {
this.annotationSource.removeFeature(e.feature);
}
});
return;
}

PolygonValidator.makePolygonSimple(e.feature);
}

e.feature.set('color', this.selectedLabel.color);

// This callback is called when saving the annotation succeeded or
Expand Down
2 changes: 1 addition & 1 deletion resources/assets/js/annotations/ol/MagicWandInteraction.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class MagicWandInteraction extends PointerInteraction {
if (this.isShowingCross) {
this.sketchSource.removeFeature(this.sketchFeature);
} else {
this.dispatchEvent({type: 'drawend', feature: this.sketchFeature});
this.dispatchEvent({ type: 'drawend', feature: this.sketchFeature });
}

this.sketchFeature = null;
Expand Down
144 changes: 144 additions & 0 deletions resources/assets/js/annotations/ol/PolygonValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Point from '@biigle/ol/geom/Point';
import Polygon from '@biigle/ol/geom/Polygon';
import MultiPolygon from '@biigle/ol/geom/MultiPolygon';
import MultiPoint from '@biigle/ol/geom/MultiPoint';
import LinearRing from '@biigle/ol/geom/LinearRing';
import LineString from '@biigle/ol/geom/LineString';
import MultiLineString from '@biigle/ol/geom/MultiLineString';
import JstsPolygon from 'jsts/org/locationtech/jts/geom/Polygon';
import JstsLinearRing from 'jsts/org/locationtech/jts/geom/LinearRing';
import OL3Parser from 'jsts/org/locationtech/jts/io/OL3Parser';
import Polygonizer from 'jsts/org/locationtech/jts/operation/polygonize/Polygonizer';
import Monkey from 'jsts/org/locationtech/jts/monkey'; // Needed for isValid(), normalize()


/**
* Checks if polygon consists of at least 3 unique points
*
* @param feature containing the polygon
* @returns True if coordinates contains at least 3 unique points, otherwise false
* **/
export function isInvalidPolygon(feature) {
let polygon = feature.getGeometry();
let points = polygon.getCoordinates()[0];
return (new Set(points.map(xy => String([xy])))).size < 3;

}

/**
* Makes non-simple polygon simple
*
* @param feature feature containing the (non-simple) polygon
* **/
export function makePolygonSimple(feature) {
// Check if polygon is self-intersecting
const parser = new OL3Parser();
parser.inject(
Point,
LineString,
LinearRing,
Polygon,
MultiPoint,
MultiLineString,
MultiPolygon
);

// Translate ol geometry into jsts geometry
let jstsPolygon = parser.read(feature.getGeometry());

if (jstsPolygon.isSimple()) {
return feature;
}

// Divide non-simple polygon at cross points into several smaller polygons
// and translate back to ol geometry
let polygons = jstsValidate(jstsPolygon)['array'].map(p => parser.write(p));
mzur marked this conversation as resolved.
Show resolved Hide resolved

if (polygons.length > 1) {
// Select biggest part
let greatestPolygon = getGreatestPolygon(polygons);
// Only change coordinates because object references are in use
feature.getGeometry().setCoordinates(greatestPolygon.getCoordinates());
}
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
*
*
* Get / create a valid version of the geometry given. If the geometry is a polygon or multi polygon, self intersections /
* inconsistencies are fixed. Otherwise the geometry is returned.
*
* @param geom
* @return a geometry
*/

function jstsValidate(geom) {
if (geom instanceof JstsPolygon) {
if (geom.isValid()) {
geom.normalize(); // validate does not pick up rings in the wrong order - this will fix that
return geom; // If the polygon is valid just return it
}
var polygonizer = new Polygonizer();
jstsAddPolygon(geom, polygonizer);
return polygonizer.getPolygons();
} else {
return geom;
mzur marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
*
* Add all line strings from the polygon given to the polygonizer given
*
* @param polygon polygon from which to extract line strings
* @param polygonizer polygonizer
*/
function jstsAddPolygon(polygon, polygonizer) {
jstsAddLineString(polygon.getExteriorRing(), polygonizer);

for (var n = polygon.getNumInteriorRing(); n > 0; n--) {
jstsAddLineString(polygon.getInteriorRingN(n), polygonizer);
}
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
* Add the linestring given to the polygonizer
*
* @param linestring line string
* @param polygonizer polygonizer
*/
function jstsAddLineString(lineString, polygonizer) {

if (lineString instanceof JstsLinearRing) {
// LinearRings are treated differently to line strings : we need a LineString NOT a LinearRing
lineString = lineString.getFactory().createLineString(lineString.getCoordinateSequence());
}

// unioning the linestring with the point makes any self intersections explicit.
var point = lineString.getFactory().createPoint(lineString.getCoordinateN(0));
var toAdd = lineString.union(point); //geometry

//Add result to polygonizer
polygonizer.add(toAdd);
}

/**
* Determine polygon with largest area
*
* @param polygonList List of polygons
* @returns Polygon
* **/
function getGreatestPolygon(polygonList) {
let areas = polygonList.map(p => p.getArea());
let idx = areas.indexOf(Math.max(...areas));
return polygonList[idx];
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Keyboard from '../../../core/keyboard';
import Styles from '../../../annotations/stores/styles';
import VectorLayer from '@biigle/ol/layer/Vector';
import VectorSource from '@biigle/ol/source/Vector';

import * as PolygonValidator from "../../../annotations/ol/PolygonValidator";
/**
* Mixin for the videoScreen component that contains logic for the draw interactions.
*
Expand Down Expand Up @@ -163,6 +163,19 @@ export default {
this.$emit('pending-annotation', null);
},
extendPendingAnnotation(e) {
// Check polygons
if (e.feature.getGeometry().getType() === 'Polygon') {
if (PolygonValidator.isInvalidPolygon(e.feature)) {
// Disallow polygons with less than three non-overlapping points
this.$emit('is-invalid-polygon')
this.removeFeature(e.feature)
mzur marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// If polygon is self-intersecting, create simple polygon
PolygonValidator.makePolygonSimple(e.feature);
}

let lastFrame = this.pendingAnnotation.frames[this.pendingAnnotation.frames.length - 1];

if (lastFrame === undefined || lastFrame < this.video.currentTime) {
Expand Down
4 changes: 4 additions & 0 deletions resources/assets/js/videos/videoContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export default {
shape_id: this.shapes[pendingAnnotation.shape],
label_id: this.selectedLabel ? this.selectedLabel.id : 0,
});

delete annotation.shape;

return VideoAnnotationApi.save({id: this.videoId}, annotation)
Expand Down Expand Up @@ -612,6 +613,9 @@ export default {
annotation.failTracking();
}
},
handleInvalidPolygon(){
Messages.danger(`Invalid shape. Polygon needs at least 3 non-overlapping vertices.`);
}
},
watch: {
'settings.playbackRate'(rate) {
Expand Down
1 change: 1 addition & 0 deletions resources/views/annotations/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
v-on:delete="handleDeleteAnnotations"
v-on:measuring="fetchImagesArea"
v-on:requires-selected-label="handleRequiresSelectedLabel"
v-on:is-invalid-polygon="handleInvalidPolygon"
ref="canvas"
inline-template>
@include('annotations.show.annotationCanvas')
Expand Down
1 change: 1 addition & 0 deletions resources/views/videos/show/content.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
v-on:attaching-active="handleAttachingLabelActive"
v-on:swapping-active="handleSwappingLabelActive"
v-on:seek="seek"
v-on:is-invalid-polygon="handleInvalidPolygon"
></video-screen>
<video-timeline
ref="videoTimeline"
Expand Down
Loading