Subversion Repositories mkgmap

Rev

Rev 3408 | View as "text/plain" | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * 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.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) {
                                        System.err.println("Unhandled exception:");
                                        ioe.printStackTrace();
                                }
                        }
                }
                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;
        }
}