From 914e9c0bc7d9412aad1a981cc30886a953ddf4f8 Mon Sep 17 00:00:00 2001 From: Tilmann Date: Sun, 30 Jun 2024 14:27:16 +0200 Subject: [PATCH] squash-15 --- CHANGELOG.md | 24 +- README.md | 5 +- pom.xml | 4 +- src/main/java/org/tinspin/index/Index.java | 1 + .../java/org/tinspin/index/IndexConfig.java | 16 +- src/main/java/org/tinspin/index/Stats.java | 5 +- .../org/tinspin/index/array/RectArray.java | 3 + .../org/tinspin/index/qthypercube/QNode.java | 1 + .../org/tinspin/index/qthypercube/QRNode.java | 1 + .../tinspin/index/qthypercube/QuadTreeKD.java | 2 +- .../index/qthypercube2/QIteratorKnn.java | 4 +- .../org/tinspin/index/qthypercube2/QNode.java | 111 +- .../index/qthypercube2/QuadTreeKD2.java | 22 +- .../java/org/tinspin/index/qtplain/QNode.java | 1 + .../org/tinspin/index/qtplain/QRNode.java | 1 + .../org/tinspin/index/util/MathTools.java | 74 + .../index/kdtree/KDTreeConfigTest.java | 181 ++ .../org/tinspin/index/test/BoxMapTest.java | 61 + .../tinspin/index/test/BoxMultimapTest.java | 61 + .../org/tinspin/index/test/Issue0037Test.java | 145 ++ .../org/tinspin/index/test/PointMapTest.java | 63 + .../tinspin/index/test/PointMultimapTest.java | 109 +- .../java/org/tinspin/util/MathToolsTest.java | 91 + .../org/tinspin/{ => util}/MinHeapTest.java | 3 +- .../tinspin/{ => util}/MinMaxHeapTest.java | 3 +- src/test/resources/issue0037.txt | 1491 +++++++++++++++++ 26 files changed, 2424 insertions(+), 59 deletions(-) create mode 100644 src/main/java/org/tinspin/index/util/MathTools.java create mode 100644 src/test/java/org/tinspin/index/kdtree/KDTreeConfigTest.java create mode 100644 src/test/java/org/tinspin/index/test/Issue0037Test.java create mode 100644 src/test/java/org/tinspin/util/MathToolsTest.java rename src/test/java/org/tinspin/{ => util}/MinHeapTest.java (99%) rename src/test/java/org/tinspin/{ => util}/MinMaxHeapTest.java (99%) create mode 100644 src/test/resources/issue0037.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e42a6..9372f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Nothing yet +## [2.1.4 - Unreleased] + +- Fixed tree corruption after remove() in QT2. [#40](https://github.com/tzaeschke/tinspin-indexes/issues/40) + - Fixed tree consistency (single-entry leaf after remove) + - Fixed tree consistency (nValues) -> verify + - Fixed bug in qt2.contains() +- Fixed QT2 inconsistency after root resizing after insert(). [#42](https://github.com/tzaeschke/tinspin-indexes/issues/42) + Essentially, we enforce all radii and the center of the root to be a power of two. + This should immensely reduce and problems with precision errors. + +## [2.1.3] - 2023-11-19 + +### Fixed +- Fixed QuadtreeKD2 kNN finding previously deleted entries [#37](https://github.com/tzaeschke/tinspin-indexes/issue/37) + +## [2.1.2] - 2023-10-31 + +### Fixed +- Fixed `create()` method for `IndexConfig` not being static [#33](https://github.com/tzaeschke/tinspin-indexes/issue/35) + ## [2.1.1] - 2023-10-14 ### Fixed @@ -146,7 +166,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Added CHANGELOG - [2015-10-11] - Pushed to v1.3.3-SNAPSHOT - [2015-10-11] -[Unreleased]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.1.1...HEAD +[Unreleased]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.1.3...HEAD +[2.1.3]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.1.2...tinspin-indexes-2.1.3 +[2.1.2]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.1.1...tinspin-indexes-2.1.2 [2.1.1]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.1.0...tinspin-indexes-2.1.1 [2.1.0]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.0.1...tinspin-indexes-2.1.0 [2.0.1]: https://github.com/tzaeschke/tinspin-indexes/compare/tinspin-indexes-2.0.0...tinspin-indexes-2.0.1 diff --git a/README.md b/README.md index cd9a6c3..ae68e89 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ TinSpin indexes are also available via maven: org.tinspin tinspin-indexes - 2.1.0 + 2.1.3 ``` @@ -46,6 +46,9 @@ Note: ## Changelog See [CHANGELOG](CHANGELOG.md) for details. + - 2.1.3 Fixed `remove()` not cleaning up properly and kNN returning deleted entries. + - 2.1.2 Made `create()` method `static` for `IndexConfig`. + - 2.1.1 Added `create()` method for `IndexConfig`. - 2.1.0 **API:** Added factory methods for all indexes. - 2.0.1 Fixed issue with dependencies in generated pom. - **2.0.0** **Major API rewrite.** diff --git a/pom.xml b/pom.xml index 4159b5e..fa22f06 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ org.tinspin tinspin-indexes jar - 2.1.1 + 2.1.4-SNAPSHOT UTF-8 @@ -24,7 +24,7 @@ https://github.com/tzaeschke/tinspin-indexes scm:git:git@github.com:tzaeschke/tinspin-indexes.git scm:git:git@github.com:tzaeschke/tinspin-indexes.git - tinspin-indexes-2.1.1 + HEAD diff --git a/src/main/java/org/tinspin/index/Index.java b/src/main/java/org/tinspin/index/Index.java index 01cba75..5bed4d7 100644 --- a/src/main/java/org/tinspin/index/Index.java +++ b/src/main/java/org/tinspin/index/Index.java @@ -41,6 +41,7 @@ public interface Index { /** * @return Collect and return some index statistics. Note that indexes are not required * to fill all fields. Also, individual indexes may use subclasses with additional fields. + * Many indexes also perform consistency checks while gathering stats. */ Stats getStats(); diff --git a/src/main/java/org/tinspin/index/IndexConfig.java b/src/main/java/org/tinspin/index/IndexConfig.java index e5d9303..ce9970e 100644 --- a/src/main/java/org/tinspin/index/IndexConfig.java +++ b/src/main/java/org/tinspin/index/IndexConfig.java @@ -25,7 +25,7 @@ protected IndexConfig(int dimensions) { this.dimensions = dimensions; } - public IndexConfig create(int dimensions) { + public static IndexConfig create(int dimensions) { return new IndexConfig(dimensions); } @@ -33,22 +33,26 @@ public IndexConfig create(int dimensions) { /** * Number of dimensions. * @param dimensions Number of dimensions of keys. + * @return this */ - public void setDimensions(int dimensions) { + public IndexConfig setDimensions(int dimensions) { this.dimensions = dimensions; + return this; } /** * @param defensiveKeyCopy - * Defensive keys copying. If `false`, the kd-tree will store the passed in + * Defensive keys copying. If 'false', the kd-tree will store the passed in * double[] keys internally (this reduces required memory). - * If `true`, the keys are copied in order to avoid accidental modification. - * The latter obviously requires more memory. + * If 'true', the keys are copied in order to avoid accidental modification. + * The latter obviously requires more memory. Default is 'true'. *

