/* * Copyright (C) 2006, 2013. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 or * version 2 as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package uk.me.parabola.mkgmap.reader.osm.boundary; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.Area; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import uk.me.parabola.imgfmt.ExitException; import uk.me.parabola.imgfmt.FormatException; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.reader.osm.Tags; import uk.me.parabola.mkgmap.reader.osm.Way; import uk.me.parabola.util.EnhancedProperties; import uk.me.parabola.util.Java2DConverter; import uk.me.parabola.util.MultiHashMap; import uk.me.parabola.util.ShapeSplitter; public class BoundaryUtil { private static final Logger log = Logger.getLogger(BoundaryUtil.class); private static final int UNKNOWN_DATA_FORMAT = 0; private static final int RAW_DATA_FORMAT_V1 = 2; private static final int QUADTREE_DATA_FORMAT_V1 = 3; public static final double MIN_DIMENSION = 0.0000001; /** * Calculate the polygons that describe the area. * @param area the Area instance * @param id an id that is used to create meaningful messages, typically a boundary Id * @return A list of BoundaryElements (can be empty) */ public static List<BoundaryElement> splitToElements(Area area, String id) { if (area.isEmpty()) { return Collections.emptyList(); } Area testArea = area; boolean tryAgain = true; while (true){ List<List<Coord>> areaElements = Java2DConverter.areaToShapes(testArea); if (areaElements.isEmpty()) { // this may happen if a boundary overlaps a raster tile in a very small area // so that it is has no dimension log.debug("Area has no dimension. Area:",area.getBounds()); return Collections.emptyList(); } List<BoundaryElement> bElements = new ArrayList<>(); for (List<Coord> singleElement : areaElements) { if (singleElement.size() <= 3) { // need at least 4 items to describe a polygon continue; } boolean outer = Way.clockwise(singleElement); bElements.add(new BoundaryElement(outer, singleElement)); } if (bElements.isEmpty()) { // should not happen because empty polygons should be removed by // the Java2DConverter log.error("Empty boundary elements list after conversion. Area: "+area.getBounds()); return Collections.emptyList(); } // reverse the list because it starts with the inner elements first and // we need the other way round Collections.reverse(bElements); if (bElements.get(0).isOuter()) return bElements; // result is not usable if first element is not outer if (tryAgain == false){ // cannot convert this area log.error(" first element is not outer. "+ bElements.get(0)); //createJavaCodeSnippet(area); //String fname = "bnd_gpx/first_not_outer" + id ; //GpxCreator.createGpx(fname, bElements.get(0).getPoints()); return Collections.emptyList(); } // try converting the area with rounded float values Path2D.Float path = new Path2D.Float(area); testArea = new Area(path); tryAgain = false; } } /** * Wrapper for {@link #loadQuadTrees(String, List, uk.me.parabola.imgfmt.app.Area, EnhancedProperties)} * @param boundaryDirName a directory name or zip file containing the *.bnd file * @param boundaryFileName the *.bnd file name * @return the quadtree or null in case of errors */ public static BoundaryQuadTree loadQuadTree (String boundaryDirName, String boundaryFileName){ Map<String,BoundaryQuadTree> trees = loadQuadTrees (boundaryDirName, Collections.singletonList(boundaryFileName), null, null); return trees.get(boundaryFileName); } /** * Create a BoundaryQuadTree for each file listed in boundaryFileNames. * @param boundaryDirName a directory name or zip file containing the *.bnd file * @param boundaryFileNames the list of *.bnd file names * @param searchBbox null or a bounding box. Data outside of this box is ignored. * @param props null or the properties to be used for the locator * @return a map with quadtrees which can be empty */ public static Map<String,BoundaryQuadTree> loadQuadTrees (String boundaryDirName, List<String> boundaryFileNames, uk.me.parabola.imgfmt.app.Area searchBbox, EnhancedProperties props){ Map<String,BoundaryQuadTree> trees = new HashMap<>(); File boundaryDir = new File(boundaryDirName); BoundaryQuadTree bqt; if (boundaryDir.isDirectory()){ for (String boundaryFileName: boundaryFileNames){ log.info("loading boundary file:", boundaryFileName); // no support for nested directories File boundaryFile = new File(boundaryDir, boundaryFileName); if (boundaryFile.exists()){ try(InputStream stream = new FileInputStream(boundaryFile)){ bqt = BoundaryUtil.loadQuadTreeFromStream(stream, boundaryFileName, searchBbox, props); if (bqt != null) trees.put(boundaryFileName,bqt); } catch (IOException exp) { log.error("Cannot load boundary file " + boundaryFileName + "." + exp); } } } } else if (boundaryDirName.endsWith(".zip")) { String currentFileName = ""; try(ZipFile zipFile = new ZipFile(boundaryDir)){ for (String boundaryFileName : boundaryFileNames){ log.info("loading boundary file:", boundaryFileName); currentFileName = boundaryFileName; // direct access ZipEntry entry = zipFile.getEntry(boundaryFileName); if (entry != null){ try(InputStream stream = zipFile.getInputStream(entry)){ bqt = BoundaryUtil.loadQuadTreeFromStream(stream, boundaryFileName, searchBbox, props); if (bqt != null) trees.put(boundaryFileName,bqt); } } } } catch (IOException exp) { log.error("Cannot load boundary file " + currentFileName + "." + exp); } } else{ log.error("Cannot read " + boundaryDirName); } return trees; } /** * read path iterator info from stream and create Area. * Data is stored with varying length doubles. * @param inpStream the already opened DataInputStream * @return a new Area object or null if not successful * @throws IOException */ public static Area readAreaAsPath(DataInputStream inpStream) throws IOException{ double[] res = new double[2]; Path2D.Double path = new Path2D.Double(PathIterator.WIND_NON_ZERO, 1024); int windingRule = inpStream.readInt(); path.setWindingRule(windingRule); int type = inpStream.readInt(); double minX = Double.MAX_VALUE,maxX = Double.MIN_VALUE; double minY = Double.MAX_VALUE,maxY = Double.MIN_VALUE; while (type >= 0) { switch (type) { case PathIterator.SEG_LINETO: int len = inpStream.readInt(); while(len > 0){ for (int ii = 0; ii < 2; ii++){ double delta = readVarDouble(inpStream); if (delta == BoundarySaver.RESET_DELTA) res[ii] = readVarDouble(inpStream); else res[ii] = res[ii] + delta; } if (res[0] < minX) minX = res[0]; if (res[0] > maxX) maxX = res[0]; if (res[1] < minY) minY = res[1]; if (res[1] > maxY) maxY = res[1]; path.lineTo(res[0],res[1]); --len; } break; case PathIterator.SEG_MOVETO: for (int ii = 0; ii < 2; ii++){ double delta = readVarDouble(inpStream); if (delta == BoundarySaver.RESET_DELTA) res[ii] = readVarDouble(inpStream); else res[ii] = res[ii] + delta; } if (res[0] < minX) minX = res[0]; if (res[0] > maxX) maxX = res[0]; if (res[1] < minY) minY = res[1]; if (res[1] > maxY) maxY = res[1]; path.moveTo(res[0],res[1]); break; case PathIterator.SEG_CLOSE: path.closePath(); break; default: log.error("Unsupported path iterator type " + type + ". This is an mkgmap error."); return null; } type = inpStream.readInt(); } if (type != -1){ log.error("Final type value != -1: " + type); } else{ if (maxX - minX >= MIN_DIMENSION || maxY - minY >= MIN_DIMENSION) return new Area(path); else { // ignore micro area caused by rounding errors in awt area routines } } return null; } /** * Read boundary info saved in RAW_DATA_FORMAT * (written by 1st pass of preparer) * @param inpStream the already opened DataInputStream * @param fname the related file name of the *.bnd file * @param bbox a bounding box. Data outside of this box is ignored. * @return * @throws IOException */ private static List<Boundary> readStreamRawFormat( DataInputStream inpStream, String fname, uk.me.parabola.imgfmt.app.Area bbox) throws IOException { List<Boundary> boundaryList = new ArrayList<>(); try { while (true) { int minLat = inpStream.readInt(); int minLong = inpStream.readInt(); int maxLat = inpStream.readInt(); int maxLong = inpStream.readInt(); if (log.isDebugEnabled()) log.debug("Next boundary. Lat min:",minLat,"max:",maxLat,"Long min:",minLong,"max:",maxLong); uk.me.parabola.imgfmt.app.Area rBbox = new uk.me.parabola.imgfmt.app.Area( minLat, minLong, maxLat, maxLong); int bSize = inpStream.readInt(); log.debug("Size:",bSize); if ( bbox == null || bbox.intersects(rBbox)) { log.debug("Bbox intersects. Load the boundary"); String id = inpStream.readUTF(); Tags tags = new Tags(); int noOfTags = inpStream.readInt(); for (int i = 0; i < noOfTags; i++) { String name = inpStream.readUTF(); String value = inpStream.readUTF(); tags.put(name, value.intern()); } Area area = readAreaAsPath(inpStream); if (area != null) { Boundary boundary = new Boundary(area, tags,id); boundaryList.add(boundary); } else { log.warn("Boundary "+tags+" does not contain any valid area in file " + fname); } } else { log.debug("Bbox does not intersect. Skip",bSize); inpStream.skipBytes(bSize); } } } catch (EOFException exp) { // it's always thrown at the end of the file // log.error("Got EOF at the end of the file"); } return boundaryList; } /** * For a given bounding box, calculate the list of file names that have to be read * @param bbox the bounding box * @return a List with the names */ public static List<String> getRequiredBoundaryFileNames(uk.me.parabola.imgfmt.app.Area bbox) { List<String> names = new ArrayList<>(); for (int latSplit = getSplitBegin(bbox.getMinLat()); latSplit <= BoundaryUtil .getSplitBegin(bbox.getMaxLat()); latSplit += RASTER) { for (int lonSplit = getSplitBegin(bbox.getMinLong()); lonSplit <= BoundaryUtil .getSplitBegin(bbox.getMaxLong()); lonSplit += RASTER) { names.add("bounds_"+ getKey(latSplit, lonSplit) + ".bnd"); } } return names; } /** * Check content of directory or zip file with precompiled boundary data, * dirName Name has to be a directory or a zip file. * @param dirName : path to a directory or a zip file containing the *.bnd files * @return the available *.bnd files in dirName. */ public static List<String> getBoundaryDirContent(String dirName) { List<String> names = new ArrayList<>(); File boundaryDir = new File(dirName); if (!boundaryDir.exists()) log.error("boundary directory/zip does not exist: " + dirName); else{ if (boundaryDir.isDirectory()){ // boundaryDir.list() is much quicker than boundaryDir.listFiles(FileFilter) String[] allNames = boundaryDir.list(); for (String name: allNames){ if (name.endsWith(".bnd")) names.add(name); } } else if (boundaryDir.getName().endsWith(".zip")){ try (ZipFile zipFile = new ZipFile(boundaryDir)){ Enumeration<? extends ZipEntry> entries = zipFile.entries(); boolean isFlat = true; while(entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (entry.isDirectory()){ isFlat = false; } if (entry.getName().endsWith(".bnd")) names.add(entry.getName()); } if (!isFlat){ log.error("boundary zip file contains directories. Files in directories will be ignored." + dirName); } } catch (IOException ioe) { log.error("Cannot read",dirName,ioe); // ioe.printStackTrace(); throw new ExitException("Failed to read required file " + dirName); } } } return names; } public static final int RASTER = 50000; public static int getSplitBegin(int value) { int rem = value % RASTER; if (rem == 0) { return value; } else if (value >= 0) { return value - rem; } else { return value - RASTER - rem; } } public static int getSplitEnd(int value) { int rem = value % RASTER; if (rem == 0) { return value; } else if (value >= 0) { return value + RASTER - rem; } else { return value - rem; } } public static String getKey(int lat, int lon) { return lat + "_" + lon; } /** * Retrieve the bounding box of the given boundary file. * @param boundaryFileName the name of the boundary file * @return the bounding box */ public static uk.me.parabola.imgfmt.app.Area getBbox(String boundaryFileName) { String filename = new String(boundaryFileName); // cut off the extension filename = filename.substring(0,filename.length()-4); String[] fParts = filename.split(Pattern.quote("_")); int lat = Integer.parseInt(fParts[1]); int lon = Integer.parseInt(fParts[2]); return new uk.me.parabola.imgfmt.app.Area(lat, lon, lat+RASTER, lon+RASTER); } /** * Create and fill a BoundaryQuadTree. Read the header of the stream to detect * the proper reading routine for the different supported formats. * @param stream an already opened InputStream * @param fname the file name of the corresponding *.bnd file * @param searchBbox a bounding box or null. If not null, area info outside of this * bounding box is ignored. * @param props properties to be used or null * @return on success it returns a new BoundaryQuadTree, else null * @throws IOException */ private static BoundaryQuadTree loadQuadTreeFromStream(InputStream stream, String fname, uk.me.parabola.imgfmt.app.Area searchBbox, EnhancedProperties props)throws IOException{ BoundaryQuadTree bqt = null; uk.me.parabola.imgfmt.app.Area qtBbox = getBbox(fname); try (DataInputStream inpStream = new DataInputStream(new BufferedInputStream(stream, 1024 * 1024))){ try { // 1st read the mkgmap release the boundary file is created by String mkgmapRel = "?"; String firstId = inpStream.readUTF(); if ("BND".equals(firstId) == false){ throw new FormatException("Unsupported boundary data type "+firstId); } int format = UNKNOWN_DATA_FORMAT; long createTime = inpStream.readLong(); int headerLength = inpStream.readInt(); byte[] header = new byte[headerLength]; int bytesRead = 0; while (bytesRead < headerLength) { int nBytes = inpStream.read(header, bytesRead, headerLength-bytesRead); if (nBytes<0) { throw new IOException("Cannot read header with size "+headerLength); } bytesRead += nBytes; } ByteArrayInputStream rawHeaderStream = new ByteArrayInputStream(header); DataInputStream headerStream =new DataInputStream(rawHeaderStream); String dataFormat = (rawHeaderStream.available() > 0 ? headerStream.readUTF() : "RAW"); int recordVersion = (rawHeaderStream.available() > 0 ? headerStream.readInt() : RAW_DATA_FORMAT_V1); mkgmapRel = (rawHeaderStream.available() > 0 ? headerStream.readUTF() : "unknown"); if ("RAW".equals(dataFormat) && recordVersion == 1) format = RAW_DATA_FORMAT_V1; else if ("QUADTREE".equals(dataFormat) && recordVersion == 1) format = QUADTREE_DATA_FORMAT_V1; if (log.isDebugEnabled()) { log.debug("File created by mkgmap release",mkgmapRel,"at",new Date(createTime)); } switch (format) { case QUADTREE_DATA_FORMAT_V1: bqt = new BoundaryQuadTree(inpStream, qtBbox, searchBbox, props); break; case RAW_DATA_FORMAT_V1: List<Boundary> boundaryList = readStreamRawFormat(inpStream, fname,searchBbox); if (boundaryList == null || boundaryList.isEmpty()) return null; boundaryList = mergePostalCodes(boundaryList); bqt = new BoundaryQuadTree(qtBbox, boundaryList, props); break; default: throw new FormatException("Unsupported boundary file format: "+format); } } catch (EOFException exp) { // it's always thrown at the end of the file // log.error("Got EOF at the end of the file"); } catch (FormatException exp) { log.error("Failed to read boundary file " + fname + " " + exp.getMessage()); } } return bqt; } /** * Merges boundaries with the same postal code. * @param boundaries a list of boundaries * @return the boundary list with postal code areas merged */ private static List<Boundary> mergePostalCodes(List<Boundary> boundaries) { List<Boundary> mergedList = new ArrayList<>(boundaries.size()); MultiHashMap<String, Boundary> equalPostalCodes = new MultiHashMap<>(); for (Boundary boundary : boundaries) { String postalCode = getPostalCode(boundary.getTags()); if (postalCode == null) { // no postal code boundary mergedList.add(boundary); } else { // postal code boundary => merge it later equalPostalCodes.add(postalCode, boundary); } } for (Entry<String, List<Boundary>> postCodeBoundary : equalPostalCodes .entrySet()) { if (postCodeBoundary.getValue().size() == 1) { // nothing to merge mergedList.addAll(postCodeBoundary.getValue()); continue; } // there are more than 2 boundaries with the same post code // => merge them Area newPostCodeArea = new Area(); for (Boundary b : postCodeBoundary.getValue()) { newPostCodeArea.add(b.getArea()); // remove the post code tags from the original boundary if (b.getTags().get("postal_code") != null) { b.getTags().remove("postal_code"); } else if ("postal_code".equals(b.getTags().get("boundary"))) { b.getTags().remove("boundary"); b.getTags().remove("name"); } // check if the boundary contains other boundary information if (isAdministrativeBoundary(b)) { mergedList.add(b); } else { log.info("Boundary", b.getId(), b.getTags(), "contains no more boundary tags. Skipping it."); } } Tags postalCodeTags = new Tags(); postalCodeTags.put("postal_code", postCodeBoundary.getKey()); Boundary postalCodeBoundary = new Boundary(newPostCodeArea, postalCodeTags, "p"+postCodeBoundary.getKey()); log.info("Merged", postCodeBoundary.getValue().size(), "postal code boundaries for postal code", postCodeBoundary.getKey()); mergedList.add(postalCodeBoundary); } return mergedList; } /** * Checks if the given boundary contains tags for an administrative boundary. * @param b a boundary * @return <code>true</code> administrative boundary or postal code; * <code>false</code> element cannot be used for precompiled bounds */ public static boolean isAdministrativeBoundary(Boundary b) { if (b.getId().startsWith("r")) { String type = b.getTags().get("type"); if ("boundary".equals(type) || "multipolygon".equals(type)) { String boundaryVal = b.getTags().get("boundary"); if ("administrative".equals(boundaryVal) == false) return false; // for boundary=administrative the admin_level must be set if (b.getTags().get("admin_level") == null) { return false; } // and a name must be set (check only for a tag containing name Iterator<Entry<String,String>> tagIterator = b.getTags().entryIterator(); while (tagIterator.hasNext()) { Entry<String,String> tag = tagIterator.next(); if (tag.getKey().contains("name")) { return true; } } // does not contain a name tag => do not use it } } else if (b.getId().startsWith("w")) { // the boundary tag must be "administrative" or "postal_code" String boundaryVal = b.getTags().get("boundary"); if ("administrative".equals(boundaryVal)) { // for boundary=administrative the admin_level must be set if (b.getTags().get("admin_level") == null) { return false; } // and a name must be set (check only for a tag containing name) Iterator<Entry<String,String>> tagIterator = b.getTags().entryIterator(); while (tagIterator.hasNext()) { Entry<String,String> tag = tagIterator.next(); if (tag.getKey().contains("name")) { return true; } } // does not contain a name tag => do not use it } } return false; } private static String getPostalCode(Tags tags) { String zip = tags.get("postal_code"); if (zip == null) { if ("postal_code".equals(tags.get("boundary"))){ String name = tags.get("name"); if (name != null) { String[] nameParts = name.split(Pattern.quote(" ")); if (nameParts.length > 0) { zip = nameParts[0].trim(); } } } } return zip; } /** * Helper to ease the reporting of errors. Creates a java code snippet * that can be compiled to have the same area. * @param area the area for which the code should be produced */ public static void createJavaCodeSnippet(Area area) { double[] res = new double[6]; PathIterator pit = area.getPathIterator(null); System.out.println("Path2D.Double path = new Path2D.Double();"); System.out.println("path.setWindingRule(" + pit.getWindingRule() + ");"); while (!pit.isDone()) { int type = pit.currentSegment(res); switch (type) { case PathIterator.SEG_LINETO: System.out.println("path.lineTo(" + res[0] + "d, " + res[1] + "d);"); break; case PathIterator.SEG_MOVETO: System.out.println("path.moveTo(" + res[0] + "d, " + res[1] + "d);"); break; case PathIterator.SEG_CLOSE: System.out.println("path.closePath();"); break; default: log.error("Unsupported path iterator type " + type + ". This is an mkgmap error."); } pit.next(); } System.out.println("Area area = new Area(path);"); } /** * read a varying length double. See BoundarySaver.writeVarDouble(). * @param inp the already opened DataInputStream * @return the extracted double value * @throws IOException */ static double readVarDouble(DataInputStream inp) throws IOException{ byte b; long res = 0; long toShift = 64 - 7; while (((b = inp.readByte()) & 0x80) != 0){ // more bytes will follow res |= (b & 0x7f); toShift -= 7; if (toShift > 0) res <<= 7; } if (toShift > 0){ res |= b; res <<= toShift; } else { // special case: all 64 bits were written, 64 = 9*7 + 1 res <<= 1; res |= 1; } return Double.longBitsToDouble(res); } /** * Raster a given area. This is the non-recursive public method. * @param areaToSplit the area * @return a map with the divided shapes */ public static Map<String, Shape> rasterArea(Area areaToSplit) { return rasterShape(areaToSplit, new HashMap<String, Shape>()); } /** * Raster a given shape. This method calls itself recursively. * @param shapeToSplit the shape * @param splits a map that will contain the resulting shapes * @return a reference to the map */ private static Map<String, Shape> rasterShape(Shape shapeToSplit, Map<String, Shape> splits) { double minX = Double.POSITIVE_INFINITY,minY = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY,maxY = Double.NEGATIVE_INFINITY; PathIterator pit = shapeToSplit.getPathIterator(null); double[] points = new double[512]; double[] res = new double[6]; int num = 0; while (!pit.isDone()) { int type = pit.currentSegment(res); double x = res[0]; double y = res[1]; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; switch (type) { case PathIterator.SEG_LINETO: case PathIterator.SEG_MOVETO: if (num + 2 >= points.length) { points = Arrays.copyOf(points, points.length * 2); } points[num++] = x; points[num++] = y; break; case PathIterator.SEG_CLOSE: int sMinLong = getSplitBegin((int)Math.round(minX)); int sMinLat = getSplitBegin((int)Math.round(minY)); int sMaxLong = getSplitEnd((int)Math.round(maxX)); int sMaxLat = getSplitEnd((int)Math.round(maxY)); int dLon = sMaxLong- sMinLong; int dLat = sMaxLat - sMinLat; Rectangle2D.Double bbox = new Rectangle2D.Double(minX,minY,maxX-minX,maxY-minY); if (dLon > RASTER || dLat > RASTER) { // split into two halves Rectangle clip1,clip2; if (dLon > dLat) { int midLon = getSplitEnd(sMinLong+dLon/2); clip1 = new Rectangle(sMinLong, sMinLat, midLon-sMinLong, dLat); clip2 = new Rectangle(midLon, sMinLat, sMaxLong-midLon, dLat); } else { int midLat = getSplitEnd(sMinLat+dLat/2); clip1 = new Rectangle(sMinLong, sMinLat, dLon, midLat-sMinLat); clip2 = new Rectangle(sMinLong, midLat, dLon, sMaxLat-midLat); } // intersect with the both halves // and split both halves recursively Path2D.Double clippedPath = ShapeSplitter.clipSinglePathWithSutherlandHodgman (points, num, clip1, bbox); if (clippedPath != null) rasterShape(clippedPath, splits); clippedPath = ShapeSplitter.clipSinglePathWithSutherlandHodgman (points, num, clip2, bbox); if (clippedPath != null) rasterShape(clippedPath, splits); } else { String key = getKey(sMinLat, sMinLong); // no need to split, path fits into one tile Path2D.Double segment = ShapeSplitter.pointsToPath2D(points, num); if (segment != null){ Path2D.Double path = (Path2D.Double) splits.get(key); if (path == null) splits.put(key, segment); else path.append(segment, false); } } num = 0; minX = minY = Double.POSITIVE_INFINITY; maxX = maxY = Double.NEGATIVE_INFINITY; break; default: log.error("Unsupported path iterator type " + type + ". This is an mkgmap error."); } pit.next(); } return splits; } }