Subversion Repositories mkgmap

Rev

Rev 4798 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2014.
 *
 * 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.filters;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import it.unimi.dsi.fastutil.ints.IntArrayList;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.osmstyle.WrongAngleFixer;
import uk.me.parabola.mkgmap.reader.osm.FakeIdGenerator;
import uk.me.parabola.mkgmap.reader.osm.GType;
import uk.me.parabola.util.Java2DConverter;


/**
 * Merge shapes with same Garmin type and similar attributes if they have common
 * points. This reduces the number of shapes as well as the number of points.
 * @author GerdP
 *
 */

public class ShapeMergeFilter{
        private static final Logger log = Logger.getLogger(ShapeMergeFilter.class);
        private final int resolution;
        private static final ShapeHelper DUP_SHAPE = new ShapeHelper(new ArrayList<>(0));
        private final boolean orderByDecreasingArea;
        private final int maxPoints;
        private final boolean ignoreRes;

        /**
         * Create the shape filter with the given attributes. It will ignore shapes
         * which are not displayed at the given resolution.
         *
         * @param resolution            the resolution
         * @param orderByDecreasingArea if true, only certain shapes are merged
         */

        public ShapeMergeFilter(int resolution, boolean orderByDecreasingArea) {
                ignoreRes = resolution < 0;
                this.resolution = resolution;
                this.orderByDecreasingArea = orderByDecreasingArea;
                if (ignoreRes)
                        maxPoints = Integer.MAX_VALUE;
                else
                        maxPoints = PolygonSplitterFilter.MAX_POINT_IN_ELEMENT;
        }

        /**
         * Merge shapes that are similar and have identical points.
         * @param shapes list of shapes
         * @return list of merged shapes (might be identical to original list)
         */

        public List<MapShape> merge(List<MapShape> shapes) {
                if (shapes.size() <= 1)
                        return shapes;
                List<MapShape> mergedShapes = new ArrayList<>();
                List<MapShape> usableShapes = new ArrayList<>();
                for (MapShape shape: shapes) {
                        if (!ignoreRes && (shape.getMinResolution() > resolution || shape.getMaxResolution() < resolution))
                                continue;
                        if (shape.getPoints().get(0) != shape.getPoints().get(shape.getPoints().size()-1)){
                                // should not happen here
                                log.error("shape is not closed with identical points", shape.getOsmid(),
                                          shape.getPoints().get(0).toOSMURL());
                                mergedShapes.add(shape);
                                continue;
                        }
                        usableShapes.add(shape);
                }
                if (usableShapes.size() < 2) {
                        mergedShapes.addAll(usableShapes);
                        return mergedShapes;
                }
               
                Comparator<MapShape> comparator = new MapShapeComparator(orderByDecreasingArea);
                usableShapes.sort(comparator);
                int p1 = 0;
                MapShape s1 = usableShapes.get(0);
                for (int i = 1; i < usableShapes.size(); i++) {
                        if (comparator.compare(s1, usableShapes.get(i)) == 0)
                                continue;
                        mergeSimilar(usableShapes.subList(p1, i), mergedShapes);
                        s1 = usableShapes.get(i);
                        p1 = i;
                }
                if (p1 < usableShapes.size())
                        mergeSimilar(usableShapes.subList(p1, usableShapes.size()), mergedShapes);
                return mergedShapes;
        }
       
        /**
         * Merge similar shapes.
         * @param similar list of similar shapes
         * @param mergedShapes list to which the shapes are added
         */