* This setting works only for kd-trees. + * @return this */ - public void setDefensiveKeyCopy(boolean defensiveKeyCopy) { + public IndexConfig setDefensiveKeyCopy(boolean defensiveKeyCopy) { this.defensiveKeyCopy = defensiveKeyCopy; + return this; } diff --git a/src/main/java/org/tinspin/index/Stats.java b/src/main/java/org/tinspin/index/Stats.java index 5270d51..312739b 100644 --- a/src/main/java/org/tinspin/index/Stats.java +++ b/src/main/java/org/tinspin/index/Stats.java @@ -27,6 +27,7 @@ public class Stats { public int minLevel = Integer.MAX_VALUE; public int maxLevel = -1; public int maxDepth = 0; + public int maxValuesInNode = 0; public double sumLevel; public int maxNodeSize = -1; public int nLeaf; @@ -49,7 +50,9 @@ public String toString() { ";nNodes=" + nNodes + ";nLeaf=" + nLeaf + ";nInner=" + nInner + - ";minLevel=" + minLevel + + ";maxDepth=" + maxDepth + + ";maxValues=" + maxValuesInNode + + ";minLevel=" + minLevel + ";maxLevel=" + maxLevel + ";avgLevel=" + (sumLevel/nEntries) + ";maxNodeSize=" + maxNodeSize; diff --git a/src/main/java/org/tinspin/index/array/RectArray.java b/src/main/java/org/tinspin/index/array/RectArray.java index 1a511ab..7c62fa5 100644 --- a/src/main/java/org/tinspin/index/array/RectArray.java +++ b/src/main/java/org/tinspin/index/array/RectArray.java @@ -238,6 +238,9 @@ private ArrayList> knnQuery(double[] center, int k) { for (int i = 0; i < phc.length/2; i++) { double[] min = phc[i*2]; double[] max = phc[i*2+1]; + if (min == null) { + continue; // Entry has been removed + } double dist = distREdge(center, min, max); if (ret.size() < k) { ret.add(new BoxEntryKnn<>(min, max, values[i].value(), dist)); diff --git a/src/main/java/org/tinspin/index/qthypercube/QNode.java b/src/main/java/org/tinspin/index/qthypercube/QNode.java index b2e8d39..393e3ad 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube/QNode.java @@ -303,6 +303,7 @@ void checkNode(QStats s, QNode parent, int depth) { s.nLeaf++; s.nEntries += values.size(); s.histoValues[values.size()]++; + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { PointEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.point(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qthypercube/QRNode.java b/src/main/java/org/tinspin/index/qthypercube/QRNode.java index 9a297a6..d07cda3 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QRNode.java +++ b/src/main/java/org/tinspin/index/qthypercube/QRNode.java @@ -350,6 +350,7 @@ void checkNode(QStats s, QRNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { BoxEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.min(), e.max(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java b/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java index 575ead4..cbe1993 100644 --- a/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java +++ b/src/main/java/org/tinspin/index/qthypercube/QuadTreeKD.java @@ -407,7 +407,7 @@ public QStats getStats() { * Statistics container class. */ public static class QStats extends Stats { - final int[] histoValues = new int[100]; + final int[] histoValues = new int[1000]; final int[] histoSubs; public QStats(int dims) { diff --git a/src/main/java/org/tinspin/index/qthypercube2/QIteratorKnn.java b/src/main/java/org/tinspin/index/qthypercube2/QIteratorKnn.java index fa321e8..89c2107 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QIteratorKnn.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QIteratorKnn.java @@ -112,8 +112,8 @@ private void findNextElement() { } if (node.isLeaf()) { - for (PointEntry entry : node.getValues()) { - processEntry(entry); + for (int i = 0; i < node.getValueCount(); i++) { + processEntry(node.getValues()[i]); } } else { for (Object o : node.getEntries()) { diff --git a/src/main/java/org/tinspin/index/qthypercube2/QNode.java b/src/main/java/org/tinspin/index/qthypercube2/QNode.java index 4817749..53150ea 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QNode.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QNode.java @@ -26,7 +26,14 @@ /** * Node class for the quadtree. - * + *

+ * A node can be in one of two modes: "directory node" and "leaf node". + * Directory nodes have an 2^dim array of "subs", where ich entry can be one of: a point, a subnode, or null. + * Leaf nodes have an array of "values" which are all points. + *

+ * A new subnode is inserted in "subs" if a slot in "subs" already contains a point. + * + * * @author ztilmann * * @param Value type. @@ -127,6 +134,7 @@ private void removeValue(int pos) { if (pos < --nValues) { System.arraycopy(getValues(), pos+1, getValues(), pos, nValues-pos); } + getValues()[nValues] = null; } else { nValues--; subs[pos] = null; @@ -202,7 +210,11 @@ PointEntry remove(QNode parent, double[] key, int maxNodeSize, Predicate

)o).remove(this, key, maxNodeSize, pred); + PointEntry removed = ((QNode)o).remove(this, key, maxNodeSize, pred); + if (removed != null) { + checkAndMergeLeafNodesInParent(parent, maxNodeSize); + } + return removed; } else if (o instanceof PointEntry) { PointEntry e = (PointEntry) o; if (removeSub(parent, key, pos, e, maxNodeSize, pred)) { @@ -227,9 +239,7 @@ private boolean removeSub( removeValue(pos); //TODO provide threshold for re-insert // i.e. do not always merge. - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); - } + checkAndMergeLeafNodesInParent(parent, maxNodeSize); return true; } return false; @@ -272,13 +282,11 @@ PointEntry update(QNode parent, double[] keyOld, double[] keyNew, int maxN requiresReinsert[0] = false; } else { requiresReinsert[0] = true; - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); - } + checkAndMergeLeafNodesInParent(parent, maxNodeSize); } return qe; } - throw new IllegalStateException(); + return null; } for (int i = 0; i < nValues; i++) { @@ -303,16 +311,37 @@ private void updateSub(double[] keyNew, PointEntry e, QNode parent, int ma requiresReinsert[0] = true; //TODO provide threshold for re-insert //i.e. do not always merge. - if (parent != null) { - parent.checkAndMergeLeafNodes(maxNodeSize); + checkAndMergeLeafNodesInParent(parent, maxNodeSize); + } + } + + private boolean checkMergeSingleLeaf(QNode parent) { + // Merge single value into parent if possible + if (!isLeaf() || parent == null || nValues > 1) { + return false; + } + for (int i = 0; i < parent.subs.length; i++) { + if (parent.subs[i] == this) { + parent.subs[i] = values[0]; + parent.nValues++; + nValues = 0; + return true; } } + throw new IllegalStateException(); + } + + @SuppressWarnings("unchecked") + private void checkAndMergeLeafNodesInParent(QNode parent, int maxNodeSize) { + if (!checkMergeSingleLeaf(parent) && parent != null) { + parent.checkAndMergeLeafNodes(maxNodeSize); + } } @SuppressWarnings("unchecked") private void checkAndMergeLeafNodes(int maxNodeSize) { //check: We start with including all local values: nValues - int nTotal = nValues; + int nTotal = 0; for (int i = 0; i < subs.length; i++) { Object e = subs[i]; if (e instanceof QNode) { @@ -323,14 +352,16 @@ private void checkAndMergeLeafNodes(int maxNodeSize) { return; } nTotal += sub.getValueCount(); - if (nTotal > maxNodeSize) { - //too many children - return; - } + } else if (e instanceof PointEntry) { + nTotal++; } } - - //okay, let's merge + if (nTotal > maxNodeSize) { + //too many children + return; + } + + //okay, let's merge. values = new PointEntry[nTotal]; nValues = 0; for (int i = 0; i < subs.length; i++) { @@ -365,7 +396,7 @@ PointEntry getExact(double[] key, Predicate> pred) { return ((QNode)sub).getExact(key, pred); } else if (sub != null) { PointEntry e = (PointEntry) sub; - if (QUtil.isPointEqual(e.point(), key)) { + if (QUtil.isPointEqual(e.point(), key) && pred.test(e)) { return e; } } @@ -401,18 +432,23 @@ void checkNode(QStats s, QNode parent, int depth) { if (parent != null) { if (!QUtil.isNodeEnclosed(center, radius, parent.center, parent.radius*QUtil.EPS_MUL)) { + System.out.println("Outer: " + parent.radius + " " + Arrays.toString(parent.center)); + System.out.println("Child(" + depth + "): " + radius + " " + Arrays.toString(center)); for (int d = 0; d < center.length; d++) { -// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || -// (centerOuter[d]-radiusOuter) / (centerEnclosed[d]-radiusEnclosed) > 1.0000001) { -// return false; -// } - System.out.println("Outer: " + parent.radius + " " + - Arrays.toString(parent.center)); - System.out.println("Child: " + radius + " " + Arrays.toString(center)); - System.out.println((parent.center[d]+parent.radius) + " vs " + (center[d]+radius)); - System.out.println("r=" + (parent.center[d]+parent.radius) / (center[d]+radius)); - System.out.println((parent.center[d]-parent.radius) + " vs " + (center[d]-radius)); - System.out.println("r=" + (parent.center[d]-parent.radius) / (center[d]-radius)); + double parentMax = parent.center[d] + parent.radius; + double childMax = center[d] + radius; + double parentMin = parent.center[d] - parent.radius; + double childMin = center[d] - radius; + if (parentMax < childMax) { + System.out.println("DIM: " + d); + System.out.println("max: " + parentMax + " vs " + childMax); + System.out.println(" r: " + parentMax / childMax); + } + if (parentMin > childMin) { + System.out.println("DIM: " + d); + System.out.println("min: " + parentMin + " vs " + childMin); + System.out.println(" r: " + parentMin / childMin); + } } throw new IllegalStateException(); } @@ -421,6 +457,7 @@ void checkNode(QStats s, QNode parent, int depth) { s.nLeaf++; s.nEntries += nValues; s.histoValues[nValues]++; + s.maxValuesInNode = Math.max(s.maxValuesInNode, nValues); for (int i = 0; i < nValues; i++) { PointEntry e = values[i]; checkEntry(e); @@ -428,12 +465,17 @@ void checkNode(QStats s, QNode parent, int depth) { if (subs != null) { throw new IllegalStateException(); } + if (nValues < 2 && parent != null) { + // Leaf nodes (except the root) must contain at least two values. + throw new IllegalStateException(); + } } else { s.nInner++; if (subs.length != 1L< parent, int depth) { ((QNode)n).checkNode(s, this, depth+1); } else if (n != null) { s.nEntries++; + nFoundValues++; checkEntry(n); } } + if (nValues != nFoundValues) { + throw new IllegalStateException(); + } + s.histoValues[nFoundValues]++; s.histo(nSubs); } } @@ -456,10 +503,6 @@ private void checkEntry(Object o) { System.out.println("Node: " + radius + " " + Arrays.toString(center)); System.out.println("Child: " + Arrays.toString(e.point())); for (int d = 0; d < center.length; d++) { -// if ((centerOuter[d]+radiusOuter) / (centerEnclosed[d]+radiusEnclosed) < 0.9999999 || -// (centerOuter[d]-radiusOuter) / (centerEnclosed[d]-radiusEnclosed) > 1.0000001) { -// return false; -// } System.out.println("min/max for " + d); System.out.println("min: " + (center[d]-radius) + " vs " + (e.point()[d])); System.out.println("r=" + (center[d]-radius) / (e.point()[d])); diff --git a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java index 018ae45..01d50d4 100644 --- a/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java +++ b/src/main/java/org/tinspin/index/qthypercube2/QuadTreeKD2.java @@ -21,6 +21,7 @@ import java.util.function.Predicate; import org.tinspin.index.*; +import org.tinspin.index.util.MathTools; import org.tinspin.index.util.StringBuilderLn; /** @@ -42,7 +43,7 @@ * With version 1, there were many nodes with just one data entry because the parent directory * node could not hold date entries. * With version two a directory node (which contains a hypercube array) will only - * create a subnode if a quadrant has to mhold more than one data entry. + * create a subnode if a quadrant has to hold more than one data entry. * * * @author ztilmann @@ -108,7 +109,7 @@ public void insert(double[] key, T value) { PointEntry e = new PointEntry<>(key, value); if (root == null) { // We calculate a better radius when adding a second point. - root = new QNode<>(key.clone(), INITIAL_RADIUS); + root = new QNode<>(MathTools.floorPowerOfTwoCopy(key), INITIAL_RADIUS); } if (root.getRadius() == INITIAL_RADIUS) { adjustRootSize(key); @@ -127,16 +128,20 @@ private void adjustRootSize(double[] key) { return; } if (root.getRadius() == INITIAL_RADIUS) { - double dist = PointDistance.L2.dist(key, root.getCenter()); - if (dist > 0) { - root.adjustRadius(2 * dist); + double dMax = MathTools.maxDelta(key, root.getCenter()); + for (int i = 0; i < root.getValueCount(); i++) { + dMax = Math.max(dMax, MathTools.maxDelta(root.getValues()[i].point(), root.getCenter())); + } + double radius = MathTools.ceilPowerOfTwo(dMax + QUtil.EPS_MUL); + if (radius > 0) { + root.adjustRadius(radius); } else if (root.getValueCount() >= maxNodeSize - 1) { - // we just set an arbitrary radius here + // all entries have (approximately?) the same coordinates. We just set an arbitrary radius here. root.adjustRadius(1000); } } } - + /** * Check whether a given key exists. * @param key the key to check @@ -378,6 +383,7 @@ private void toStringTree(StringBuilderLn sb, QNode node, int depth, int posInParent) { String prefix = ".".repeat(depth); sb.append(prefix + posInParent + " d=" + depth); + sb.append(" nV=" + node.getValueCount()); sb.append(" " + Arrays.toString(node.getCenter())); sb.appendLn("/" + node.getRadius()); prefix += " "; @@ -418,7 +424,7 @@ public QStats getStats() { * Statistics container class. */ public static class QStats extends Stats { - final int[] histoValues = new int[100]; + final int[] histoValues = new int[1000]; final int[] histoSubs; static final int HISTO_MAX = (1 << 10) + 1; public QStats(int dims) { diff --git a/src/main/java/org/tinspin/index/qtplain/QNode.java b/src/main/java/org/tinspin/index/qtplain/QNode.java index 6c386e1..2a801fe 100644 --- a/src/main/java/org/tinspin/index/qtplain/QNode.java +++ b/src/main/java/org/tinspin/index/qtplain/QNode.java @@ -296,6 +296,7 @@ void checkNode(QStats s, QNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { PointEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.point(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/qtplain/QRNode.java b/src/main/java/org/tinspin/index/qtplain/QRNode.java index bdf942d..766b5c8 100644 --- a/src/main/java/org/tinspin/index/qtplain/QRNode.java +++ b/src/main/java/org/tinspin/index/qtplain/QRNode.java @@ -389,6 +389,7 @@ void checkNode(QStats s, QRNode parent, int depth) { } } if (values != null) { + s.maxValuesInNode = Math.max(s.maxValuesInNode, values.size()); for (int i = 0; i < values.size(); i++) { BoxEntry e = values.get(i); if (!QUtil.fitsIntoNode(e.min(), e.max(), center, radius*QUtil.EPS_MUL)) { diff --git a/src/main/java/org/tinspin/index/util/MathTools.java b/src/main/java/org/tinspin/index/util/MathTools.java new file mode 100644 index 0000000..2507478 --- /dev/null +++ b/src/main/java/org/tinspin/index/util/MathTools.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016-2024 Tilmann Zaeschke + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tinspin.index.util; + +public class MathTools { + + private MathTools() {} + + /** + * Similar to Math.ceil() with the ceiling being the next higher power of 2. + * The resulting number can repeatedly and (almost) always be divided by two without loss of precision. + * @param d input + * @return next power of two above or equal to 'input' + */ + public static double ceilPowerOfTwo(double d) { + double ceil = floorPowerOfTwo(d); + return ceil == d ? ceil : ceil * 2; + } + + /** + * Similar to Math.floor() with the floor being the next lower power of 2. + * The resulting number can repeatedly and (almost) always be divided by two without loss of precision. + * We calculate the "floor" by setting the "fraction" of the bit representation to 0. + * @param d input + * @return next power of two below or equal to 'input' + */ + public static double floorPowerOfTwo(double d) { + // Set fraction to "0". + return Double.longBitsToDouble(Double.doubleToRawLongBits(d) & 0xFFF0_0000_0000_0000L); + } + + /** + * Calculates the {@link #floorPowerOfTwo(double)} of an array. + * @param d input vector + * @return copied vector with next lower power of two below 'input' + * @see #floorPowerOfTwo(double) + */ + public static double[] floorPowerOfTwoCopy(double[] d) { + double[] d2 = new double[d.length]; + for (int i = 0; i < d.length; i++) { + d2[i] = floorPowerOfTwo(d[i]); + } + return d2; + } + + /** + * Returns the maximal delta between any pair of scalars in the vector. + * @param v1 vector 1 + * @param v2 vector 2 + * @return maximal delta (positive or zero). + */ + public static double maxDelta(double[] v1, double[] v2) { + double dMax = 0; + for (int i = 0; i < v1.length; i++) { + dMax = Math.max(dMax, Math.abs(v1[i] - v2[i])); + } + return dMax; + } +} diff --git a/src/test/java/org/tinspin/index/kdtree/KDTreeConfigTest.java b/src/test/java/org/tinspin/index/kdtree/KDTreeConfigTest.java new file mode 100644 index 0000000..5425b27 --- /dev/null +++ b/src/test/java/org/tinspin/index/kdtree/KDTreeConfigTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2009-2017 Tilmann Zaeschke. All rights reserved. + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tinspin.index.kdtree; + +import org.junit.Test; +import org.tinspin.index.IndexConfig; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.*; +import static org.tinspin.index.Index.PointIterator; +import static org.tinspin.index.Index.PointIteratorKnn; + +public class KDTreeConfigTest { + + private static final int N_DUP = 4; + private static final int BOUND = 100; + + private List createInt(long seed, int n, int dim) { + List data = new ArrayList<>(n); + Random R = new Random(seed); + for (int i = 0; i < n; i += N_DUP) { + Entry e = new Entry(dim, i); + data.add(e); + Arrays.setAll(e.p, (x) -> R.nextInt(BOUND)); + for (int i2 = 1; i2 < N_DUP; ++i2) { + Entry e2 = new Entry(dim, i + i2); + data.add(e2); + System.arraycopy(e.p, 0, e2.p, 0, e.p.length); + } + } + return data; + } + + @Test + public void smokeTest2D() { + smokeTest(createInt(0, 100_000, 2)); + } + + @Test + public void smokeTest3D() { + smokeTest(createInt(0, 10_000, 3)); + } + + @Test + public void testDefensiveCopyingDefault() { + IndexConfig config = IndexConfig.create(42); + assertEquals(42, config.getDimensions()); + assertTrue(config.getDefensiveKeyCopy()); + config.setDefensiveKeyCopy(false); + assertFalse(config.getDefensiveKeyCopy()); + } + + @Test + public void testDefensiveCopyingFalse() { + IndexConfig config = IndexConfig.create(3).setDefensiveKeyCopy(false); + KDTree tree = KDTree.create(config); + + double[] point = new double[]{1, 2, 3}; + double[] pointOriginal = point.clone(); + Entry e = new Entry(point, 42); + tree.insert(point, e); + + // Never do this! We just verify that the key is not copied. + point[1] = 15; + assertTrue(tree.contains(point)); + assertFalse(tree.contains(pointOriginal)); + } + + @Test + public void testDefensiveCopyingTrue() { + IndexConfig config = IndexConfig.create(3).setDefensiveKeyCopy(true); + KDTree tree = KDTree.create(config); + + double[] point = new double[]{1, 2, 3}; + double[] pointOriginal = point.clone(); + Entry e = new Entry(point, 42); + tree.insert(point, e); + + // Never do this! We just verify that the key is not copied. + point[1] = 15; + assertFalse(tree.contains(point)); + assertTrue(tree.contains(pointOriginal)); + } + + private void smokeTest(List data) { + int dim = data.get(0).p.length; + IndexConfig config = IndexConfig.create(dim).setDefensiveKeyCopy(false); + KDTree tree = KDTree.create(config); + for (Entry e : data) { + tree.insert(e.p, e); + } +// System.out.println(tree.toStringTree()); + for (Entry e : data) { + if (!tree.contains(e.p)) { + throw new IllegalStateException(Arrays.toString(e.p)); + } + } + + for (Entry e : data) { + // System.out.println("kNN query: " + e); + PointIteratorKnn iter = tree.queryKnn(e.p, N_DUP); + if (!iter.hasNext()) { + throw new IllegalStateException("kNN() failed: " + Arrays.toString(e.p)); + } + Entry answer = iter.next().value(); + if (answer.p != e.p && !Arrays.equals(answer.p, e.p)) { + throw new IllegalStateException("Expected " + Arrays.toString(e.p) + " but got " + Arrays.toString(answer.p)); + } + } + + for (Entry e : data) { + // System.out.println("query: " + Arrays.toString(e.p)); + PointIterator iter = tree.query(e.p, e.p); + if (!iter.hasNext()) { + throw new IllegalStateException("query() failed: " + Arrays.toString(e.p)); + } + for (int i = 0; i < N_DUP; ++i) { + // System.out.println(" found: " + i + " " + e); + Entry answer = iter.next().value(); + if (!Arrays.equals(answer.p, e.p)) { + throw new IllegalStateException("Expected " + e + " but got " + answer); + } + } + } + + for (Entry e : data) { +// System.out.println(tree.toStringTree()); +// System.out.println("Removing: " + Arrays.toString(key)); + if (!tree.contains(e.p)) { + throw new IllegalStateException("containsExact() failed: " + Arrays.toString(e.p)); + } + Entry answer = tree.remove(e.p); + if (answer.p != e.p && !Arrays.equals(answer.p, e.p)) { + throw new IllegalStateException("Expected " + Arrays.toString(e.p) + " but got " + Arrays.toString(answer.p)); + } + } + } + + private static class Entry { + double[] p; + int id; + + public Entry(int dim, int id) { + this.p = new double[dim]; + this.id = id; + } + + public Entry(double[] key, int id) { + this.p = key; + this.id = id; + } + + boolean equals(Entry e) { + return id == e.id && Arrays.equals(p, e.p); + } + + @Override + public String toString() { + return "id=" + id + ":" + Arrays.toString(p); + } + } +} diff --git a/src/test/java/org/tinspin/index/test/BoxMapTest.java b/src/test/java/org/tinspin/index/test/BoxMapTest.java index a85dd0a..be7f9d5 100644 --- a/src/test/java/org/tinspin/index/test/BoxMapTest.java +++ b/src/test/java/org/tinspin/index/test/BoxMapTest.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.tinspin.index.BoxMap; +import org.tinspin.index.Index; import java.util.*; @@ -265,6 +266,66 @@ public void testRemove() { assertEquals(0, tree.size()); } + @Test + public void testIssueKnnRemove() { + if (candidate == IDX.COVER) { + return; + } + Random r = new Random(0); + int dim = 3; + int n = 1000; + int nDelete = n/2; + ArrayList data = createInt(0, n, 3); + BoxMap tree = createTree(data.size(), dim); + + Collections.shuffle(data, r); + + for (Entry e : data) { + tree.insert(e.p1, e.p2, e); + } + + // remove 1st half + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertNotNull(tree.remove(e.p1, e.p2)); + assertNull(tree.remove(e.p1, e.p2)); + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2)); + } + + // check contains() & kNN + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2)); + Index.BoxEntryKnn eKnn = tree.query1nn(e.p1); + assertTrue(tree.contains(eKnn.min(), eKnn.max())); + } + + // Issue #37: Entries remained referenced in arrays after removal. + // Moreover, the entry count was ignored by kNN, so it looked at the whole + // array instead of just the valid entries. + Index.BoxIteratorKnn itKnn = tree.queryKnn(data.get(0).p1, n); + Set kNNResult = new HashSet<>(); + while (itKnn.hasNext()) { + Index.BoxEntryKnn eKnn = itKnn.next(); + kNNResult.add(eKnn.value().id); + } + for (int i = 0; i < n; i++) { + Entry e = data.get(i); + if (i < nDelete) { + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2)); + assertFalse(kNNResult.contains(e.id)); + } else { + assertTrue(containsExact(tree, e.p1, e.p2, e.id)); + assertTrue(tree.contains(e.p1, e.p2)); + assertTrue(kNNResult.contains(e.id)); + } + } + assertEquals(n - nDelete, kNNResult.size()); + } + private BoxMap createTree(int size, int dims) { switch (candidate) { case ARRAY: diff --git a/src/test/java/org/tinspin/index/test/BoxMultimapTest.java b/src/test/java/org/tinspin/index/test/BoxMultimapTest.java index 3ce7dc2..75017d8 100644 --- a/src/test/java/org/tinspin/index/test/BoxMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/BoxMultimapTest.java @@ -21,6 +21,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.tinspin.index.BoxMultimap; +import org.tinspin.index.Index; import org.tinspin.index.util.MutableInt; import java.util.*; @@ -326,6 +327,66 @@ public void testRemoveIf() { assertEquals(0, tree.size()); } + @Test + public void testIssueKnnRemove() { + if (candidate == IDX.COVER) { + return; + } + Random r = new Random(0); + int dim = 3; + int n = 1000; + int nDelete = n/2; + ArrayList data = createInt(0, n, 3); + BoxMultimap tree = createTree(data.size(), dim); + + Collections.shuffle(data, r); + + for (Entry e : data) { + tree.insert(e.p1, e.p2, e); + } + + // remove 1st half + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertTrue(tree.remove(e.p1, e.p2, e)); + assertFalse(tree.remove(e.p1, e.p2, e)); + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2, e)); + } + + // check contains() & kNN + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2, e)); + Index.BoxEntryKnn eKnn = tree.query1nn(e.p1); + assertTrue(tree.contains(eKnn.min(), eKnn.max(), eKnn.value())); + } + + // Issue #37: Entries remained referenced in arrays after removal. + // Moreover, the entry count was ignored by kNN, so it looked at the whole + // array instead of just the valid entries. + Index.BoxIteratorKnn itKnn = tree.queryKnn(data.get(0).p1, n); + Set kNNResult = new HashSet<>(); + while (itKnn.hasNext()) { + Index.BoxEntryKnn eKnn = itKnn.next(); + kNNResult.add(eKnn.value().id); + } + for (int i = 0; i < n; i++) { + Entry e = data.get(i); + if (i < nDelete) { + assertFalse(containsExact(tree, e.p1, e.p2, e.id)); + assertFalse(tree.contains(e.p1, e.p2, e)); + assertFalse(kNNResult.contains(e.id)); + } else { + assertTrue(containsExact(tree, e.p1, e.p2, e.id)); + assertTrue(tree.contains(e.p1, e.p2, e)); + assertTrue(kNNResult.contains(e.id)); + } + } + assertEquals(n - nDelete, kNNResult.size()); + } + private BoxMultimap createTree(int size, int dims) { switch (candidate) { case ARRAY: diff --git a/src/test/java/org/tinspin/index/test/Issue0037Test.java b/src/test/java/org/tinspin/index/test/Issue0037Test.java new file mode 100644 index 0000000..d288eb2 --- /dev/null +++ b/src/test/java/org/tinspin/index/test/Issue0037Test.java @@ -0,0 +1,145 @@ +/* + * Copyright 2009-2017 Tilmann Zaeschke. All rights reserved. + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.tinspin.index.test; + +import org.junit.Test; +import org.tinspin.index.PointMap; +import org.tinspin.index.Stats; +import org.tinspin.index.qthypercube.QuadTreeKD; +import org.tinspin.index.qthypercube2.QuadTreeKD2; +import org.tinspin.index.qtplain.QuadTreeKD0; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; + +import static org.junit.Assert.assertTrue; + +public class Issue0037Test { + + @Test + public void testQT() throws IOException { + QuadTreeKD tree = QuadTreeKD.create(2); + testTree(tree); + } + + @Test + public void testQT0() throws IOException { + QuadTreeKD0 tree = QuadTreeKD0.create(2); + testTree(tree); + } + + @Test + public void testQT2() throws IOException { + QuadTreeKD2 tree = QuadTreeKD2.create(2); + testTree(tree); + } + + /** + * p=20.7747859954834, -335.053844928741 + * knn=[13.7747859954834, -335.053844928741] + * 1476 + * true + * p=20.7747859954834, -335.053844928741 + * knn=[24.7677965164185, -335.507710456848] + * 1476 + * false + * + * + * p=20.7747859954834, -335.053844928741 + * knn=[13.7747859954834, -335.053844928741] + * 1476 + * true + */ + private void testTree(PointMap tree) throws IOException { + File file = new File("src/test/resources/issue0037.txt"); + + BufferedReader reader = new BufferedReader(new FileReader(file)); + int n = Integer.parseInt(reader.readLine()); + for (int i = 0; i < n; i++) { + String[] sp = reader.readLine().split(" "); + double d1 = Double.parseDouble(sp[0]), d2 = Double.parseDouble(sp[1]); + + tree.insert(new double[]{d1, d2}, i); + } + int k = Integer.parseInt(reader.readLine()); + for (int i = 0; i < k; i++) { + String[] sp = reader.readLine().split(" "); + double d1 = Double.parseDouble(sp[0]), d2 = Double.parseDouble(sp[1]); + System.out.println("Remove: " + d1 + ", " + d2); + tree.remove(new double[]{d1, d2}); + } + Stats stats = tree.getStats(); + System.out.println("Stats: n=" + stats.nEntries); + { + String[] sp = reader.readLine().split(" "); + double d1 = Double.parseDouble(sp[0]), d2 = Double.parseDouble(sp[1]); + System.out.println("p=" + d1 + ", " + d2); + boolean has13_335 = tree.contains(new double[]{13.7747859954834, -335.053844928741}); + System.out.println("contains: 13.7747859954834, -335.053844928741 ? " + has13_335); + var node = tree.query1nn(new double[]{d1, d2}); + System.out.println("knn=" + Arrays.toString(node.point())); + boolean has = tree.contains(node.point()); + System.out.println(n); + + System.out.println(has); // Should print true, but prints false with QuadTreeKD2 + assertTrue(has); + } + } + + private void testTree2(PointMap tree) throws IOException { + Random R = new Random(0); + int n = 100; + int k = n/2; + + ArrayList array = new ArrayList<>(); + for (int i = 0; i < n; i++) { + double d1 = R.nextDouble(), d2 = R.nextDouble(); + double[] p = new double[]{d1, d2}; + array.add(p); + tree.insert(p, i); + } + for (int i = 0; i < k; i++) { + double d1 = R.nextDouble(), d2 = R.nextDouble(); + tree.remove(new double[]{d1, d2}); + } +// { +// String[] sp = reader.readLine().split(" "); +// double d1 = Double.parseDouble(sp[0]), d2 = Double.parseDouble(sp[1]); +// var node = tree.query1nn(new double[]{d1, d2}); +// boolean has = tree.contains(node.point()); +// System.out.println(n); +// +// System.out.println(has); // Should print true, but prints false with QuadTreeKD2 +// assertTrue(has); +// } + for (int i = 0; i < n; i++) { + var node = tree.query1nn(array.get(i)); + boolean has = tree.contains(node.point()); + System.out.println(n); + + System.out.println(has); // Should print true, but prints false with QuadTreeKD2 + assertTrue(has); + } + } +} diff --git a/src/test/java/org/tinspin/index/test/PointMapTest.java b/src/test/java/org/tinspin/index/test/PointMapTest.java index 8b2bfa0..224a0de 100644 --- a/src/test/java/org/tinspin/index/test/PointMapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMapTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.tinspin.index.Index; import org.tinspin.index.PointMap; import java.util.*; @@ -126,6 +127,7 @@ private void smokeTest(List data) { tree.insert(e.p, e); } // System.out.println(tree.toStringTree()); + tree.getStats(); for (Entry e : data) { assertTrue("contains(point) failed: " + e, tree.contains(e.p)); Entry e2 = tree.queryExact(e.p); @@ -258,6 +260,67 @@ public void testRemove() { assertEquals(0, tree.size()); } + @Test + public void testIssueKnnRemove() { + if (candidate == IDX.COVER) { + return; + } + Random r = new Random(0); + int dim = 3; + int n = 1000; + int nDelete = n/2; + ArrayList data = createInt(0, n, 3); + PointMap tree = createTree(data.size(), dim); + + Collections.shuffle(data, r); + + for (Entry e : data) { + tree.insert(e.p, e); + } + + // remove 1st half + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertNotNull(tree.remove(e.p)); + assertNull(tree.remove(e.p)); + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p)); + } + + // check contains() & kNN + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p)); + Index.PointEntryKnn eKnn = tree.query1nn(e.p); + assertFalse(Arrays.equals(e.p, eKnn.point())); + assertTrue(tree.contains(eKnn.point())); + } + + // Issue #37: Entries remained referenced in arrays after removal. + // Moreover, the entry count was ignored by kNN, so it looked at the whole + // array instead of just the valid entries. + Index.PointIteratorKnn itKnn = tree.queryKnn(data.get(0).p, n); + Set kNNResult = new HashSet<>(); + while (itKnn.hasNext()) { + Index.PointEntryKnn eKnn = itKnn.next(); + kNNResult.add(eKnn.value().id); + } + for (int i = 0; i < n; i++) { + Entry e = data.get(i); + if (i < nDelete) { + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p)); + assertFalse(kNNResult.contains(e.id)); + } else { + assertTrue(containsExact(tree, e.p, e.id)); + assertTrue(tree.contains(e.p)); + assertTrue(kNNResult.contains(e.id)); + } + } + assertEquals(n - nDelete, kNNResult.size()); + } + private PointMap createTree(int size, int dims) { switch (candidate) { case ARRAY: diff --git a/src/test/java/org/tinspin/index/test/PointMultimapTest.java b/src/test/java/org/tinspin/index/test/PointMultimapTest.java index 6669b02..a6a7c6e 100644 --- a/src/test/java/org/tinspin/index/test/PointMultimapTest.java +++ b/src/test/java/org/tinspin/index/test/PointMultimapTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.tinspin.index.Index; import org.tinspin.index.PointMultimap; import org.tinspin.index.test.util.TestInstances; @@ -140,7 +141,10 @@ private void smokeTest(List data) { for (Entry e : data) { tree.insert(e.p, e); } - // System.out.println(tree.toStringTree()); + + // Check consistency + tree.getStats(); + for (Entry e : data) { PointIterator it = tree.queryExactPoint(e.p); assertTrue("query(point) failed: " + e, it.hasNext()); @@ -166,6 +170,8 @@ private void smokeTest(List data) { assertEquals(data.size(), nExtent); } + tree.getStats(); + for (Entry e : data) { // System.out.println("query: " + Arrays.toString(e.p)); PointIterator iter = tree.query(e.p, e.p); @@ -210,6 +216,7 @@ public void testUpdate() { assertFalse(containsExact(tree, pOld, e.id)); assertTrue(containsExact(tree, e.p, e.id)); } + tree.getStats(); for (int i = 0; i < data.size(); ++i) { Entry e = data.get(i); @@ -309,6 +316,68 @@ public void testRemoveIf() { assertEquals(0, tree.size()); } + @Test + public void testIssueKnnRemove() { + if (candidate == IDX.COVER) { + return; + } + Random r = new Random(0); + int dim = 3; + int n = 1000; + int nDelete = n/2; + ArrayList data = createInt(0, n, 3); + PointMultimap tree = createTree(data.size(), dim); + + Collections.shuffle(data, r); + + for (Entry e : data) { + tree.insert(e.p, e); + } + tree.getStats(); + + // remove 1st half + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertTrue(tree.remove(e.p, e)); + assertFalse(tree.remove(e.p, e)); + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p, e)); + } + tree.getStats(); + + // check contains() & kNN + for (int i = 0; i < nDelete; ++i) { + Entry e = data.get(i); + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p, e)); + Index.PointEntryKnn eKnn = tree.query1nn(e.p); + assertTrue(tree.contains(eKnn.point(), eKnn.value())); + } + + // Issue #37: Entries remained referenced in arrays after removal. + // Moreover, the entry count was ignored by kNN, so it looked at the whole + // array instead of just the valid entries. + Index.PointIteratorKnn itKnn = tree.queryKnn(data.get(0).p, n); + Set kNNResult = new HashSet<>(); + while (itKnn.hasNext()) { + Index.PointEntryKnn eKnn = itKnn.next(); + kNNResult.add(eKnn.value().id); + } + for (int i = 0; i < n; i++) { + Entry e = data.get(i); + if (i < nDelete) { + assertFalse(containsExact(tree, e.p, e.id)); + assertFalse(tree.contains(e.p, e)); + assertFalse(kNNResult.contains(e.id)); + } else { + assertTrue(containsExact(tree, e.p, e.id)); + assertTrue(tree.contains(e.p, e)); + assertTrue(kNNResult.contains(e.id)); + } + } + assertEquals(n - nDelete, kNNResult.size()); + } + private PointMultimap createTree(int size, int dims) { switch (candidate) { case ARRAY: @@ -356,4 +425,42 @@ public String toString() { return "id=" + id + ":" + Arrays.toString(p); } } + + @Test + public void testIssue0040_remove() { + double[][] data = new double[][] { + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 0}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-45.6938514709473, 32.9847145080566, -2056090140, 0}, + new double[]{-1.7595032453537, 112.097793579102, -267989921, 0}, + new double[]{-1.75950336456299, 112.097793579102, -267989921, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{45.6938438415527, 32.9847145080566, 1591613824, 0}, + new double[]{49.0948944091797, -2.05027413368225, 14481734, 0}, + new double[]{49.0948944091797, -2.05027389526367, 14481734, 0}, + new double[]{-49.0949020385742, -2.05027413368225, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027389526367, 819588127, 1}, + new double[]{-49.0949020385742, -2.05027413368225, 916603126, 0}, + }; + + PointMultimap tree = createTree(100,2); + for (int i = 0; i < data.length; i++) { + if (data[i][3] == 0) { + tree.insert(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } else { + tree.remove(Arrays.copyOf(data[i], 2), (int)data[i][2]); + } + } + + assertEquals(9, tree.size()); + int n = 0; + double[] min = new double[]{-50, -3}; + double[] max = new double[]{50, 113}; + for (Index.PointIterator it = tree.query(min, max); it.hasNext(); it.next()) { + n++; + } + assertEquals(9, n); + } + } diff --git a/src/test/java/org/tinspin/util/MathToolsTest.java b/src/test/java/org/tinspin/util/MathToolsTest.java new file mode 100644 index 0000000..c33150f --- /dev/null +++ b/src/test/java/org/tinspin/util/MathToolsTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-2024 Tilmann Zaeschke + * + * This file is part of TinSpin. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tinspin.util; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.tinspin.index.util.MathTools; + +import java.util.Arrays; + +public class MathToolsTest { + + @Test + public void powerOfTwoCeil() { + assertEquals(1./32., MathTools.ceilPowerOfTwo(0.03), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.3), 0.0); + assertEquals(4, MathTools.ceilPowerOfTwo(3), 0.0); + assertEquals(32, MathTools.ceilPowerOfTwo(30), 0.0); + assertEquals(512, MathTools.ceilPowerOfTwo(300), 0.0); + + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.3), 0.0); + assertEquals(-4, MathTools.ceilPowerOfTwo(-3), 0.0); + assertEquals(-32, MathTools.ceilPowerOfTwo(-30), 0.0); + + // identity + assertEquals(0, MathTools.ceilPowerOfTwo(0), 0.0); + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.5), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.5), 0.0); + assertEquals(-1, MathTools.ceilPowerOfTwo(-1), 0.0); + assertEquals(1, MathTools.ceilPowerOfTwo(1), 0.0); + assertEquals(-2, MathTools.ceilPowerOfTwo(-2), 0.0); + assertEquals(2, MathTools.ceilPowerOfTwo(2), 0.0); + } + + @Test + public void powerOfTwoFloor() { + assertEquals(1./64., MathTools.floorPowerOfTwo(0.03), 0.0); + assertEquals(0.25, MathTools.floorPowerOfTwo(0.3), 0.0); + assertEquals(2, MathTools.floorPowerOfTwo(3), 0.0); + assertEquals(16, MathTools.floorPowerOfTwo(30), 0.0); + assertEquals(256, MathTools.floorPowerOfTwo(300), 0.0); + + assertEquals(-0.25, MathTools.floorPowerOfTwo(-0.3), 0.0); + assertEquals(-2, MathTools.floorPowerOfTwo(-3), 0.0); + assertEquals(-16, MathTools.floorPowerOfTwo(-30), 0.0); + + // identity + assertEquals(0, MathTools.ceilPowerOfTwo(0), 0.0); + assertEquals(-0.5, MathTools.ceilPowerOfTwo(-0.5), 0.0); + assertEquals(0.5, MathTools.ceilPowerOfTwo(0.5), 0.0); + assertEquals(-1, MathTools.ceilPowerOfTwo(-1), 0.0); + assertEquals(1, MathTools.ceilPowerOfTwo(1), 0.0); + assertEquals(-2, MathTools.ceilPowerOfTwo(-2), 0.0); + assertEquals(2, MathTools.ceilPowerOfTwo(2), 0.0); + } + + @Test + public void powerOfTwoFloor_vector() { + double[] d = {0.03, 0.3, 3, 30, 300}; + double[] dCopy = MathTools.floorPowerOfTwoCopy(d); + assertFalse(Arrays.equals(d, dCopy)); + assertEquals(1./64., dCopy[0], 0.0); + assertEquals(0.25, dCopy[1], 0.0); + assertEquals(2, dCopy[2], 0.0); + assertEquals(16, dCopy[3], 0.0); + assertEquals(256, dCopy[4], 0.0); + } + + @Test + public void maxDelta() { + assertEquals(12, MathTools.maxDelta(new double[]{-4.}, new double[]{8.}), 0.0); + assertEquals(12, MathTools.maxDelta(new double[]{8.}, new double[]{-4.}), 0.0); + + assertEquals(4, MathTools.maxDelta(new double[]{2, 4, 2}, new double[]{3, 8, 4}), 0.0); + } +} diff --git a/src/test/java/org/tinspin/MinHeapTest.java b/src/test/java/org/tinspin/util/MinHeapTest.java similarity index 99% rename from src/test/java/org/tinspin/MinHeapTest.java rename to src/test/java/org/tinspin/util/MinHeapTest.java index f729c0e..8eff04a 100644 --- a/src/test/java/org/tinspin/MinHeapTest.java +++ b/src/test/java/org/tinspin/util/MinHeapTest.java @@ -14,7 +14,8 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package org.tinspin; + */ +package org.tinspin.util; import org.junit.Test; import org.tinspin.index.util.MinHeapI; diff --git a/src/test/java/org/tinspin/MinMaxHeapTest.java b/src/test/java/org/tinspin/util/MinMaxHeapTest.java similarity index 99% rename from src/test/java/org/tinspin/MinMaxHeapTest.java rename to src/test/java/org/tinspin/util/MinMaxHeapTest.java index a8bc46d..c812671 100644 --- a/src/test/java/org/tinspin/MinMaxHeapTest.java +++ b/src/test/java/org/tinspin/util/MinMaxHeapTest.java @@ -14,7 +14,8 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */package org.tinspin; + */ +package org.tinspin.util; import static org.junit.Assert.*; import org.junit.Test; diff --git a/src/test/resources/issue0037.txt b/src/test/resources/issue0037.txt new file mode 100644 index 0000000..76c3cc5 --- /dev/null +++ b/src/test/resources/issue0037.txt @@ -0,0 +1,1491 @@ +1476 +31.7677965164185 -335.507710456848 +31.0062980651855 19.9242215156555 +-1.0154333114624 -333.677649497986 +-1.96946239471436 18.9298062324524 +9.97757720947266 -334.131515026093 +8.82774639129639 18.0074763298035 +64.7435569763184 -334.513295173645 +63.7895278930664 18.0941605567932 +42.9533376693726 -333.137099742889 +41.9993085861206 19.4703559875488 +53.9463481903076 -333.590965270996 +52.7965173721313 18.5480260848999 +109.707783699036 -334.397708415985 +108.753754615784 18.2097473144531 +120.700794219971 -334.851573944092 +119.939295768738 20.5803580284119 +87.9175643920898 -333.021512985229 +86.9635353088379 19.5859427452087 +98.9105749130249 -333.475378513336 +97.7607440948486 18.6636128425598 +153.676554679871 -333.857158660889 +152.722525596619 18.7502970695496 +131.886335372925 -332.480963230133 +130.932306289673 20.1264925003052 +142.87934589386 -332.93482875824 +141.729515075684 19.2041625976563 +199.085423469543 -335.160625934601 +198.131394386292 17.4468297958374 +210.078433990479 -335.614491462708 +209.316935539246 19.8174405097961 +177.295204162598 -333.784430503845 +176.341175079346 18.823025226593 +188.288214683533 -334.238296031952 +187.138383865356 17.9006953239441 +243.054194450378 -334.620076179504 +242.100165367126 17.9873795509338 +221.263975143433 -333.243880748749 +220.309946060181 19.3635749816895 +232.256985664368 -333.697746276855 +231.107154846191 18.4412450790405 +288.018421173096 -334.504489421844 +287.064392089844 18.1029663085938 +299.011431694031 -334.958354949951 +298.249933242798 20.4735770225525 +266.22820186615 -333.128293991089 +265.274172782898 19.4791617393494 +277.221212387085 -333.582159519196 +276.071381568909 18.5568318367004 +331.987192153931 -333.963939666748 +331.033163070679 18.6435160636902 +310.196972846985 -332.587744235992 +309.242943763733 20.0197114944458 +321.18998336792 -333.041609764099 +320.040152549744 19.0973815917969 +375.944495201111 -336.525193214417 +374.990466117859 16.0822625160217 +386.937505722046 -336.979058742523 +386.176007270813 18.4528732299805 +354.154275894165 -335.148997783661 +353.200246810913 17.4584579467773 +365.1472864151 -335.602863311768 +363.997455596924 16.5361280441284 +419.913266181946 -335.98464345932 +418.959237098694 16.6228122711182 +398.123046875 -334.608448028564 +397.169017791748 17.9990077018738 +409.116057395935 -335.062313556671 +407.966226577759 17.0766777992249 +464.877492904663 -335.86905670166 +463.923463821411 16.7383990287781 +475.870503425598 -336.322922229767 +475.109004974365 19.1090097427368 +443.087273597717 -334.492861270905 +442.133244514465 18.1145944595337 +454.080284118652 -334.946726799011 +452.930453300476 17.1922645568848 +508.846263885498 -335.328506946564 +507.892234802246 17.2789487838745 +487.056044578552 -333.952311515808 +486.1020154953 18.6551442146301 +498.049055099487 -334.406177043915 +496.899224281311 17.7328143119812 +554.255132675171 -336.631974220276 +553.301103591919 15.9754815101624 +565.248143196106 -337.085839748383 +564.486644744873 18.3460922241211 +532.464913368225 -335.25577878952 +531.510884284973 17.351676940918 +543.45792388916 -335.709644317627 +542.308093070984 16.429347038269 +598.223903656006 -336.091424465179 +597.269874572754 16.5160312652588 +576.43368434906 -334.715229034424 +575.479655265808 17.8922266960144 +587.426694869995 -335.169094562531 +586.276864051819 16.9698967933655 +643.188130378723 -335.97583770752 +642.234101295471 16.6316180229187 +654.181140899658 -336.429703235626 +653.419642448425 19.0022287368774 +621.397911071777 -334.599642276764 +620.443881988525 18.0078134536743 +632.390921592712 -335.053507804871 +631.241090774536 17.0854835510254 +687.156901359558 -335.435287952423 +686.202872276306 17.1721677780151 +665.366682052612 -334.059092521667 +664.41265296936 18.5483632087708 +676.359692573547 -334.512958049774 +675.209861755371 17.6260333061218 +14.7762746810913 -291.039459228516 +13.8207569122314 17.5536108016968 +25.7692852020264 -291.493324756622 +25.0062980651855 19.9242215156555 +-7.01394462585449 -289.66326379776 +-7.96946239471436 18.9298062324524 +3.97906589508057 -290.117129325867 +2.82774639129639 18.0074763298035 +58.7450456619263 -290.498909473419 +57.7895278930664 18.0941605567932 +36.9548263549805 -289.122714042664 +35.9993085861206 19.4703559875488 +47.9478368759155 -289.57657957077 +46.7965173721313 18.5480260848999 +16.7747859954834 -335.053844928741 +15.8192682266235 -26.4607748985291 +27.7677965164185 -335.507710456848 +27.0048093795776 -24.0901641845703 +-5.0154333114624 -333.677649497986 +-5.97095108032227 -25.0845794677734 +5.97757720947266 -334.131515026093 +4.82625770568848 -26.0069093704224 +60.7435569763184 -334.513295173645 +59.7880392074585 -25.9202251434326 +38.9533376693726 -333.137099742889 +37.9978199005127 -24.544029712677 +49.9463481903076 -333.590965270996 +48.7950286865234 -25.4663596153259 +103.709272384644 -290.383322715759 +102.753754615784 18.2097473144531 +114.702282905579 -290.837188243866 +113.939295768738 20.5803580284119 +81.9190530776978 -289.007127285004 +80.9635353088379 19.5859427452087 +92.9120635986328 -289.46099281311 +91.7607440948486 18.6636128425598 +147.678043365479 -289.842772960663 +146.722525596619 18.7502970695496 +125.887824058533 -288.466577529907 +124.932306289673 20.1264925003052 +136.880834579468 -288.920443058014 +135.729515075684 19.2041625976563 +105.707783699036 -334.397708415985 +104.752265930176 -25.8046383857727 +116.700794219971 -334.851573944092 +115.93780708313 -23.434027671814 +83.9175643920898 -333.021512985229 +82.96204662323 -24.4284429550171 +94.9105749130249 -333.475378513336 +93.7592554092407 -25.350772857666 +149.676554679871 -333.857158660889 +148.721036911011 -25.2640886306763 +127.886335372925 -332.480963230133 +126.930817604065 -23.8878931999207 +138.87934589386 -332.93482875824 +137.728026390076 -24.8102231025696 +193.086912155151 -291.146240234375 +192.131394386292 17.4468297958374 +204.079922676086 -291.600105762482 +203.316935539246 19.8174405097961 +171.296692848206 -289.770044803619 +170.341175079346 18.823025226593 +182.289703369141 -290.223910331726 +181.138383865356 17.9006953239441 +237.055683135986 -290.605690479279 +236.100165367126 17.9873795509338 +215.265463829041 -289.229495048523 +214.309946060181 19.3635749816895 +226.258474349976 -289.68336057663 +225.107154846191 18.4412450790405 +195.085423469543 -335.160625934601 +194.129905700684 -26.5675559043884 +206.078433990479 -335.614491462708 +205.315446853638 -24.1969451904297 +173.295204162598 -333.784430503845 +172.339686393738 -25.1913604736328 +184.288214683533 -334.238296031952 +183.136895179749 -26.1136903762817 +239.054194450378 -334.620076179504 +238.098676681519 -26.027006149292 +217.263975143433 -333.243880748749 +216.308457374573 -24.6508107185364 +228.256985664368 -333.697746276855 +227.105666160583 -25.5731406211853 +282.019909858704 -290.490103721619 +281.064392089844 18.1029663085938 +293.012920379639 -290.943969249725 +292.249933242798 20.4735770225525 +260.229690551758 -289.113908290863 +259.274172782898 19.4791617393494 +271.222701072693 -289.56777381897 +270.071381568909 18.5568318367004 +325.988680839539 -289.949553966522 +325.033163070679 18.6435160636902 +304.198461532593 -288.573358535767 +303.242943763733 20.0197114944458 +315.191472053528 -289.027224063873 +314.040152549744 19.0973815917969 +284.018421173096 -334.504489421844 +283.062903404236 -25.9114193916321 +295.011431694031 -334.958354949951 +294.24844455719 -23.5408086776733 +262.22820186615 -333.128293991089 +261.27268409729 -24.5352239608765 +273.221212387085 -333.582159519196 +272.069892883301 -25.4575538635254 +327.987192153931 -333.963939666748 +327.031674385071 -25.3708696365356 +306.196972846985 -332.587744235992 +305.241455078125 -23.99467420578 +317.18998336792 -333.041609764099 +316.038663864136 -24.917004108429 +369.945983886719 -292.510807514191 +368.990466117859 16.0822625160217 +380.938994407654 -292.964673042297 +380.176007270813 18.4528732299805 +348.155764579773 -291.134612083435 +347.200246810913 17.4584579467773 +359.148775100708 -291.588477611542 +357.997455596924 16.5361280441284 +413.914754867554 -291.970257759094 +412.959237098694 16.6228122711182 +392.124535560608 -290.594062328339 +391.169017791748 17.9990077018738 +403.117546081543 -291.047927856445 +401.966226577759 17.0766777992249 +371.944495201111 -336.525193214417 +370.988977432251 -27.9321231842041 +382.937505722046 -336.979058742523 +382.174518585205 -25.5615124702454 +350.154275894165 -335.148997783661 +349.198758125305 -26.5559277534485 +361.1472864151 -335.602863311768 +359.995966911316 -27.4782576560974 +415.913266181946 -335.98464345932 +414.957748413086 -27.3915734291077 +394.123046875 -334.608448028564 +393.16752910614 -26.0153779983521 +405.116057395935 -335.062313556671 +403.964737892151 -26.937707901001 +458.878981590271 -291.854671001434 +457.923463821411 16.7383990287781 +469.871992111206 -292.308536529541 +469.109004974365 19.1090097427368 +437.088762283325 -290.478475570679 +436.133244514465 18.1145944595337 +448.08177280426 -290.932341098785 +446.930453300476 17.1922645568848 +502.847752571106 -291.314121246338 +501.892234802246 17.2789487838745 +481.05753326416 -289.937925815582 +480.1020154953 18.6551442146301 +492.050543785095 -290.391791343689 +490.899224281311 17.7328143119812 +460.877492904663 -335.86905670166 +459.921975135803 -27.2759866714478 +471.870503425598 -336.322922229767 +471.107516288757 -24.905375957489 +439.087273597717 -334.492861270905 +438.131755828857 -25.8997912406921 +450.080284118652 -334.946726799011 +448.928964614868 -26.8221211433411 +504.846263885498 -335.328506946564 +503.890746116638 -26.7354369163513 +483.056044578552 -333.952311515808 +482.100526809692 -25.3592414855957 +494.049055099487 -334.406177043915 +492.897735595703 -26.2815713882446 +548.256621360779 -292.61758852005 +547.301103591919 15.9754815101624 +559.249631881714 -293.071454048157 +558.486644744873 18.3460922241211 +526.466402053833 -291.241393089294 +525.510884284973 17.351676940918 +537.459412574768 -291.695258617401 +536.308093070984 16.429347038269 +592.225392341614 -292.077038764954 +591.269874572754 16.5160312652588 +570.435173034668 -290.700843334198 +569.479655265808 17.8922266960144 +581.428183555603 -291.154708862305 +580.276864051819 16.9698967933655 +550.255132675171 -336.631974220276 +549.299614906311 -28.0389041900635 +561.248143196106 -337.085839748383 +560.485156059265 -25.6682934761047 +528.464913368225 -335.25577878952 +527.509395599365 -26.6627087593079 +539.45792388916 -335.709644317627 +538.306604385376 -27.5850386619568 +594.223903656006 -336.091424465179 +593.268385887146 -27.498354434967 +572.43368434906 -334.715229034424 +571.4781665802 -26.1221590042114 +583.426694869995 -335.169094562531 +582.275375366211 -27.0444889068604 +637.189619064331 -291.961452007294 +636.234101295471 16.6316180229187 +648.182629585266 -292.4153175354 +647.419642448425 19.0022287368774 +615.399399757385 -290.585256576538 +614.443881988525 18.0078134536743 +626.39241027832 -291.039122104645 +625.241090774536 17.0854835510254 +681.158390045166 -291.420902252197 +680.202872276306 17.1721677780151 +659.36817073822 -290.044706821442 +658.41265296936 18.5483632087708 +670.361181259155 -290.498572349548 +669.209861755371 17.6260333061218 +639.188130378723 -335.97583770752 +638.232612609863 -27.3827676773071 +650.181140899658 -336.429703235626 +649.418153762817 -25.0121569633484 +617.397911071777 -334.599642276764 +616.442393302917 -26.0065722465515 +628.390921592712 -335.053507804871 +627.239602088928 -26.9289021492004 +683.156901359558 -335.435287952423 +682.201383590698 -26.8422179222107 +661.366682052612 -334.059092521667 +660.411164283752 -25.4660224914551 +672.359692573547 -334.512958049774 +671.208373069763 -26.388352394104 +69.7380561828613 -290.952775001526 +68.7880392074585 -25.9202251434326 +158.671053886414 -290.29663848877 +157.721036911011 -25.2640886306763 +248.048693656921 -291.059556007385 +247.098676681519 -26.027006149292 +336.981691360474 -290.403419494629 +336.031674385071 -25.3708696365356 +424.907765388489 -292.424123287201 +423.957748413086 -27.3915734291077 +513.840763092041 -291.767986774445 +512.890746116638 -26.7354369163513 +603.218402862549 -292.53090429306 +602.268385887146 -27.498354434967 +692.151400566101 -291.874767780304 +691.201383590698 -26.8422179222107 +12.9273357391357 -201.649954795837 +9.82774639129639 18.0074763298035 +23.9203462600708 -202.103820323944 +20.8207569122314 17.5536108016968 +-11.8628835678101 -200.273759365082 +-12.9624729156494 19.3836717605591 +-8.86288356781006 -200.273759365082 +-11.9624729156494 19.3836717605591 +2.130126953125 -200.727624893188 +-0.969462394714355 18.9298062324524 +56.8961067199707 -201.109405040741 +53.7965173721313 18.5480260848999 +67.8891172409058 -201.563270568848 +64.7895278930664 18.0941605567932 +35.1058874130249 -199.733209609985 +32.0062980651855 19.9242215156555 +46.09889793396 -200.187075138092 +42.9993085861206 19.4703559875488 +101.860333442688 -200.993818283081 +98.7607440948486 18.6636128425598 +112.853343963623 -201.447683811188 +109.753754615784 18.2097473144531 +77.0701141357422 -199.617622852325 +75.9705247879028 20.0398082733154 +80.0701141357422 -199.617622852325 +76.9705247879028 20.0398082733154 +91.0631246566772 -200.071488380432 +87.9635353088379 19.5859427452087 +145.829104423523 -200.453268527985 +142.729515075684 19.2041625976563 +156.822114944458 -200.907134056091 +153.722525596619 18.7502970695496 +124.038885116577 -199.077073097229 +120.939295768738 20.5803580284119 +135.031895637512 -199.530938625336 +131.932306289673 20.1264925003052 +19.7747859954834 -335.053844928741 +17.6682071685791 -115.850279331207 +30.7677965164185 -335.507710456848 +28.8537483215332 -113.479668617249 +-2.0154333114624 -333.677649497986 +-4.1220121383667 -114.474083900452 +8.97757720947266 -334.131515026093 +6.67519664764404 -115.396413803101 +63.7435569763184 -334.513295173645 +61.6369781494141 -115.309729576111 +71.7365674972534 -334.967160701752 +70.6369781494141 -115.309729576111 +74.7365674972534 -334.967160701752 +73.8179750442505 -113.364081859589 +41.9533376693726 -333.137099742889 +39.8467588424683 -113.933534145355 +52.9463481903076 -333.590965270996 +50.643967628479 -114.855864048004 +108.707783699036 -334.397708415985 +106.601204872131 -115.194142818451 +119.700794219971 -334.851573944092 +117.786746025085 -112.823532104492 +86.9175643920898 -333.021512985229 +84.8109855651855 -113.817947387695 +97.9105749130249 -333.475378513336 +95.6081943511963 -114.740277290344 +152.676554679871 -333.857158660889 +150.569975852966 -114.653593063354 +160.669565200806 -334.311024188995 +159.569975852966 -114.653593063354 +130.886335372925 -332.480963230133 +128.779756546021 -113.277397632599 +141.87934589386 -332.93482875824 +139.576965332031 -114.199727535248 +191.237973213196 -201.756735801697 +188.138383865356 17.9006953239441 +202.230983734131 -202.210601329803 +199.131394386292 17.4468297958374 +166.44775390625 -200.380540370941 +165.348164558411 19.2768907546997 +169.44775390625 -200.380540370941 +166.348164558411 19.2768907546997 +180.440764427185 -200.834405899048 +177.341175079346 18.823025226593 +235.206744194031 -201.2161860466 +232.107154846191 18.4412450790405 +246.199754714966 -201.670051574707 +243.100165367126 17.9873795509338 +213.416524887085 -199.839990615845 +210.316935539246 19.8174405097961 +224.40953540802 -200.293856143951 +221.309946060181 19.3635749816895 +280.170970916748 -201.10059928894 +277.071381568909 18.5568318367004 +291.163981437683 -201.554464817047 +288.064392089844 18.1029663085938 +255.380751609802 -199.724403858185 +254.281162261963 19.9330272674561 +258.380751609802 -199.724403858185 +255.281162261963 19.9330272674561 +269.373762130737 -200.178269386292 +266.274172782898 19.4791617393494 +324.139741897583 -200.560049533844 +321.040152549744 19.0973815917969 +335.132752418518 -201.013915061951 +332.033163070679 18.6435160636902 +302.349522590637 -199.183854103088 +299.249933242798 20.4735770225525 +313.342533111572 -199.637719631195 +310.242943763733 20.0197114944458 +198.085423469543 -335.160625934601 +195.978844642639 -115.957060337067 +209.078433990479 -335.614491462708 +207.164385795593 -113.586449623108 +176.295204162598 -333.784430503845 +174.188625335693 -114.580864906311 +187.288214683533 -334.238296031952 +184.985834121704 -115.50319480896 +242.054194450378 -334.620076179504 +239.947615623474 -115.41651058197 +250.047204971313 -335.073941707611 +248.947615623474 -115.41651058197 +253.047204971313 -335.073941707611 +252.128612518311 -113.470862865448 +220.263975143433 -333.243880748749 +218.157396316528 -114.040315151215 +231.256985664368 -333.697746276855 +228.954605102539 -114.962645053864 +287.018421173096 -334.504489421844 +284.911842346191 -115.30092382431 +298.011431694031 -334.958354949951 +296.097383499146 -112.930313110352 +265.22820186615 -333.128293991089 +263.121623039246 -113.924728393555 +276.221212387085 -333.582159519196 +273.918831825256 -114.847058296204 +330.987192153931 -333.963939666748 +328.880613327026 -114.760374069214 +338.980202674866 -334.417805194855 +337.880613327026 -114.760374069214 +341.980202674866 -334.417805194855 +340.054686546326 -115.49156665802 +309.196972846985 -332.587744235992 +307.090394020081 -113.384178638458 +320.18998336792 -333.041609764099 +317.887602806091 -114.306508541107 +368.097044944763 -203.121303081512 +364.997455596924 16.5361280441284 +379.090055465698 -203.575168609619 +375.990466117859 16.0822625160217 +343.306825637817 -201.745107650757 +342.207236289978 17.912323474884 +346.306825637817 -201.745107650757 +343.207236289978 17.912323474884 +357.299836158752 -202.198973178864 +354.200246810913 17.4584579467773 +412.065815925598 -202.580753326416 +408.966226577759 17.0766777992249 +423.058826446533 -203.034618854523 +419.959237098694 16.6228122711182 +390.275596618652 -201.20455789566 +387.176007270813 18.4528732299805 +401.268607139587 -201.658423423767 +398.169017791748 17.9990077018738 +457.030042648315 -202.465166568756 +453.930453300476 17.1922645568848 +468.02305316925 -202.919032096863 +464.923463821411 16.7383990287781 +432.23982334137 -201.088971138 +431.14023399353 18.5684599876404 +435.23982334137 -201.088971138 +432.14023399353 18.5684599876404 +446.232833862305 -201.542836666107 +443.133244514465 18.1145944595337 +500.99881362915 -201.92461681366 +497.899224281311 17.7328143119812 +511.991824150085 -202.378482341766 +508.892234802246 17.2789487838745 +479.208594322205 -200.548421382904 +476.109004974365 19.1090097427368 +490.20160484314 -201.002286911011 +487.1020154953 18.6551442146301 +374.944495201111 -336.525193214417 +372.837916374207 -117.321627616882 +385.937505722046 -336.979058742523 +384.023457527161 -114.951016902924 +353.154275894165 -335.148997783661 +351.047697067261 -115.945432186127 +364.1472864151 -335.602863311768 +361.844905853271 -116.867762088776 +418.913266181946 -335.98464345932 +416.806687355042 -116.781077861786 +426.906276702881 -336.438508987427 +425.806687355042 -116.781077861786 +429.906276702881 -336.438508987427 +428.987684249878 -114.835430145264 +397.123046875 -334.608448028564 +395.016468048096 -115.40488243103 +408.116057395935 -335.062313556671 +405.813676834106 -116.327212333679 +463.877492904663 -335.86905670166 +461.770914077759 -116.665491104126 +474.870503425598 -336.322922229767 +472.956455230713 -114.294880390167 +442.087273597717 -334.492861270905 +439.980694770813 -115.28929567337 +453.080284118652 -334.946726799011 +450.777903556824 -116.211625576019 +507.846263885498 -335.328506946564 +505.739685058594 -116.12494134903 +515.839274406433 -335.78237247467 +514.739685058594 -116.12494134903 +486.056044578552 -333.952311515808 +483.949465751648 -114.748745918274 +497.049055099487 -334.406177043915 +494.746674537659 -115.671075820923 +546.407682418823 -203.228084087372 +543.308093070984 16.429347038269 +557.400692939758 -203.681949615479 +554.301103591919 15.9754815101624 +521.617463111877 -201.851888656616 +520.517873764038 17.8055424690247 +524.617463111877 -201.851888656616 +521.517873764038 17.8055424690247 +535.610473632813 -202.305754184723 +532.510884284973 17.351676940918 +590.376453399658 -202.687534332275 +587.276864051819 16.9698967933655 +601.369463920593 -203.141399860382 +598.269874572754 16.5160312652588 +568.586234092712 -201.31133890152 +565.486644744873 18.3460922241211 +579.579244613647 -201.765204429626 +576.479655265808 17.8922266960144 +635.340680122375 -202.571947574615 +632.241090774536 17.0854835510254 +646.333690643311 -203.025813102722 +643.234101295471 16.6316180229187 +610.55046081543 -201.19575214386 +609.45087146759 18.461678981781 +613.55046081543 -201.19575214386 +610.45087146759 18.461678981781 +624.543471336365 -201.649617671967 +621.443881988525 18.0078134536743 +679.30945110321 -202.031397819519 +676.209861755371 17.6260333061218 +690.302461624146 -202.485263347626 +687.202872276306 17.1721677780151 +657.519231796265 -200.655202388763 +654.419642448425 19.0022287368774 +668.5122423172 -201.10906791687 +665.41265296936 18.5483632087708 +553.255132675171 -336.631974220276 +551.148553848267 -117.428408622742 +564.248143196106 -337.085839748383 +562.334095001221 -115.057797908783 +531.464913368225 -335.25577878952 +529.358334541321 -116.052213191986 +542.45792388916 -335.709644317627 +540.155543327332 -116.974543094635 +597.223903656006 -336.091424465179 +595.117324829102 -116.887858867645 +605.216914176941 -336.545289993286 +604.117324829102 -116.887858867645 +608.216914176941 -336.545289993286 +607.298321723938 -114.942211151123 +575.43368434906 -334.715229034424 +573.327105522156 -115.51166343689 +586.426694869995 -335.169094562531 +584.124314308167 -116.433993339539 +642.188130378723 -335.97583770752 +640.081551551819 -116.772272109985 +653.181140899658 -336.429703235626 +651.267092704773 -114.401661396027 +620.397911071777 -334.599642276764 +618.291332244873 -115.39607667923 +631.390921592712 -335.053507804871 +629.088541030884 -116.318406581879 +686.156901359558 -335.435287952423 +684.050322532654 -116.231722354889 +694.149911880493 -335.88915348053 +693.050322532654 -116.231722354889 +664.366682052612 -334.059092521667 +662.260103225708 -114.855526924133 +675.359692573547 -334.512958049774 +673.057312011719 -115.777856826782 +13.2203102111816 -156.710306167603 +11.8277463912964 18.0074763298035 +24.2133207321167 -157.164171695709 +22.8207569122314 17.5536108016968 +-8.56990909576416 -155.334110736847 +-9.96247291564941 19.3836717605591 +2.4231014251709 -155.787976264954 +1.03053760528564 18.9298062324524 +57.1890811920166 -156.169756412506 +55.7965173721313 18.5480260848999 +68.1820917129517 -156.623621940613 +66.7895278930664 18.0941605567932 +35.3988618850708 -154.79356098175 +34.0062980651855 19.9242215156555 +46.3918724060059 -155.247426509857 +44.9993085861206 19.4703559875488 +102.153307914734 -156.054169654846 +100.760744094849 18.6636128425598 +113.146318435669 -156.508035182953 +111.753754615784 18.2097473144531 +80.3630886077881 -154.677974224091 +78.9705247879028 20.0398082733154 +91.3560991287231 -155.131839752197 +89.9635353088379 19.5859427452087 +146.122078895569 -155.51361989975 +144.729515075684 19.2041625976563 +157.115089416504 -155.967485427856 +155.722525596619 18.7502970695496 +124.331859588623 -154.137424468994 +122.939295768738 20.5803580284119 +135.324870109558 -154.591289997101 +133.932306289673 20.1264925003052 +191.530947685242 -156.817087173462 +190.138383865356 17.9006953239441 +202.523958206177 -157.270952701569 +201.131394386292 17.4468297958374 +169.740728378296 -155.440891742706 +168.348164558411 19.2768907546997 +180.733738899231 -155.894757270813 +179.341175079346 18.823025226593 +235.499718666077 -156.276537418365 +234.107154846191 18.4412450790405 +246.492729187012 -156.730402946472 +245.100165367126 17.9873795509338 +213.709499359131 -154.90034198761 +212.316935539246 19.8174405097961 +224.702509880066 -155.354207515717 +223.309946060181 19.3635749816895 +280.463945388794 -156.160950660706 +279.071381568909 18.5568318367004 +291.456955909729 -156.614816188812 +290.064392089844 18.1029663085938 +258.673726081848 -154.78475522995 +257.281162261963 19.9330272674561 +269.666736602783 -155.238620758057 +268.274172782898 19.4791617393494 +324.432716369629 -155.620400905609 +323.040152549744 19.0973815917969 +335.425726890564 -156.074266433716 +334.033163070679 18.6435160636902 +302.642497062683 -154.244205474854 +301.249933242798 20.4735770225525 +313.635507583618 -154.69807100296 +312.242943763733 20.0197114944458 +18.7747859954834 -335.053844928741 +16.3822221755981 -160.336062431335 +29.7677965164185 -335.507710456848 +27.3752326965332 -160.789927959442 +-3.0154333114624 -333.677649497986 +-5.40799713134766 -158.95986700058 +7.97757720947266 -334.131515026093 +5.5850133895874 -159.413732528687 +62.7435569763184 -334.513295173645 +60.3509931564331 -159.795512676239 +73.7365674972534 -334.967160701752 +71.3440036773682 -160.249378204346 +40.9533376693726 -333.137099742889 +38.5607738494873 -158.419317245483 +51.9463481903076 -333.590965270996 +49.5537843704224 -158.87318277359 +107.707783699036 -334.397708415985 +105.31521987915 -159.679925918579 +118.700794219971 -334.851573944092 +116.308230400085 -160.133791446686 +85.9175643920898 -333.021512985229 +83.5250005722046 -158.303730487823 +96.9105749130249 -333.475378513336 +94.5180110931396 -158.75759601593 +151.676554679871 -333.857158660889 +149.283990859985 -159.139376163483 +163.669565200806 -334.311024188995 +162.27700138092 -159.593241691589 +162.669565200806 -334.311024188995 +160.27700138092 -159.593241691589 +129.886335372925 -332.480963230133 +127.49377155304 -157.763180732727 +140.87934589386 -332.93482875824 +138.486782073975 -158.217046260834 +197.085423469543 -335.160625934601 +194.692859649658 -160.442843437195 +208.078433990479 -335.614491462708 +205.685870170593 -160.896708965302 +175.295204162598 -333.784430503845 +172.902640342712 -159.066648006439 +186.288214683533 -334.238296031952 +183.895650863647 -159.520513534546 +241.054194450378 -334.620076179504 +238.661630630493 -159.902293682098 +252.047204971313 -335.073941707611 +249.654641151428 -160.356159210205 +219.263975143433 -333.243880748749 +216.871411323547 -158.526098251343 +230.256985664368 -333.697746276855 +227.864421844482 -158.979963779449 +286.018421173096 -334.504489421844 +283.62585735321 -159.786706924438 +297.011431694031 -334.958354949951 +294.618867874146 -160.240572452545 +264.22820186615 -333.128293991089 +261.835638046265 -158.410511493683 +275.221212387085 -333.582159519196 +272.8286485672 -158.86437702179 +329.987192153931 -333.963939666748 +327.594628334045 -159.246157169342 +340.980202674866 -334.417805194855 +338.58763885498 -159.700022697449 +308.196972846985 -332.587744235992 +305.8044090271 -157.869961738586 +319.18998336792 -333.041609764099 +316.797419548035 -158.323827266693 +368.390019416809 -158.181654453278 +366.997455596924 16.5361280441284 +379.383029937744 -158.635519981384 +377.990466117859 16.0822625160217 +346.599800109863 -156.805459022522 +345.207236289978 17.912323474884 +357.592810630798 -157.259324550629 +356.200246810913 17.4584579467773 +412.358790397644 -157.641104698181 +410.966226577759 17.0766777992249 +423.351800918579 -158.094970226288 +421.959237098694 16.6228122711182 +390.568571090698 -156.264909267426 +389.176007270813 18.4528732299805 +401.561581611633 -156.718774795532 +400.169017791748 17.9990077018738 +457.323017120361 -157.525517940521 +455.930453300476 17.1922645568848 +468.316027641296 -157.979383468628 +466.923463821411 16.7383990287781 +435.532797813416 -156.149322509766 +434.14023399353 18.5684599876404 +446.525808334351 -156.603188037872 +445.133244514465 18.1145944595337 +501.291788101196 -156.984968185425 +499.899224281311 17.7328143119812 +512.284798622131 -157.438833713531 +510.892234802246 17.2789487838745 +479.50156879425 -155.608772754669 +478.109004974365 19.1090097427368 +490.494579315186 -156.062638282776 +489.1020154953 18.6551442146301 +546.700656890869 -158.288435459137 +545.308093070984 16.429347038269 +557.693667411804 -158.742300987244 +556.301103591919 15.9754815101624 +524.910437583923 -156.912240028381 +523.517873764038 17.8055424690247 +535.903448104858 -157.366105556488 +534.510884284973 17.351676940918 +590.669427871704 -157.747885704041 +589.276864051819 16.9698967933655 +601.662438392639 -158.201751232147 +600.269874572754 16.5160312652588 +568.879208564758 -156.371690273285 +567.486644744873 18.3460922241211 +579.872219085693 -156.825555801392 +578.479655265808 17.8922266960144 +635.633654594421 -157.632298946381 +634.241090774536 17.0854835510254 +646.626665115356 -158.086164474487 +645.234101295471 16.6316180229187 +613.843435287476 -156.256103515625 +612.45087146759 18.461678981781 +624.836445808411 -156.709969043732 +623.443881988525 18.0078134536743 +679.602425575256 -157.091749191284 +678.209861755371 17.6260333061218 +690.595436096191 -157.545614719391 +689.202872276306 17.1721677780151 +657.812206268311 -155.715553760529 +656.419642448425 19.0022287368774 +668.805216789246 -156.169419288635 +667.41265296936 18.5483632087708 +373.944495201111 -336.525193214417 +371.551931381226 -161.80741071701 +384.937505722046 -336.979058742523 +382.544941902161 -162.261276245117 +352.154275894165 -335.148997783661 +349.76171207428 -160.431215286255 +363.1472864151 -335.602863311768 +360.754722595215 -160.885080814362 +417.913266181946 -335.98464345932 +415.520702362061 -161.266860961914 +428.906276702881 -336.438508987427 +426.513712882996 -161.720726490021 +396.123046875 -334.608448028564 +393.730483055115 -159.890665531158 +407.116057395935 -335.062313556671 +404.72349357605 -160.344531059265 +462.877492904663 -335.86905670166 +460.484929084778 -161.151274204254 +473.870503425598 -336.322922229767 +471.477939605713 -161.605139732361 +441.087273597717 -334.492861270905 +438.694709777832 -159.775078773499 +452.080284118652 -334.946726799011 +449.687720298767 -160.228944301605 +506.846263885498 -335.328506946564 +504.453700065613 -160.610724449158 +518.839274406433 -335.78237247467 +517.446710586548 -161.064589977264 +517.839274406433 -335.78237247467 +515.446710586548 -161.064589977264 +485.056044578552 -333.952311515808 +482.663480758667 -159.234529018402 +496.049055099487 -334.406177043915 +493.656491279602 -159.688394546509 +552.255132675171 -336.631974220276 +549.862568855286 -161.91419172287 +563.248143196106 -337.085839748383 +560.855579376221 -162.368057250977 +530.464913368225 -335.25577878952 +528.07234954834 -160.537996292114 +541.45792388916 -335.709644317627 +539.065360069275 -160.991861820221 +596.223903656006 -336.091424465179 +593.831339836121 -161.373641967773 +607.216914176941 -336.545289993286 +604.824350357056 -161.82750749588 +574.43368434906 -334.715229034424 +572.041120529175 -159.997446537018 +585.426694869995 -335.169094562531 +583.03413105011 -160.451312065125 +641.188130378723 -335.97583770752 +638.795566558838 -161.258055210114 +652.181140899658 -336.429703235626 +649.788577079773 -161.71192073822 +619.397911071777 -334.599642276764 +617.005347251892 -159.881859779358 +630.390921592712 -335.053507804871 +627.998357772827 -160.335725307465 +685.156901359558 -335.435287952423 +682.764337539673 -160.717505455017 +697.149911880493 -335.88915348053 +695.757348060608 -161.171370983124 +696.149911880493 -335.88915348053 +693.757348060608 -161.171370983124 +663.366682052612 -334.059092521667 +660.974118232727 -159.341310024261 +674.359692573547 -334.512958049774 +671.967128753662 -159.795175552368 +8.22179889678955 -112.695920467377 +7.82774639129639 18.0074763298035 +19.2148094177246 -113.149785995483 +18.8207569122314 17.5536108016968 +-13.5684204101563 -111.319725036621 +-13.9624729156494 19.3836717605591 +-2.57540988922119 -111.773590564728 +-2.96946239471436 18.9298062324524 +52.1905698776245 -112.15537071228 +51.7965173721313 18.5480260848999 +63.1835803985596 -112.609236240387 +62.7895278930664 18.0941605567932 +30.4003505706787 -110.779175281525 +30.0062980651855 19.9242215156555 +41.3933610916138 -111.233040809631 +40.9993085861206 19.4703559875488 +97.1547966003418 -112.03978395462 +96.7607440948486 18.6636128425598 +108.147807121277 -112.493649482727 +107.753754615784 18.2097473144531 +75.364577293396 -110.663588523865 +74.9705247879028 20.0398082733154 +86.3575878143311 -111.117454051971 +85.9635353088379 19.5859427452087 +141.123567581177 -111.499234199524 +140.729515075684 19.2041625976563 +152.116578102112 -111.953099727631 +151.722525596619 18.7502970695496 +119.333348274231 -110.123038768768 +118.939295768738 20.5803580284119 +130.326358795166 -110.576904296875 +129.932306289673 20.1264925003052 +186.53243637085 -112.802701473236 +186.138383865356 17.9006953239441 +197.525446891785 -113.256567001343 +197.131394386292 17.4468297958374 +164.742217063904 -111.42650604248 +164.348164558411 19.2768907546997 +175.735227584839 -111.880371570587 +175.341175079346 18.823025226593 +230.501207351685 -112.26215171814 +230.107154846191 18.4412450790405 +241.49421787262 -112.716017246246 +241.100165367126 17.9873795509338 +208.710988044739 -110.885956287384 +208.316935539246 19.8174405097961 +219.703998565674 -111.339821815491 +219.309946060181 19.3635749816895 +275.465434074402 -112.14656496048 +275.071381568909 18.5568318367004 +286.458444595337 -112.600430488586 +286.064392089844 18.1029663085938 +253.675214767456 -110.770369529724 +253.281162261963 19.9330272674561 +264.668225288391 -111.224235057831 +264.274172782898 19.4791617393494 +319.434205055237 -111.606015205383 +319.040152549744 19.0973815917969 +330.427215576172 -112.05988073349 +330.033163070679 18.6435160636902 +297.643985748291 -110.229819774628 +297.249933242798 20.4735770225525 +308.636996269226 -110.683685302734 +308.242943763733 20.0197114944458 +22.7747859954834 -335.053844928741 +22.3737440109253 -204.804313659668 +33.7677965164185 -335.507710456848 +33.5592851638794 -202.433702945709 +0.984566688537598 -333.677649497986 +0.583524703979492 -203.428118228912 +11.9775772094727 -334.131515026093 +11.3807334899902 -204.350448131561 +66.7435569763184 -334.513295173645 +66.3425149917603 -204.263763904572 +75.7365674972534 -334.967160701752 +75.3425149917603 -204.263763904572 +44.9533376693726 -333.137099742889 +44.5522956848145 -202.887568473816 +55.9463481903076 -333.590965270996 +55.3495044708252 -203.809898376465 +111.707783699036 -334.397708415985 +111.306741714478 -204.148177146912 +122.700794219971 -334.851573944092 +122.492282867432 -201.777566432953 +89.9175643920898 -333.021512985229 +89.5165224075317 -202.771981716156 +100.910574913025 -333.475378513336 +100.313731193542 -203.694311618805 +155.676554679871 -333.857158660889 +155.275512695313 -203.607627391815 +164.669565200806 -334.311024188995 +164.275512695313 -203.607627391815 +133.886335372925 -332.480963230133 +133.485293388367 -202.23143196106 +144.87934589386 -332.93482875824 +144.282502174377 -203.153761863708 +201.085423469543 -335.160625934601 +200.684381484985 -204.911094665527 +212.078433990479 -335.614491462708 +211.869922637939 -202.540483951569 +179.295204162598 -333.784430503845 +178.89416217804 -203.534899234772 +190.288214683533 -334.238296031952 +189.69137096405 -204.457229137421 +245.054194450378 -334.620076179504 +244.65315246582 -204.370544910431 +254.047204971313 -335.073941707611 +253.65315246582 -204.370544910431 +223.263975143433 -333.243880748749 +222.862933158875 -202.994349479675 +234.256985664368 -333.697746276855 +233.660141944885 -203.916679382324 +290.018421173096 -334.504489421844 +289.617379188538 -204.254958152771 +301.011431694031 -334.958354949951 +300.802920341492 -201.884347438812 +268.22820186615 -333.128293991089 +267.827159881592 -202.878762722015 +279.221212387085 -333.582159519196 +278.624368667603 -203.801092624664 +333.987192153931 -333.963939666748 +333.586150169373 -203.714408397675 +342.980202674866 -334.417805194855 +342.586150169373 -203.714408397675 +312.196972846985 -332.587744235992 +311.795930862427 -202.338212966919 +323.18998336792 -333.041609764099 +322.593139648438 -203.260542869568 +363.391508102417 -114.167268753052 +362.997455596924 16.5361280441284 +374.384518623352 -114.621134281158 +373.990466117859 16.0822625160217 +341.601288795471 -112.791073322296 +341.207236289978 17.912323474884 +352.594299316406 -113.244938850403 +352.200246810913 17.4584579467773 +407.360279083252 -113.626718997955 +406.966226577759 17.0766777992249 +418.353289604187 -114.080584526062 +417.959237098694 16.6228122711182 +385.570059776306 -112.2505235672 +385.176007270813 18.4528732299805 +396.563070297241 -112.704389095306 +396.169017791748 17.9990077018738 +452.324505805969 -113.511132240295 +451.930453300476 17.1922645568848 +463.317516326904 -113.964997768402 +462.923463821411 16.7383990287781 +430.534286499023 -112.13493680954 +430.14023399353 18.5684599876404 +441.527297019959 -112.588802337646 +441.133244514465 18.1145944595337 +496.293276786804 -112.970582485199 +495.899224281311 17.7328143119812 +507.286287307739 -113.424448013306 +506.892234802246 17.2789487838745 +474.503057479858 -111.594387054443 +474.109004974365 19.1090097427368 +485.496068000793 -112.04825258255 +485.1020154953 18.6551442146301 +541.702145576477 -114.274049758911 +541.308093070984 16.429347038269 +552.695156097412 -114.727915287018 +552.301103591919 15.9754815101624 +519.911926269531 -112.897854328156 +519.517873764038 17.8055424690247 +530.904936790466 -113.351719856262 +530.510884284973 17.351676940918 +585.670916557312 -113.733500003815 +585.276864051819 16.9698967933655 +596.663927078247 -114.187365531921 +596.269874572754 16.5160312652588 +563.880697250366 -112.357304573059 +563.486644744873 18.3460922241211 +574.873707771301 -112.811170101166 +574.479655265808 17.8922266960144 +630.635143280029 -113.617913246155 +630.241090774536 17.0854835510254 +641.628153800964 -114.071778774261 +641.234101295471 16.6316180229187 +608.844923973084 -112.241717815399 +608.45087146759 18.461678981781 +619.837934494019 -112.695583343506 +619.443881988525 18.0078134536743 +674.603914260864 -113.077363491058 +674.209861755371 17.6260333061218 +685.596924781799 -113.531229019165 +685.202872276306 17.1721677780151 +652.813694953918 -111.701168060303 +652.419642448425 19.0022287368774 +663.806705474854 -112.155033588409 +663.41265296936 18.5483632087708 +377.944495201111 -336.525193214417 +377.543453216553 -206.275661945343 +388.937505722046 -336.979058742523 +388.728994369507 -203.905051231384 +356.154275894165 -335.148997783661 +355.753233909607 -204.899466514587 +367.1472864151 -335.602863311768 +366.550442695618 -205.821796417236 +421.913266181946 -335.98464345932 +421.512224197388 -205.735112190247 +430.906276702881 -336.438508987427 +430.512224197388 -205.735112190247 +400.123046875 -334.608448028564 +399.722004890442 -204.358916759491 +411.116057395935 -335.062313556671 +410.519213676453 -205.28124666214 +466.877492904663 -335.86905670166 +466.476450920105 -205.619525432587 +477.870503425598 -336.322922229767 +477.661992073059 -203.248914718628 +445.087273597717 -334.492861270905 +444.686231613159 -204.243330001831 +456.080284118652 -334.946726799011 +455.48344039917 -205.16565990448 +510.846263885498 -335.328506946564 +510.44522190094 -205.07897567749 +519.839274406433 -335.78237247467 +519.44522190094 -205.07897567749 +489.056044578552 -333.952311515808 +488.655002593994 -203.702780246735 +500.049055099487 -334.406177043915 +499.452211380005 -204.625110149384 +556.255132675171 -336.631974220276 +555.854090690613 -206.382442951202 +567.248143196106 -337.085839748383 +567.039631843567 -204.011832237244 +534.464913368225 -335.25577878952 +534.063871383667 -205.006247520447 +545.45792388916 -335.709644317627 +544.861080169678 -205.928577423096 +600.223903656006 -336.091424465179 +599.822861671448 -205.841893196106 +609.216914176941 -336.545289993286 +608.822861671448 -205.841893196106 +578.43368434906 -334.715229034424 +578.032642364502 -204.46569776535 +589.426694869995 -335.169094562531 +588.829851150513 -205.388027667999 +645.188130378723 -335.97583770752 +644.787088394165 -205.726306438446 +656.181140899658 -336.429703235626 +655.972629547119 -203.355695724487 +623.397911071777 -334.599642276764 +622.996869087219 -204.35011100769 +634.390921592712 -335.053507804871 +633.79407787323 -205.272440910339 +689.156901359558 -335.435287952423 +688.755859375 -205.18575668335 +698.149911880493 -335.88915348053 +697.755859375 -205.18575668335 +667.366682052612 -334.059092521667 +666.965640068054 -203.809561252594 +678.359692573547 -334.512958049774 +677.762848854065 -204.731891155243 +-16.5684204101563 -111.319725036621 +-16.9639616012573 -24.6307139396667 +72.364577293396 -110.663588523865 +71.9690361022949 -23.9745774269104 +161.742217063904 -111.42650604248 +161.346675872803 -24.7374949455261 +250.675214767456 -110.770369529724 +250.279673576355 -24.0813584327698 +-10.0139446258545 -289.66326379776 +-10.4094858169556 -202.974252700806 +78.9190530776978 -289.007127285004 +78.5235118865967 -202.318116188049 +168.296692848206 -289.770044803619 +167.901151657104 -203.081033706665 +257.229690551758 -289.113908290863 +256.834149360657 -202.424897193909 +338.601288795471 -112.791073322296 +338.20574760437 -26.1020622253418 +427.534286499023 -112.13493680954 +427.138745307922 -25.4459257125854 +516.911926269531 -112.897854328156 +516.51638507843 -26.2088432312012 +605.844923973084 -112.241717815399 +605.449382781982 -25.5527067184448 +345.155764579773 -291.134612083435 +344.760223388672 -204.445600986481 +434.088762283325 -290.478475570679 +433.693221092224 -203.789464473724 +523.466402053833 -291.241393089294 +523.070860862732 -204.55238199234 +612.399399757385 -290.585256576538 +612.003858566284 -203.896245479584 +6.37285995483398 -23.3064160346985 +5.82774639129639 18.0074763298035 +17.365870475769 -23.7602815628052 +16.8207569122314 17.5536108016968 +-18.4173593521118 -21.9302206039429 +-18.9624729156494 19.3836717605591 +-15.4173593521118 -21.9302206039429 +-15.9624729156494 19.3836717605591 +-4.42434883117676 -22.3840861320496 +-4.96946239471436 18.9298062324524 +50.3416309356689 -22.7658662796021 +49.7965173721313 18.5480260848999 +61.334641456604 -23.2197318077087 +60.7895278930664 18.0941605567932 +28.5514116287231 -21.3896708488464 +28.0062980651855 19.9242215156555 +39.5444221496582 -21.8435363769531 +38.9993085861206 19.4703559875488 +95.3058576583862 -22.6502795219421 +94.7607440948486 18.6636128425598 +106.298868179321 -23.1041450500488 +105.753754615784 18.2097473144531 +70.5156383514404 -21.2740840911865 +69.9705247879028 20.0398082733154 +73.5156383514404 -21.2740840911865 +72.9705247879028 20.0398082733154 +84.5086488723755 -21.7279496192932 +83.9635353088379 19.5859427452087 +139.274628639221 -22.1097297668457 +138.729515075684 19.2041625976563 +150.267639160156 -22.5635952949524 +149.722525596619 18.7502970695496 +117.484409332275 -20.7335343360901 +116.939295768738 20.5803580284119 +128.47741985321 -21.1873998641968 +127.932306289673 20.1264925003052 +-14.5699090957642 -155.334110736847 +-15.1150226593018 -114.020218372345 +184.683497428894 -23.4131970405579 +184.138383865356 17.9006953239441 +195.676507949829 -23.8670625686646 +195.131394386292 17.4468297958374 +159.893278121948 -22.0370016098022 +159.348164558411 19.2768907546997 +162.893278121948 -22.0370016098022 +162.348164558411 19.2768907546997 +173.886288642883 -22.4908671379089 +173.341175079346 18.823025226593 +228.652268409729 -22.8726472854614 +228.107154846191 18.4412450790405 +239.645278930664 -23.3265128135681 +239.100165367126 17.9873795509338 +206.862049102783 -21.4964518547058 +206.316935539246 19.8174405097961 +217.855059623718 -21.9503173828125 +217.309946060181 19.3635749816895 +273.616495132446 -22.7570605278015 +273.071381568909 18.5568318367004 +284.609505653381 -23.2109260559082 +284.064392089844 18.1029663085938 +248.8262758255 -21.3808650970459 +248.281162261963 19.9330272674561 +251.8262758255 -21.3808650970459 +251.281162261963 19.9330272674561 +262.819286346436 -21.8347306251526 +262.274172782898 19.4791617393494 +317.585266113281 -22.2165107727051 +317.040152549744 19.0973815917969 +328.578276634216 -22.6703763008118 +328.033163070679 18.6435160636902 +295.795046806335 -20.8403153419495 +295.249933242798 20.4735770225525 +306.788057327271 -21.2941808700562 +306.242943763733 20.0197114944458 +163.740728378296 -155.440891742706 +163.195614814758 -114.126999378204 +13.7747859954834 -335.053844928741 +13.2296724319458 -293.73995256424 +24.7677965164185 -335.507710456848 +24.2226829528809 -294.193818092346 +-8.0154333114624 -333.677649497986 +-8.560546875 -292.363757133484 +2.97757720947266 -334.131515026093 +2.43246364593506 -292.817622661591 +57.7435569763184 -334.513295173645 +57.1984434127808 -293.199402809143 +68.7365674972534 -334.967160701752 +68.1914539337158 -293.65326833725 +77.7365674972534 -334.967160701752 +77.1914539337158 -293.65326833725 +35.9533376693726 -333.137099742889 +35.408224105835 -291.823207378387 +46.9463481903076 -333.590965270996 +46.40123462677 -292.277072906494 +102.707783699036 -334.397708415985 +102.162670135498 -293.083816051483 +113.700794219971 -334.851573944092 +113.155680656433 -293.53768157959 +80.9175643920898 -333.021512985229 +80.3724508285522 -291.707620620728 +91.9105749130249 -333.475378513336 +91.3654613494873 -292.161486148834 +146.676554679871 -333.857158660889 +146.131441116333 -292.543266296387 +157.669565200806 -334.311024188995 +157.124451637268 -292.997131824493 +166.669565200806 -334.311024188995 +166.124451637268 -292.997131824493 +124.886335372925 -332.480963230133 +124.341221809387 -291.167070865631 +135.87934589386 -332.93482875824 +135.334232330322 -291.620936393738 +192.085423469543 -335.160625934601 +191.540309906006 -293.846733570099 +203.078433990479 -335.614491462708 +202.533320426941 -294.300599098206 +170.295204162598 -333.784430503845 +169.75009059906 -292.470538139343 +181.288214683533 -334.238296031952 +180.743101119995 -292.92440366745 +236.054194450378 -334.620076179504 +235.509080886841 -293.306183815002 +247.047204971313 -335.073941707611 +246.502091407776 -293.760049343109 +256.047204971313 -335.073941707611 +255.502091407776 -293.760049343109 +214.263975143433 -333.243880748749 +213.718861579895 -291.929988384247 +225.256985664368 -333.697746276855 +224.71187210083 -292.383853912354 +281.018421173096 -334.504489421844 +280.473307609558 -293.190597057343 +292.011431694031 -334.958354949951 +291.466318130493 -293.644462585449 +259.22820186615 -333.128293991089 +258.683088302612 -291.814401626587 +270.221212387085 -333.582159519196 +269.676098823547 -292.268267154694 +324.987192153931 -333.963939666748 +324.442078590393 -292.650047302246 +335.980202674866 -334.417805194855 +335.435089111328 -293.103912830353 +344.980202674866 -334.417805194855 +344.435089111328 -293.103912830353 +303.196972846985 -332.587744235992 +302.651859283447 -291.27385187149 +314.18998336792 -333.041609764099 +313.644869804382 -291.727717399597 +361.542569160461 -24.7777643203735 +360.997455596924 16.5361280441284 +372.535579681396 -25.2316298484802 +371.990466117859 16.0822625160217 +336.752349853516 -23.4015688896179 +336.207236289978 17.912323474884 +339.752349853516 -23.4015688896179 +339.207236289978 17.912323474884 +350.745360374451 -23.8554344177246 +350.200246810913 17.4584579467773 +405.511340141296 -24.2372145652771 +404.966226577759 17.0766777992249 +416.504350662231 -24.6910800933838 +415.959237098694 16.6228122711182 +383.721120834351 -22.8610191345215 +383.176007270813 18.4528732299805 +394.714131355286 -23.3148846626282 +394.169017791748 17.9990077018738 +450.475566864014 -24.1216278076172 +449.930453300476 17.1922645568848 +461.468577384949 -24.5754933357239 +460.923463821411 16.7383990287781 +425.685347557068 -22.7454323768616 +425.14023399353 18.5684599876404 +428.685347557068 -22.7454323768616 +428.14023399353 18.5684599876404 +439.678358078003 -23.1992979049683 +439.133244514465 18.1145944595337 +494.444337844849 -23.5810780525208 +493.899224281311 17.7328143119812 +505.437348365784 -24.0349435806274 +504.892234802246 17.2789487838745 +472.654118537903 -22.2048826217651 +472.109004974365 19.1090097427368 +483.647129058838 -22.6587481498718 +483.1020154953 18.6551442146301 +539.853206634521 -24.8845453262329 +539.308093070984 16.429347038269 +550.846217155457 -25.3384108543396 +550.301103591919 15.9754815101624 +515.062987327576 -23.5083498954773 +514.517873764038 17.8055424690247 +518.062987327576 -23.5083498954773 +517.517873764038 17.8055424690247 +529.055997848511 -23.962215423584 +528.510884284973 17.351676940918 +583.821977615356 -24.3439955711365 +583.276864051819 16.9698967933655 +594.814988136292 -24.7978610992432 +594.269874572754 16.5160312652588 +562.031758308411 -22.9678001403809 +561.486644744873 18.3460922241211 +573.024768829346 -23.4216656684875 +572.479655265808 17.8922266960144 +628.786204338074 -24.2284088134766 +628.241090774536 17.0854835510254 +639.779214859009 -24.6822743415833 +639.234101295471 16.6316180229187 +603.995985031128 -22.8522133827209 +603.45087146759 18.461678981781 +606.995985031128 -22.8522133827209 +606.45087146759 18.461678981781 +617.988995552063 -23.3060789108276 +617.443881988525 18.0078134536743 +672.754975318909 -23.6878590583801 +672.209861755371 17.6260333061218 +683.747985839844 -24.1417245864868 +683.202872276306 17.1721677780151 +650.964756011963 -22.3116636276245 +650.419642448425 19.0022287368774 +661.957766532898 -22.7655291557312 +661.41265296936 18.5483632087708 +518.910437583923 -156.912240028381 +518.365324020386 -115.598347663879 +368.944495201111 -336.525193214417 +368.399381637573 -295.211300849915 +379.937505722046 -336.979058742523 +379.392392158508 -295.665166378021 +347.154275894165 -335.148997783661 +346.609162330627 -293.835105419159 +358.1472864151 -335.602863311768 +357.602172851563 -294.288970947266 +412.913266181946 -335.98464345932 +412.368152618408 -294.670751094818 +423.906276702881 -336.438508987427 +423.361163139343 -295.124616622925 +432.906276702881 -336.438508987427 +432.361163139343 -295.124616622925 +391.123046875 -334.608448028564 +390.577933311462 -293.294555664063 +402.116057395935 -335.062313556671 +401.570943832397 -293.748421192169 +457.877492904663 -335.86905670166 +457.332379341125 -294.555164337158 +468.870503425598 -336.322922229767 +468.325389862061 -295.009029865265 +436.087273597717 -334.492861270905 +435.54216003418 -293.178968906403 +447.080284118652 -334.946726799011 +446.535170555115 -293.632834434509 +501.846263885498 -335.328506946564 +501.30115032196 -294.014614582062 +512.839274406433 -335.78237247467 +512.294160842896 -294.468480110168 +521.839274406433 -335.78237247467 +521.294160842896 -294.468480110168 +480.056044578552 -333.952311515808 +479.510931015015 -292.638419151306 +491.049055099487 -334.406177043915 +490.50394153595 -293.092284679413 +547.255132675171 -336.631974220276 +546.710019111633 -295.318081855774 +558.248143196106 -337.085839748383 +557.703029632568 -295.771947383881 +525.464913368225 -335.25577878952 +524.919799804688 -293.941886425018 +536.45792388916 -335.709644317627 +535.912810325623 -294.395751953125 +591.223903656006 -336.091424465179 +590.678790092468 -294.777532100677 +602.216914176941 -336.545289993286 +601.671800613403 -295.231397628784 +611.216914176941 -336.545289993286 +610.671800613403 -295.231397628784 +569.43368434906 -334.715229034424 +568.888570785522 -293.401336669922 +580.426694869995 -335.169094562531 +579.881581306458 -293.855202198029 +636.188130378723 -335.97583770752 +635.643016815186 -294.661945343018 +647.181140899658 -336.429703235626 +646.636027336121 -295.115810871124 +614.397911071777 -334.599642276764 +613.85279750824 -293.285749912262 +625.390921592712 -335.053507804871 +624.845808029175 -293.739615440369 +680.156901359558 -335.435287952423 +679.611787796021 -294.121395587921 +691.149911880493 -335.88915348053 +690.604798316956 -294.575261116028 +700.149911880493 -335.88915348053 +699.604798316956 -294.575261116028 +658.366682052612 -334.059092521667 +657.821568489075 -292.745200157166 +669.359692573547 -334.512958049774 +668.81457901001 -293.199065685272 +12 +19.7747859954834 -335.053844928741 +17.6682071685791 -115.850279331207 +18.7747859954834 -335.053844928741 +16.3822221755981 -160.336062431335 +22.7747859954834 -335.053844928741 +22.3737440109253 -204.804313659668 +16.7747859954834 -335.053844928741 +15.8192682266235 -26.4607748985291 +24.7677965164185 -335.507710456848 +24.2226829528809 -294.193818092346 +24.7677965164185 -335.507710456848 +24.2226829528809 -294.193818092346 +20.7747859954834 -335.053844928741