Skip to content

Commit

Permalink
Merge branch 'feature/rounder'
Browse files Browse the repository at this point in the history
  • Loading branch information
mfogel committed Feb 25, 2019
2 parents 7c48f52 + 0d6c2d7 commit 59b6713
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 364 deletions.
26 changes: 13 additions & 13 deletions src/bbox.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cmp, touch } from './flp'
import { touch } from './flp'

/**
* A bounding box has the format:
Expand All @@ -9,10 +9,10 @@ import { cmp, touch } from './flp'

export const isInBbox = (bbox, point) => {
return (
cmp(bbox.ll.x, point.x) <= 0 &&
cmp(point.x, bbox.ur.x) <= 0 &&
cmp(bbox.ll.y, point.y) <= 0 &&
cmp(point.y, bbox.ur.y) <= 0
(bbox.ll.x <= point.x) &&
(point.x <= bbox.ur.x) &&
(bbox.ll.y <= point.y) &&
(point.y <= bbox.ur.y)
)
}

Expand All @@ -22,10 +22,10 @@ export const isInBbox = (bbox, point) => {
* - it 'touches' one of the sides (another greedy comparison) */
export const touchesBbox = (bbox, point) => {
return (
(cmp(bbox.ll.x, point.x) <= 0 || touch(bbox.ll.x, point.x)) &&
(cmp(point.x, bbox.ur.x) <= 0 || touch(point.x, bbox.ur.x)) &&
(cmp(bbox.ll.y, point.y) <= 0 || touch(bbox.ll.y, point.y)) &&
(cmp(point.y, bbox.ur.y) <= 0 || touch(point.y, bbox.ur.y))
((bbox.ll.x <= point.x) || touch(bbox.ll.x, point.x)) &&
((point.x <= bbox.ur.x) || touch(point.x, bbox.ur.x)) &&
((bbox.ll.y <= point.y) || touch(bbox.ll.y, point.y)) &&
((point.y <= bbox.ur.y) || touch(point.y, bbox.ur.y))
)
}