        private void mergeSimilar(List<MapShape> similar, List<MapShape> mergedShapes) {
                if (similar.size() == 1) {
                        mergedShapes.addAll(similar);
                        return;
                }
               
                final int partSize = 8192;
                // sorting is meant to reduce the self intersections created by merging
                similar.sort((o1,o2) -> o1.getBounds().getCenter().compareTo(o2.getBounds().getCenter()));
                List<ShapeHelper> list = new ArrayList<>();
                MapShape pattern = similar.get(0);
                for (MapShape ms : similar) {
                        ShapeHelper sh = new ShapeHelper(ms.getPoints());
                        sh.id = ms.getOsmid();
                        list.add(sh);
                }
               
                // if list is very large use partitions to reduce BitSet sizes in called methods
                while (list.size() > partSize) {
                        int oldSize = list.size();
                        List<ShapeHelper> nextList = new ArrayList<>();
                        for (int part = 0; part < list.size(); part += partSize) {
                                List<ShapeHelper> lower = new ArrayList<>(list.subList(part, Math.min(part+partSize, list.size())));
                                tryMerge(pattern, lower);
                                nextList.addAll(lower);
                        }
                        list.clear();
                        list.addAll(nextList);
                        if(list.size() == oldSize)
                                break;
                }
                tryMerge(pattern, list);
                for (ShapeHelper sh : list) {
                        MapShape newShape = pattern.copy();
                        if (sh.getPoints().get(0) != sh.getPoints().get(sh.getPoints().size() - 1)) {
                                throw new MapFailedException("shape is no longer closed");
                        }
                        if (sh.id == 0) {
                                // this shape is the result of a merge
                                List<Coord> optimizedPoints = sh.getPoints();
                                if (!ignoreRes)  {
                                        optimizedPoints = WrongAngleFixer.fixAnglesInShape(optimizedPoints);
                                }
                                if (optimizedPoints.isEmpty())
                                        continue;
                                newShape.setPoints(optimizedPoints);
                                newShape.setOsmid(FakeIdGenerator.makeFakeId());
                        } else {
                                newShape.setPoints(sh.getPoints());
                                newShape.setOsmid(sh.id);
                        }
                        mergedShapes.add(newShape);
                }
        }

        /**
         * Merge ShapeHelpers. Calls itself recursively.
         * @param pattern a MapShape
         * @param similarShapes {@link ShapeHelper} instances created from similar {@link MapShape}.
         * This list is modified if shapes were merged.
         */

        private void tryMerge(MapShape pattern, List<ShapeHelper> similarShapes) {
                if (similarShapes.size() <= 1)
                        return;
                int numCandidates = similarShapes.size();
                List<BitSet> candidates = createMatrix(similarShapes);
                List<ShapeHelper> next = new ArrayList<>();
                boolean merged = false;
                BitSet done = new BitSet();
                BitSet delayed = new BitSet();
               
                List<ShapeHelper> noMerge = new ArrayList<>();
                class SortHelper {
                        final int pos;
                        final BitSet all;
                        public SortHelper(int index, BitSet set) {
                                pos = index;
                                all = set;
                        }
                }
                List<SortHelper> byNum = new ArrayList<>();
                for (int i = 0; i < numCandidates; i++) {
                        byNum.add(new SortHelper(i, candidates.get(i)));
                }
                // sort so that shapes with more neighbours come
                byNum.sort((o1, o2) -> o1.all.cardinality() - o2.all.cardinality());
                for (SortHelper helper : byNum) {
                        final int pos = helper.pos;
                        BitSet all = helper.all;
                        if (all.isEmpty()) {
                                noMerge.add(similarShapes.get(pos));
                                done.set(pos);
                        }
                        if (done.get(pos))
                                continue;
                        if (all.cardinality() == 1) {
                                delayed.set(pos);
                                continue;
                        }
                        all.andNot(done);
                       
                        if (all.isEmpty())
                                continue;
                       
                        IntArrayList byCenter = new IntArrayList();
                        byCenter.add(pos);
                        all.stream().filter(k -> k != pos).forEach(byCenter::add);

                        // try to process the connected shapes so that parts are added to the one in the middle
                        // this should allow to remove spikes as early as possible
                        if (byCenter.size() > 2) {
                                Map<Integer, Coord> centers = new HashMap<>();
                                for (int j : byCenter) {
                                        centers.put(j, Area.getBBox(similarShapes.get(j).points).getCenter()); 
                                }
                                Coord mid = Area.getBBox(new ArrayList<>(centers.values())).getCenter();
                                byCenter.sort((o1, o2) -> Double.compare(centers.get(o1).distance(mid), centers.get(o2).distance(mid)));
                        }                      
                        List<ShapeHelper> result = Collections.emptyList();
                        for (int j : byCenter) {
                                ShapeHelper sh = similarShapes.get(j);
                                int oldSize = result.size();
                                result = addWithConnectedHoles(result, sh, pattern.getType());
                                if (result.size() < oldSize + 1) {
                                        merged = true;
                                        if (log.isDebugEnabled()) {
                                                log.debug("shape with id", sh.id, "was merged", (oldSize + 1 - result.size()),
                                                                " time(s) at resolution", resolution);
                                        }
                                }
                        }
                        // XXX : not exact, there may be other combinations of shapes which can be merged
                        // e.g. merge of shape 1 + 2 may not work but 2 and 3 could still be candidates.
                        done.or(all);
                        next.addAll(result);
                }
               
                delayed.andNot(done);
                if (!delayed.isEmpty()) {
                        delayed.stream().forEach(i -> noMerge.add(similarShapes.get(i)));
                }
                delayed.clear();
                similarShapes.clear();
                similarShapes.addAll(noMerge);
                candidates.clear();
                if (merged) {
                        tryMerge(pattern, next);
                }
               
                // Maybe add final step which calls addWithConnectedHoles for all remaining shapes
                // this will find a few more merges but is still slow for maps with lots of islands
                similarShapes.addAll(next);
        }

