Subversion Repositories splitter

Rev

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

/*
 * Copyright (c) 2009.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 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.splitter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Used to split an area down into roughly equal sized pieces.
 * They will all have less than a specified number of nodes.
 *
 * @author Steve Ratcliffe
 */

public class SplittableNodeArea implements SplittableArea {
        private final int shift;
        private final int resolution;
        private final Area bounds;
        private SplitIntList coords;
        private int size;

        public SplittableNodeArea(Area bounds, int resolution) {
                this(bounds, new SplitIntList(), resolution);
        }

        public SplittableNodeArea(Area bounds, SplitIntList coords, int resolution) {
                this.bounds = bounds;
                this.coords = coords;
                this.resolution = resolution;
                shift = 24 - resolution;
        }

        @Override
        public Area getBounds() {
                return bounds;
        }

        public void clear() {
                if (coords != null)
                        size = coords.size();
                coords = null;
        }

        private void add(int co) {
                coords.add(co);
        }

        public int getSize() {
                if (coords != null)
                        return coords.size();
                else
                        return size;
        }
        /**
         * Split a single area which would normally be the complete area of the map.
         * We just split areas that are too big into two.  We make a rough determination
         * of the largest dimension and split that way.
         *
         * @param area The original area.
         * @param maxNodes The maximum number of nodes that any area can contain.
         * @return An array of areas.  Each will have less than the specified number of nodes.
         */

        @Override
        public List<Area> split(int maxNodes) {
                if (coords == null || coords.size() == 0)
                        return Collections.emptyList();

                if (coords.size() < maxNodes) {
                        clear();
                        return Collections.singletonList(bounds);
                }

                int height = bounds.getHeight();
                int width = bounds.getWidth();

                // If we've already split the area down to the minimum allowable size, we don't split it further
                boolean minHeight = height <= 2 << shift;
                boolean minWidth = width <= 2 << shift;
                if (minHeight && minWidth) {
                        System.out.println("Area " + bounds + " contains " + Utils.format(getSize())
                                                        + " nodes but is already at the minimum size so can't be split further");
                        return Collections.singletonList(bounds);
                }

                List<Area> results = new ArrayList<Area>();

                // Decide whether to split vertically or horizontally and go ahead with the split
                int width1 = (int) (width * Math.cos(Math.toRadians(Utils.toDegrees(bounds.getMinLat()))));
                int width2 = (int) (width * Math.cos(Math.toRadians(Utils.toDegrees(bounds.getMaxLat()))));
                width = Math.max(width1, width2);
                SplittableNodeArea[] splitResult;
                if (height > width && !minHeight) {
                        splitResult = splitVert();
                } else {
                        splitResult = splitHoriz();
                }
                clear();
                results.addAll(splitResult[0].split(maxNodes));
                results.addAll(splitResult[1].split(maxNodes));
                return results;
        }

        protected SplittableNodeArea[] splitHoriz() {
                int left = bounds.getMinLong();
                int right = bounds.getMaxLong();

          SplitIntList.Iterator it = coords.getIterator();
                int count = 0;
                long total = 0;
                while (it.hasNext()) {
                        int val = it.next();
                        int lon = extractLongitude(val);
                        assert lon >= left && lon <= right : lon;
                        count++;
                        total += lon - left + 1;
                }
                int mid = limit(left, right, total / count);

                Area b1 = new Area(bounds.getMinLat(), bounds.getMinLong(), bounds.getMaxLat(), mid);
                Area b2 = new Area(bounds.getMinLat(), mid, bounds.getMaxLat(), bounds.getMaxLong());

                SplittableNodeArea a1 = new SplittableNodeArea(b1, resolution);
                SplittableNodeArea a2 = new SplittableNodeArea(b2, resolution);

                it = coords.getDeletingIterator();
                while (it.hasNext()) {
                        int co = it.next();
                        if (extractLongitude(co) < mid) {
                                a1.add(co);
                        } else {
                                a2.add(co);
                        }
                }
                return new SplittableNodeArea[]{a1, a2};
        }

        protected SplittableNodeArea[] splitVert() {
                int top = bounds.getMaxLat();
                int bot = bounds.getMinLat();

                SplitIntList.Iterator it = coords.getIterator();
                int count = 0;
                long total = 0;
                while (it.hasNext()) {
                        int val = it.next();
                        int lat = extractLatitude(val);
                        assert lat >= bot && extractLongitude(val) <= top : lat;
                        count++;
                        total += lat - bot;
                }
                int mid = limit(bot, top, total / count);

                Area b1 = new Area(bounds.getMinLat(), bounds.getMinLong(), mid, bounds.getMaxLong());
                Area b2 = new Area(mid, bounds.getMinLong(), bounds.getMaxLat(), bounds.getMaxLong());

                SplittableNodeArea a1 = new SplittableNodeArea(b1, resolution);
                SplittableNodeArea a2 = new SplittableNodeArea(b2, resolution);

                it = coords.getDeletingIterator();
                while (it.hasNext()) {
                        int co = it.next();
                        if (extractLatitude(co) <= mid) {
                                a1.add(co);
                        } else {
                                a2.add(co);
                        }
                }
                return new SplittableNodeArea[]{a1, a2};
        }

        private int extractLatitude(int value) {
                return ((value & 0xffff0000) >> 8);
        }

        private int extractLongitude(int value) {
                int lon = value & 0xffff;
                if ((lon & 0x8000) != 0)
                        lon |= 0xffff0000;
                return lon << 8;
        }

        private int limit(int first, int second, long calcOffset) {
                int mid = first + (int) calcOffset;
                int limitoff = (second - first) / 5;
                if (mid - first < limitoff)
                        mid = first + limitoff;
                else if (second - mid < limitoff)
                        mid = second - limitoff;

                // Round to a garmin map unit at the desired zoom level.
                int nmid = RoundingUtils.round(mid, shift);

                // Check that the midpoint is on the appropriate alignment boundary. If not, adjust
                int alignment = 1 << shift;
                if ((nmid & alignment) != (first & alignment)) {
                        if (nmid < mid) {
                                nmid += alignment;
                } else {
                                nmid -= alignment;
                        }
                }

                // Check if we're going to end up on the edge of a tile. If so, move away. We always
                // have room to move away because a split is only attempted in the first place if
                // the tile to split is bigger than the minimum tile width.
                if (nmid == first) {
                        nmid += alignment << 1;
                } else if (nmid == second) {
                        nmid -= alignment << 1;
                }

                assert nmid > first && nmid < second;
                return nmid;
        }
}