/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ package org.voltdb.types; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.voltdb.types.GeographyValue.XYZPoint; import junit.framework.TestCase; public class TestGeographyValue extends TestCase { private void assertEquals(GeographyPointValue expected, GeographyPointValue actual, double epsilon) { String message = String.format("Expected: %s, actual %s", expected, actual); assertTrue(message, Math.abs(expected.getLongitude() - actual.getLongitude()) < epsilon && Math.abs(expected.getLatitude() - actual.getLatitude()) < epsilon); } private static final String WKT = "polygon((0 0, 1 0, 1 1, 0 1, 0 0), (0.1 0.1, 0.1 0.9, 0.9 0.9, 0.9 0.1, 0.1 0.1))"; private void printOneXYZPointForDoc(GeographyPointValue pt) { XYZPoint xyzpt = XYZPoint.fromGeographyPointValue(pt); System.out.printf(" <tr><td>%f</td><td>%f</td><td>%f</td><td>%f</td><td>%f</td></tr>\n", pt.getLongitude(), pt.getLatitude(), xyzpt.x(), xyzpt.y(), xyzpt.z()); } /* * This just prints out the XYZPoint coordinates for a polygon. We use * this in generating the documentation. Run this as a junit test and the * XYZPoint coordinates will be printed on the console as the body of an * HTML table. Note that we don't reverse any order here, as we don't really * need to for this application. */ public void notestXYZPointForDoc() { GeographyValue poly = GeographyValue.fromWKT(WKT); List<List<GeographyPointValue>> rings = poly.getRings(); for (List<GeographyPointValue> oneRing : rings) { for (int idx = 0; idx != oneRing.size()-1; idx += 1) { printOneXYZPointForDoc(oneRing.get(idx)); } } } private void printOneGVRowMessageForDoc(String message) { System.out.printf(" <tr><td colspan=\"5\">%s</td></tr>\n", message); } private int printOneGVRowForDoc(int bytePos, int length, String value, String type, String meaning) { System.out.printf(" <tr><td>%d</td><td>%d</td><td>%s</td><td>%s</td><td>%s</td></tr>\n", bytePos, length, value, type, meaning); try { return meaning.getBytes("UTF-16BE").length; } catch (UnsupportedEncodingException ex) { return 0; } } private int printOneGVRowForDoc(int bytePos, byte value, String meaning) { printOneGVRowForDoc(bytePos, 1, "byte", String.format("%x", value), meaning); return 1; } private int printOneGVRowForDoc(int bytePos, int value, String meaning) { printOneGVRowForDoc(bytePos, 4, "32 bit int", String.format("%d", value), meaning); return 4; } private int printOneGVRowForDoc(int bytePos, double value, String meaning) { printOneGVRowForDoc(bytePos, 8, "double", String.format("%f", value), meaning); return 8; } private int printOneGVRowOfZerosForDoc(int pos, int length, String meaning) { printOneGVRowForDoc(pos, length, "0", "blob of zeros", meaning); return length; } /* * Print the wire protocol representation for a polygon. This is used to generate * the wire protocol documentation. */ public void notestGeographyValueForDoc() { GeographyValue poly = GeographyValue.fromWKT(WKT); ByteBuffer buf = ByteBuffer.allocate(poly.getLengthInBytes()); poly.flattenToBuffer(buf); buf.position(0); int pos = 0; pos += printOneGVRowForDoc(pos, buf.get(pos), "IsValid. Initially zero (0)"); pos += printOneGVRowForDoc(pos, buf.get(pos), "Internal. Initially one (1)"); pos += printOneGVRowForDoc(pos, buf.get(pos), "Polygon has holes."); int nrings = buf.getInt(pos); pos += printOneGVRowForDoc(pos, nrings, "Number of Rings"); printOneGVRowMessageForDoc("Vertices follow here."); for (int ringNo = 0; ringNo < nrings; ringNo += 1) { printOneGVRowMessageForDoc(String.format("Ring %d", ringNo + 1)); pos += printOneGVRowForDoc(pos, buf.get(pos), "Is initialized. Initially zero (0)"); int numVerts = buf.getInt(pos); pos += printOneGVRowForDoc(pos, numVerts, String.format("Number Vertices in ring %d", ringNo + 1)); for (int vertNo = 0; vertNo < numVerts; vertNo += 1) { pos += printOneGVRowForDoc(pos, buf.getDouble(pos), String.format("X Coordinate for ring %d, vertex %d", ringNo + 1, vertNo + 1)); pos += printOneGVRowForDoc(pos, buf.getDouble(pos), String.format("Y Coordinate for ring %d, vertex %d", ringNo + 1, vertNo + 1)); pos += printOneGVRowForDoc(pos, buf.getDouble(pos), String.format("Z Coordinate for ring %d, vertex %d", ringNo + 1, vertNo + 1)); } pos += printOneGVRowOfZerosForDoc(pos, 38, "Internal plus the bounding box of the ring. Initially zero (0)."); } pos += printOneGVRowOfZerosForDoc(pos, 33, "Internal fields plus the bounding box of the polygon. Initially zero(0)."); } public void testGeographyValuePositive() { GeographyValue geog; GeographyValue rtGeog; // The Bermuda Triangle List<GeographyPointValue> outerLoop = Arrays.asList( new GeographyPointValue(-64.751, 32.305), new GeographyPointValue(-80.437, 25.244), new GeographyPointValue(-66.371, 18.476), new GeographyPointValue(-64.751, 32.305)); // A triangular hole List<GeographyPointValue> innerLoop = Arrays.asList( new GeographyPointValue(-68.874, 28.066), new GeographyPointValue(-68.855, 25.361), new GeographyPointValue(-73.381, 28.376), new GeographyPointValue(-68.874, 28.066)); List<List<GeographyPointValue>> expectedLol = Arrays.asList(outerLoop, innerLoop); geog = new GeographyValue(expectedLol); assertEquals("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066, -68.855 25.361, -73.381 28.376, -68.874 28.066))", geog.toString()); // round trip geog = new GeographyValue("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066,-68.855 25.361, -73.381 28.376, -68.874 28.066))"); assertEquals("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066, -68.855 25.361, -73.381 28.376, -68.874 28.066))", geog.toString()); String rtStr = "POLYGON ((0.0 20.0, -17.320508076 -10.0, 17.320508076 -10.0, 0.0 20.0))"; rtGeog = new GeographyValue(rtStr); assertEquals(rtStr, rtGeog.toString()); // serialize this. ByteBuffer buf = ByteBuffer.allocate(geog.getLengthInBytes()); geog.flattenToBuffer(buf); assertEquals(270, buf.position()); buf.position(0); GeographyValue newGeog = GeographyValue.unflattenFromBuffer(buf); assertEquals("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066, -68.855 25.361, -73.381 28.376, -68.874 28.066))", newGeog.toString()); assertEquals(270, buf.position()); // Try the absolute version of unflattening // Note that the hole's coordinates have been reversed again. buf.position(77); newGeog = GeographyValue.unflattenFromBuffer(buf, 0); assertEquals("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066, -68.855 25.361, -73.381 28.376, -68.874 28.066))", newGeog.toString()); assertEquals(77, buf.position()); // Try getting the loops as loops, and see if we get what we put in. geog = new GeographyValue(expectedLol); final double EPSILON = 1.0e-13; List<List<GeographyPointValue>> lol = geog.getRings(); assertEquals(expectedLol.size(), lol.size()); for (int oidx = 0; oidx < lol.size(); oidx += 1) { List<GeographyPointValue> loop = lol.get(oidx); List<GeographyPointValue> expectedLoop = expectedLol.get(oidx); assertEquals(expectedLoop.size(), loop.size()); for (int iidx = 0; iidx < loop.size(); iidx += 1) { GeographyPointValue expected = expectedLoop.get(iidx); GeographyPointValue actual = loop.get(iidx); assertEquals(expected, actual, EPSILON); } } } // // Test GeographyValue objects which extend over the // discontinuities between -180 and 180, and the poles. // public void testGeographyValueOverDiscontinuities() { String geoWKT = "POLYGON ((160.0 40.0, -160.0 40.0, -160.0 60.0, 160.0 60.0, 160.0 40.0))"; GeographyValue disPoly = GeographyValue.fromWKT(geoWKT); assertEquals(geoWKT, disPoly.toString()); GeographyPointValue offset = new GeographyPointValue(10.0, -10.0); GeographyValue disPolyOver = disPoly.add(offset); String geoWKTMoved = "POLYGON ((170.0 30.0, -150.0 30.0, -150.0 50.0, 170.0 50.0, 170.0 30.0))"; assertEquals(geoWKTMoved, disPolyOver.toString()); } public void testGeographyValueNegativeCases() { List<GeographyPointValue> outerLoop = new ArrayList<GeographyPointValue>(); outerLoop.add(new GeographyPointValue(-64.751, 32.305)); outerLoop.add(new GeographyPointValue(-80.437, 25.244)); outerLoop.add(new GeographyPointValue(-66.371, 18.476)); outerLoop.add(new GeographyPointValue(-76.751, 20.305)); outerLoop.add(new GeographyPointValue(-64.751, 32.305)); GeographyValue geoValue; // start with valid loop geoValue = new GeographyValue(Arrays.asList(outerLoop)); assertEquals("POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -76.751 20.305, -64.751 32.305))", geoValue.toString()); Exception exception = null; // first and last vertex are not equal in the loop outerLoop.remove(outerLoop.size() - 1); try { geoValue = new GeographyValue(Arrays.asList(outerLoop)); } catch (IllegalArgumentException illegalArgs) { exception = illegalArgs; assertTrue(exception.getMessage().contains("closing points of ring are not equal")); } finally { assertNotNull(exception); } // loop has less than 4 vertex exception = null; outerLoop.remove(outerLoop.size() - 1); try { geoValue = new GeographyValue(Arrays.asList(outerLoop)); } catch (IllegalArgumentException illegalArgs) { exception = illegalArgs; assertTrue(exception.getMessage().contains("a polygon ring must contain at least 4 points " + "(including repeated closing vertex")); } finally { assertNotNull(exception); } // loop is empty outerLoop.clear(); try { geoValue = new GeographyValue(Arrays.asList(outerLoop)); } catch (IllegalArgumentException illegalArgs) { exception = illegalArgs; assertTrue(exception.getMessage().contains("a polygon ring must contain at least 4 points " + "(including repeated closing vertex")); } finally { assertNotNull(exception); } } private static String canonicalizeWkt(String wkt) { return (new GeographyValue(wkt)).toString(); } public void testWktParsingPositive() { // Parsing is case-insensitive String expected = "POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305))"; assertEquals(expected, canonicalizeWkt("Polygon((-64.751 32.305,-80.437 25.244,-66.371 18.476,-64.751 32.305))")); assertEquals(expected, canonicalizeWkt("polygon((-64.751 32.305,-80.437 25.244,-66.371 18.476,-64.751 32.305))")); assertEquals(expected, canonicalizeWkt("PoLyGoN((-64.751 32.305,-80.437 25.244,-66.371 18.476,-64.751 32.305))")); // Parsing is whitespace-insensitive assertEquals(expected, canonicalizeWkt(" POLYGON ( ( -64.751 32.305 , -80.437 25.244 , -66.371 18.476 , -64.751 32.305 ) ) ")); assertEquals(expected, canonicalizeWkt("\nPOLYGON\n(\n(\n-64.751\n32.305\n,\n-80.437\n25.244\n,\n-66.371\n18.476\n,-64.751\n32.305\n)\n)\n")); assertEquals(expected, canonicalizeWkt("\tPOLYGON\t(\t(\t-64.751\t32.305\t,\t-80.437\t25.244\t,\t-66.371\t18.476\t,\t-64.751\t32.305\t)\t)\t")); // Parsing with more than one loop should work the same. expected = "POLYGON ((-64.751 32.305, -80.437 25.244, -66.371 18.476, -64.751 32.305), " + "(-68.874 28.066, -68.855 25.361, -73.381 28.376, -68.874 28.066))"; assertEquals(expected, canonicalizeWkt("PoLyGoN\t( (\n-64.751\n32.305 , -80.437\t25.244\n, -66.371 18.476,-64.751\t\t\t32.305 ),\t " + "(\n-68.874 28.066,\t -68.855\n25.361\n, -73.381\t28.376,\n\n-68.874\t28.066\t)\n)\t")); } private void assertWktParseError(String error, String wkt) { try { new GeographyValue(wkt); fail("Expected an expection parsing WKT, but it didn't happen"); } catch (IllegalArgumentException iae) { assertTrue("Did not find \n" + " \"" + error + "\"\n" + "in exception message\n" + " \"" + iae.getMessage() + "\"\n", iae.getMessage().contains(error)); } } /** * This tests that the maximum error when we transform a latitude/longitude pair to an * S2, 3-dimensinal point and then back again is less than 1.0e-13. * * We sample the sphere, looking at NUM_PTS X NUM_PTS pairs. At each pair, <m, n>, * we calculate a latitude and longitude, convert the GeographyPointValue with this latitude and * longitude to an XYZPoint, and then back. We then take the maximum error over the * entire sphere. * * Setting NUM_PTS to 2000000 is a very bad idea. * * The error bound is 1.0e-13, which is the value of EPSILON below. We tested 1.0e-14, * but that fails. * * Note that no conversions from text to floating point happen anywhere here, so that * is not a source of precision loss. Only calculation cause these precision losses. * * @throws Exception */ public void testXYZPoint() throws Exception { final double EPSILON = 1.0e-13; // We transform from latitude, longitude to XYZ this many // times for each point. This could be set higher. At about 85, there // is more than 1.0e-13 error. I leave this at 1, because the test // time burden is somewhat severe at higher numbers. final int NUMBER_TRANSFORMS = 1; // This has been tested at 10000, but it takes too long. final int NUM_PTS = 2000; final int MIN_PTS = -(NUM_PTS/2); final int MAX_PTS = (NUM_PTS/2); double max_latitude_error = 0; double max_longitude_error = 0; for (int ycoord = MIN_PTS; ycoord <= MAX_PTS; ycoord += 1) { double latitude = ycoord*(90.0/NUM_PTS); for (int xcoord = MIN_PTS; xcoord <= MAX_PTS; xcoord += 1) { double longitude = xcoord*(180.0/NUM_PTS); GeographyPointValue PT_point = new GeographyPointValue(longitude, latitude); for (int idx = 0; idx < NUMBER_TRANSFORMS; idx += 1) { GeographyValue.XYZPoint xyz_point = GeographyValue.XYZPoint.fromGeographyPointValue(PT_point); PT_point = xyz_point.toGeographyPointValue(); double laterr = Math.abs(latitude-PT_point.getLatitude()); double lngerr = Math.abs(longitude-PT_point.getLongitude()); if (laterr > max_latitude_error) { max_latitude_error = laterr; assertTrue(String.format("Maximum Latitude Error out of range: error=%e >= epsilon = %e, latitude = %f, num_transforms = %d\n", max_latitude_error, EPSILON, latitude, idx), max_latitude_error < EPSILON); } if (lngerr > max_longitude_error) { max_longitude_error = lngerr; assertTrue(String.format("Maximum LongitudeError out of range: error=%e >= epsilon = %e, longitude = %f, num_transforms = %d\n", max_longitude_error, EPSILON, longitude, idx), max_longitude_error < EPSILON); } } } } } public void testWktParsingNegative() { assertWktParseError("expected WKT to start with POLYGON", "NOT_A_POLYGON(...)"); assertWktParseError("expected left parenthesis after POLYGON", "POLYGON []"); assertWktParseError("missing opening parenthesis", "POLYGON(3 3, 4 4, 5 5, 3 3)"); assertWktParseError("missing latitude", "POLYGON ((80 80, 60, 70 70, 90 90))"); assertWktParseError("missing comma", "POLYGON ((80 80 60 60, 70 70, 90 90))"); assertWktParseError("premature end of input", "POLYGON ((80 80, 60 60, 70 70,"); assertWktParseError("missing closing parenthesis", "POLYGON ((80 80, 60 60, 70 70, (30 15, 15 30, 15 45)))"); assertWktParseError("unrecognized token", "POLYGON ((80 80, 60 60, 70 70, 80 80)z)"); assertWktParseError("unrecognized input after WKT", "POLYGON ((80 80, 60 60, 70 70, 90 90, 80 80))blahblah"); assertWktParseError("a polygon must contain at least one ring " + "(with each ring at least 4 points, including repeated closing vertex)", "POLYGON ()"); assertWktParseError("a polygon ring must contain at least 4 points " + "(including repeated closing vertex)", "POLYGON (())"); assertWktParseError("a polygon ring must contain at least 4 points " + "(including repeated closing vertex)", "POLYGON ((10 10, 20 20, 30 30))"); assertWktParseError("closing points of ring are not equal", "POLYGON ((10 10, 20 20, 30 30, 40 40))"); } public void testGetValueDisplaySize() { // Minumum size of serialized polygon is 155, which would be just // three vertices. try { GeographyValue.getValueDisplaySize(154); fail("Expected exception to be thrown"); } catch (IllegalArgumentException iae) { assertEquals(iae.getMessage(), "Cannot compute max display size for a GEOGRAPHY value of size 154 bytes, " + "since minimum allowed size is 155"); } // We need a max 120 characters to represent a triangle assertEquals(120, GeographyValue.getValueDisplaySize(155)); // An extra 10 bytes is not enough to represent another vertex, so // display size is the same. assertEquals(120, GeographyValue.getValueDisplaySize(165)); // We can fit 4 vertices in 179 bytes. assertEquals(120 + 36, GeographyValue.getValueDisplaySize(179)); } }