        /**
         * Calculate matrix of shapes which share node
         * @param similarShapes
         * @return list of sets with indexes of shared nodes, empty for shapes which cannot be merged
         */

        private List<BitSet> createMatrix(List<ShapeHelper> similarShapes) {
                // abuse highway count to find identical points in different shapes
                similarShapes.forEach(sh -> sh.getPoints().forEach(Coord::resetHighwayCount));
                similarShapes.forEach(sh -> sh.getPoints().forEach(Coord::incHighwayCount));
                // decrement counter for duplicated start/end node
                similarShapes.forEach(sh -> sh.getPoints().get(0).decHighwayCount());
               
                // points with count > 1 are probably shared by different shapes, collect the shapes
                IdentityHashMap<Coord, BitSet> coord2Shape = new IdentityHashMap<>();
                final List<BitSet>candidates = new ArrayList<>(similarShapes.size());
                for (int i = 0; i < similarShapes.size(); i++) {
                        candidates.add(new BitSet());
                }
                for (int i = 0; i < similarShapes.size(); i++) {
                        ShapeHelper sh0 = similarShapes.get(i);
                        List<Coord> sharedPoints = sh0.getPoints().stream().filter(p -> p.getHighwayCount() > 1)
                                        .collect(Collectors.toList());
                        if (sh0.getPoints().get(0).getHighwayCount() > 1)
                                sharedPoints.remove(0);
                        if (sharedPoints.isEmpty() || (sh0.getPoints().size() - sharedPoints.size() > maxPoints)) {
                                // merge will not work
                                continue;
                        }

                        BitSet curr = candidates.get(i);
                        curr.set(i);

                        for (Coord c : sharedPoints) {
                                BitSet set = coord2Shape.get(c);
                                if (set == null) {
                                        set = new BitSet();
                                        coord2Shape.put(c, set);
                                } else {
                                        final int row = i;
                                        set.stream().forEach(j -> candidates.get(j).set(row));
                                        curr.or(set);
                                }
                                set.set(i);
                        }
                }
                return candidates;
        }

        /**
         * Try to merge a shape with one or more of the shapes in the list.
         *  If it cannot be merged, it is added to the list.
         *  Holes in shapes are connected with the outer lines,
         *  so no following routine must use {@link Java2DConverter}
         *  to process these shapes.
         * @param list list of shapes with equal type
         * @param toAdd new shape
         * @param type garmin type of pattern MapShape
         * @return new list of shapes, this might contain fewer (merged) elements
         */

        private List<ShapeHelper> addWithConnectedHoles(List<ShapeHelper> list,
                        final ShapeHelper toAdd, final int type) {
                assert toAdd.getPoints().size() > 3;
                List<ShapeHelper> result = new ArrayList<>(list.size() + 1);
                ShapeHelper shNew = new ShapeHelper(toAdd);
                for (ShapeHelper shOld : list) {
                        ShapeHelper mergeRes = tryMerge(shOld, shNew);
                        if (mergeRes == shOld){
                                result.add(shOld);
                                continue;
                        } else {
                                shNew = mergeRes;
                        }
                        if (shNew == DUP_SHAPE){
                                log.warn("ignoring duplicate shape with id", toAdd.id, "at",  toAdd.getPoints().get(0).toOSMURL(), "with type", GType.formatType(type), "for resolution", resolution);
                                return list; // nothing to do
                        }
                }
                if (shNew != DUP_SHAPE)
                        result.add(shNew);
                if (result.size() > list.size() + 1)
                        log.error("result list size is wrong", list.size(), "->", result.size());
                return result;
        }

        /**
         * Find out if two shapes have common points. If yes, merge them.
         * @param sh1 1st shape1
         * @param sh2 2st shape2
         * @return merged shape or 1st shape if no common point found or {@code dupShape}
         * if both shapes describe the same area.
         */