Expand All @@ -35,10 +35,10 @@ export const touchesBbox = (bbox, point) => {
export const getBboxOverlap = (b1, b2) => {
// check if the bboxes overlap at all
if (
cmp(b2.ur.x, b1.ll.x) < 0 ||
cmp(b1.ur.x, b2.ll.x) < 0 ||
cmp(b2.ur.y, b1.ll.y) < 0 ||
cmp(b1.ur.y, b2.ll.y) < 0
b2.ur.x < b1.ll.x ||
b1.ur.x < b2.ll.x ||
b2.ur.y < b1.ll.y ||
b1.ur.y < b2.ll.y
) return null

// find the middle two X values
Expand Down
14 changes: 8 additions & 6 deletions src/clean-input.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cmpPoints } from './flp'
import { compareVectorAngles } from './vector'
import rounder from './rounder'

/* Given input geometry as a standard array-of-arrays geojson-style
* geometry, return one that uses objects as points, for better perf */
Expand Down Expand Up @@ -30,7 +30,7 @@ export const pointsAsObjects = geom => {
'Only 2-dimensional polygons supported.'
)
}
output[i][j].push({ x: geom[i][j][k][0], y: geom[i][j][k][1] })
output[i][j].push(rounder.round(geom[i][j][k][0], geom[i][j][k][1]))
}
} else { // polygon
if (geom[i][j].length < 2) {
Expand All @@ -42,7 +42,7 @@ export const pointsAsObjects = geom => {
'Only 2-dimensional polygons supported.'
)
}
output[i].push({ x: geom[i][j][0], y: geom[i][j][1] })
output[i].push(rounder.round(geom[i][j][0], geom[i][j][1]))
}
}
}
Expand Down Expand Up @@ -116,11 +116,13 @@ export const cleanMultiPoly = multipoly => {
* WARN: input modified directly */
export const cleanRing = ring => {
if (ring.length === 0) return
if (cmpPoints(ring[0], ring[ring.length - 1]) === 0) ring.pop()
const firstPt = ring[0]
const lastPt = ring[ring.length - 1]
if (firstPt.x === lastPt.x && firstPt.y === lastPt.y) ring.pop()

const isPointUncessary = (prevPt, pt, nextPt) =>
cmpPoints(prevPt, pt) === 0 ||
cmpPoints(pt, nextPt) === 0 ||
(prevPt.x === pt.x && prevPt.y === pt.y) ||
(nextPt.x === pt.x && nextPt.y === pt.y) ||
compareVectorAngles(pt, prevPt, nextPt) === 0

let i = 0
Expand Down
57 changes: 14 additions & 43 deletions src/flp.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,6 @@ export const cmp = (a, b) => {
return a < b ? -1 : 1
}

/* FLP point comparator, favors point encountered first by sweep line */
export const cmpPoints = (aPt, bPt) => {
if (aPt === bPt) return 0

// fist compare X, then compare Y
let a = aPt.x
let b = bPt.x

// inlined version of cmp() for performance boost
if (
a <= -epsilon ||
epsilon <= a ||
b <= -epsilon ||
epsilon <= b
) {
const diff = a - b
if (diff * diff >= EPSILON_SQ * a * b) {
return a < b ? -1 : 1
}
}

a = aPt.y
b = bPt.y

// inlined version of cmp() for performance boost
if (
a <= -epsilon ||
epsilon <= a ||
b <= -epsilon ||
epsilon <= b
) {
const diff = a - b
if (diff * diff >= EPSILON_SQ * a * b) {
return a < b ? -1 : 1
}
}

// they're the same
return 0
}

/* Greedy comparison. Two numbers are defined to touch
* if their midpoint is indistinguishable from either. */
export const touch = (a, b) => {
Expand All @@ -80,6 +39,18 @@ export const touch = (a, b) => {
/* Greedy comparison. Two points are defined to touch
* if their midpoint is indistinguishable from either. */
export const touchPoints = (aPt, bPt) => {
const mPt = { x: (aPt.x + bPt.x) / 2, y: (aPt.y + bPt.y) / 2 }
return cmpPoints(mPt, aPt) === 0 || cmpPoints(mPt, bPt) === 0
// call directly to (skip touch()) cmp() for performance boost
const mx = (aPt.x + bPt.x) / 2
const aXMiss = cmp(mx, aPt.x) !== 0
if (aXMiss && cmp(mx, bPt.x) !== 0) return false

const my = (aPt.y + bPt.y) / 2
const aYMiss = cmp(my, aPt.y) !== 0
if (aYMiss && cmp(my, bPt.y) !== 0) return false

// we have touching on both x & y, we have to make sure it's
// not just on opposite points thou
if (aYMiss && aYMiss) return true
if (!aYMiss && !aYMiss) return true
return false
}
4 changes: 3 additions & 1 deletion src/operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import SplayTree from 'splaytree'
import * as cleanInput from './clean-input'
import * as geomIn from './geom-in'
import * as geomOut from './geom-out'
import rounder from './rounder'
import SweepEvent from './sweep-event'
import SweepLine from './sweep-line'

export class Operation {
run (type, geom, moreGeoms) {
operation.type = type
rounder.reset()

/* Make a copy of the input geometry with points as objects, for perf */
/* Make a copy of the input geometry with rounded points as objects */
const geoms = [cleanInput.pointsAsObjects(geom)]
for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {
geoms.push(cleanInput.pointsAsObjects(moreGeoms[i]))
Expand Down
71 changes: 71 additions & 0 deletions src/rounder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { cmp } from './flp'
import SplayTree from 'splaytree'

/**
* This class rounds incoming values sufficiently so that
* floating points problems are, for the most part, avoided.
*
* Incoming points are have their x & y values tested against
* all previously seen x & y values. If either is 'too close'
* to a previously seen value, it's value is 'snapped' to the
* previously seen value.
*
* All points should be rounded by this class before being
* stored in any data structures in the rest of this algorithm.
*/

class PtRounder {
constructor () {
this.reset()
}

reset () {
this.xRounder = new CoordRounder()
this.yRounder = new CoordRounder()
}

round (x, y) {
return {
x: this.xRounder.round(x),
y: this.yRounder.round(y),
}
}
}

class CoordRounder {
constructor () {
this.tree = new SplayTree()
// preseed with 0 so we don't end up with values < Number.EPSILON
this.round(0)
}

// Note: this can rounds input values backwards or forwards.
// You might ask, why not restrict this to just rounding
// forwards? Wouldn't that allow left endpoints to always
// remain left endpoints during splitting (never change to
// right). No - it wouldn't, because we snap intersections
// to endpoints (to establish independence from the segment
// angle for t-intersections).
round (coord) {
const node = this.tree.add(coord)

const prevNode = this.tree.prev(node)
if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {
this.tree.remove(coord)
return prevNode.key
}

const nextNode = this.tree.next(node)
if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {
this.tree.remove(coord)
return nextNode.key
}

return coord
}
}

// singleton available by import
const rounder = new PtRounder()

export default rounder
Loading

0 comments on commit 59b6713

Please sign in to comment.