        private ShapeHelper tryMerge(ShapeHelper sh1, ShapeHelper sh2) {
               
                // both clockwise or both ccw ?
                boolean sameDir = sh1.areaTestVal > 0 && sh2.areaTestVal > 0 || sh1.areaTestVal < 0 && sh2.areaTestVal < 0;
               
                List<Coord> points1, points2;
                if (sh2.getPoints().size()> sh1.getPoints().size()){
                        points1 = sh2.getPoints();
                        points2 = sh1.getPoints();
                } else {
                        points1 = sh1.getPoints();
                        points2 = sh2.getPoints();
                }
                // find all coords that are common in the two shapes
                IntArrayList sh1PositionsToCheck = new IntArrayList();
                IntArrayList sh2PositionsToCheck = new IntArrayList();

                findCommonCoords(points1, points2, sh1PositionsToCheck, sh2PositionsToCheck);          
                if (sh1PositionsToCheck.isEmpty()){
                        return sh1;
                }
                if (sh2PositionsToCheck.size() + 1 >= points2.size()){
                        // all points are identical, might be a duplicate
                        // or a piece that fills a hole
                        if (points1.size() == points2.size() && Math.abs(sh1.areaTestVal) == Math.abs(sh2.areaTestVal)){
                                // it is a duplicate, we can ignore it
                                // XXX this might fail if one of the shapes is self intersecting
                                return DUP_SHAPE;
                        }
                }
                List<Coord> merged = null;
                if (points1.size() + points2.size() - 2 * sh1PositionsToCheck.size() < maxPoints) {
                        merged = findBestMerge(points1, points2, sh1PositionsToCheck, sh2PositionsToCheck, sameDir);
                        if (merged == null)
                                return sh1;
                        if (merged.isEmpty())
                                return DUP_SHAPE;
                        if (merged.get(0) != merged.get(merged.size() - 1))
                                merged = null;
                        else if (merged.size() > maxPoints) {
                                // don't merge because merged polygon would probably be split again
                                log.info("merge rejected: merged shape has too many points " + merged.size());
                                merged = null;
                        }
                }
                if (merged == null)
                        return sh1;
               
                ShapeHelper shm = new ShapeHelper(merged);
                if (Math.abs(shm.areaTestVal) != Math.abs(sh1.areaTestVal) + Math.abs(sh2.areaTestVal)){
                        log.warn("merging shapes skipped for shapes near", points1.get(sh1PositionsToCheck.getInt(0)).toOSMURL(),
                                        "(maybe overlapping shapes?)");
                        return sh1;
                } else {
                        if (log.isInfoEnabled()){
                                log.info("merge of shapes near",points1.get(sh1PositionsToCheck.getInt(0)).toOSMURL(),
                                                "reduces number of points from",(points1.size()+points2.size()),
                                                "to",merged.size());

                        }
                }
                return shm;
        }

        /**
         * Find the common Coord instances and save their positions for both shapes.
         * @param s1 shape 1
         * @param s2 shape 2
         * @param s1PositionsToCheck will contain common positions in shape 1  
         * @param s2PositionsToCheck will contain common positions in shape 2
         */

        private static void findCommonCoords(List<Coord> s1, List<Coord> s2,
                        IntArrayList s1PositionsToCheck,
                        IntArrayList s2PositionsToCheck) {
                Map<Coord, Integer> s2PosMap = new IdentityHashMap<>(s2.size() - 1);
               
                for (int i = 0; i+1 < s1.size(); i++){
                    Coord co = s1.get(i);
                    co.setPartOfShape2(false);
                }
                for (int i = 0; i+1 < s2.size(); i++){
                    Coord co = s2.get(i);
                    co.setPartOfShape2(true);
                    s2PosMap.put(co, i);
                }
               
                int start = 0;
                while(start < s1.size()){
                        Coord co = s1.get(start);
                        if (!co.isPartOfShape2())
                                break;
                        start++;
                }
                int pos = start+1;
                int tested = 0;
                while(true){
                        if (pos+1 >= s1.size())
                                pos = 0;
                        Coord co = s1.get(pos);
                        if (++tested >= s1.size())
                                break;
                        if (co.isPartOfShape2()){
                                s1PositionsToCheck.add(pos);
                                Integer posInSh2 = s2PosMap.get(co);
                                assert posInSh2 != null;
                                s2PositionsToCheck.add(posInSh2.intValue());
                        }
                        pos++;
                }
        }      
       
        /**
         * Merges two shapes if possible and removes the longest common sequence of points
         * @param points1 list of Coord instances that describes the 1st shape
         * @param points2 list of Coord instances that describes the 2nd shape
         * @param sh1PositionsToCheck positions in the 1st shape that are common
         * @param sh2PositionsToCheck positions in the 2nd shape that are common
         * @param sameDir set to true if both shapes are clockwise or both are counter-clockwise
         * @return the shape with the shortest overall length
         */

        private static List<Coord> findBestMerge(List<Coord> points1, List<Coord> points2, IntArrayList sh1PositionsToCheck,
                        IntArrayList sh2PositionsToCheck, boolean sameDir) {
                if (sh1PositionsToCheck.isEmpty())
                        throw new IllegalArgumentException("nothing to merge");
                int s1Size = points1.size();
                int s2Size = points2.size();
                int longestSequence = 0;
                int length = 0;
                int start = -1;
                int n1 = sh1PositionsToCheck.size();
                List<Map.Entry<Integer, Integer>> possibleCombinations = new ArrayList<>();
                assert sh2PositionsToCheck.size() == n1;
                boolean inSequence = false;
                for (int i = 0; i + 1 < n1; i++) {
                        int pred1 = sh1PositionsToCheck.getInt(i);
                        int succ1 = sh1PositionsToCheck.getInt(i + 1);
                        if (Math.abs(succ1 - pred1) == 1 || pred1 + 2 == s1Size && succ1 == 0 || succ1 + 2 == s1Size && pred1 == 0) {
                                // found sequence in s1
                                int pred2 = sh2PositionsToCheck.getInt(i);
                                int succ2 = sh2PositionsToCheck.getInt(i + 1);
                                if (Math.abs(succ2 - pred2) == 1 || pred2 + 2 == s2Size && succ2 == 0 || succ2 + 2 == s2Size && pred2 == 0) {
                                        // found common sequence
                                        if (start < 0)
                                                start = i;
                                        inSequence = true;
                                        length++;
                                } else {
                                        inSequence = false;
                                }
                        } else {
                                inSequence = false;
                        }
                        if (!inSequence) {
                                if (length > longestSequence)
                                        longestSequence = length;
                                possibleCombinations.add(new AbstractMap.SimpleEntry<>(start < 0 ? i : start, length));
                                length = 0;
                                start = -1;
                        }
                }
                if (length > longestSequence)
                        longestSequence = length;
                possibleCombinations.add(new AbstractMap.SimpleEntry<>(start < 0 ? n1 - 1 : start, length));

                // now collect the alternative merge results,
                List<List<Coord>> results = new ArrayList<>();
                for (Map.Entry<Integer, Integer> combi : possibleCombinations) {
                        int len = combi.getValue();
                        if (len < longestSequence)
                                continue;
                        int pos = combi.getKey();
                        List<Coord> merged = combine(points1, points2, sh1PositionsToCheck.getInt(pos + len),
                                        sh2PositionsToCheck.getInt(pos), sameDir, len);
                        if (merged.size() >= 4 && merged.get(0) == merged.get(merged.size() - 1))
                                results.add(merged);
                }

                return filterBest(results);
        }
       
        private static List<Coord> filterBest(List<List<Coord>> allMerged) {
                // if we have more than one result, use the one that produced the shortest line
                // so that connection lines to holes are shortest.
                List<Coord> best = null;
                double bestLen = Double.MAX_VALUE;
                for (List<Coord> merged : allMerged) {
                        if (best == null)
                                best = merged;
                        else {
                                if (bestLen == Double.MAX_VALUE) {
                                        bestLen = getSumLengthSquared(best);
                                }
                                double len = getSumLengthSquared(merged);
                                if (len < bestLen) {
                                        best = merged;
                                        bestLen = len;
                                }
                        }
                }
                return best;
        }
       
        private static List<Coord> combine(List<Coord> points1, List<Coord> points2, final int s1PosInit,
                        final int s2PosInit, boolean sameDir, int lengthOfSequence) {
                int s1Size = points1.size();
                int s2Size = points2.size();
                // merge the shapes with the parts given
                // The remaining points are connected in the direction of the 1st shape.
                int remaining = s1Size + s2Size - 2 * lengthOfSequence - 1;
                if (remaining < 3) {
                        return Collections.emptyList(); // may happen with self-intersecting duplicated shapes
                }
                List<Coord> merged = new ArrayList<>(remaining);
                int s1Pos = s1PosInit;
                for (int i = 0; i < s1Size - lengthOfSequence - 1; i++) {
                        merged.add(points1.get(s1Pos));
                        s1Pos++;
                        if (s1Pos + 1 >= s1Size)
                                s1Pos = 0;
                }
                int s2Pos = s2PosInit;
                int s2Step = sameDir ? 1 : -1;
                for (int i = 0; i < s2Size - lengthOfSequence; i++) {
                        merged.add(points2.get(s2Pos));
                        s2Pos += s2Step;
                        if (s2Pos < 0)
                                s2Pos = s2Size - 2;
                        else if (s2Pos + 1 >= s2Size)
                                s2Pos = 0;
                }
                if (merged.get(0) == merged.get(merged.size()-1))
                        merged = WrongAngleFixer.removeSpikeInShape(merged);
                return merged;
        }
       
        public static double getSumLengthSquared(final List<Coord> points) {
                double length = 0;
                Iterator<Coord> iter = points.iterator();
                Coord p0 = null;
                while (iter.hasNext()) {
                        final Coord p1 = iter.next();
                        if (p0 != null) {
                                length += p0.distanceInHighPrecSquared(p1);
                        }
                        p0 = p1;
                }
                return length;
        }

        private static class ShapeHelper {
                private final List<Coord> points;
                long id;
                long areaTestVal;

                public ShapeHelper(List<Coord> merged) {
                        this.points = merged;
                        areaTestVal = calcAreaSizeTestVal(points);
                }

                public ShapeHelper(ShapeHelper other) {
                        this.points = other.points;
                        this.areaTestVal = other.areaTestVal;
                        this.id = other.id;
                }

                public List<Coord> getPoints() {
                        return points;
                }
        }
       
        public static final long SINGLE_POINT_AREA = 1L << Coord.DELTA_SHIFT * 1L << Coord.DELTA_SHIFT;
       
        /**
         * Calculate the high precision area size test value.  
         * @param points
         * @return area size in high precision map units * 2.
         * The value is >= 0 if the shape is clockwise, else < 0  
         */

        public static long calcAreaSizeTestVal(List<Coord> points){
                if (points.size() < 4)
                        return 0; // straight line cannot enclose an area
                if (!points.get(0).highPrecEquals(points.get(points.size()-1))){
                        log.error("shape is not closed");
                        return 0;
                }
                Iterator<Coord> polyIter = points.iterator();
                Coord c2 = polyIter.next();
                long signedAreaSize = 0;
                while (polyIter.hasNext()) {
                        Coord c1 = c2;
                        c2 = polyIter.next();
                        signedAreaSize += (long) (c2.getHighPrecLon() + c1.getHighPrecLon())
                                        * (c1.getHighPrecLat() - c2.getHighPrecLat());
                }
                if (Math.abs(signedAreaSize) < SINGLE_POINT_AREA && log.isDebugEnabled()) {
                        log.debug("very small shape near", points.get(0).toOSMURL(), "signed area in high prec map units:", signedAreaSize );
                }
                return signedAreaSize;
        }
       
        public static class MapShapeComparator implements Comparator<MapShape> {
               
                private boolean orderByDecreasingArea;
               

                public MapShapeComparator(boolean orderByDecreasingArea) {
                        this.orderByDecreasingArea = orderByDecreasingArea;
                }


                @Override
                public int compare(MapShape o1, MapShape o2) {
                        int d = Integer.compare(o1.getType(), o2.getType());
                        if (d != 0)
                                return d;
                        d = Boolean.compare(o1.isSkipSizeFilter(), o2.isSkipSizeFilter());
                        if (d != 0)
                                return d;
                        // XXX wasClipped() is ignored here, might be needed if later filters need it  
                        if (this.orderByDecreasingArea) {
                                d = Long.compare(o1.getFullArea(), o2.getFullArea());
                                if (d != 0)
                                        return d;
                        }
                        String n1 = o1.getName();
                        String n2 = o2.getName();

                        if (n1 == null) {
                                return (n2 == null) ? 0 : 1;
                        }
                        if (n2 == null)
                                return -1;
                       
                        return n1.compareTo(n2);
                }
        }
}