Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2006 Steve Ratcliffe
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License 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.
 *
 *
 * Author: Steve Ratcliffe
 * Create date: 07-Dec-2006
 */

package uk.me.parabola.imgfmt.app.trergn;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.ImgFileReader;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
import uk.me.parabola.imgfmt.app.Label;
import uk.me.parabola.imgfmt.app.lbl.LBLFile;
import uk.me.parabola.log.Logger;

/**
 * The map is divided into areas, depending on the zoom level.  These are
 * known as subdivisions.
 *
 * A subdivision 'belongs' to a zoom level and cannot be interpreted correctly
 * without knowing the <i>bitsPerCoord</i> of the associated zoom level.
 *
 * Subdivisions also form a tree as subdivisions are further divided at
 * lower levels.  The subdivisions need to know their child divisions
 * because this information is represented in the map.
 *
 * @author Steve Ratcliffe
 */

public class Subdivision {
        private static final Logger log = Logger.getLogger(Subdivision.class);

        private static final int MAP_POINT = 0;
        private static final int MAP_INDEXED_POINT = 1;
        private static final int MAP_LINE = 2;
        private static final int MAP_SHAPE = 3;

        private final LBLFile lblFile;
        private final RGNFile rgnFile;

        // The start pointer is set for read and write.  The end pointer is only
        // set for subdivisions that are read from a file.
        private int startRgnPointer;
        private int endRgnPointer;

        private int lastMapElement;

        // The zoom level contains the number of bits per coordinate which is
        // critical for scaling quantities by.
        private final Zoom zoomLevel;

        private boolean hasPoints;
        private boolean hasIndPoints;
        private boolean hasPolylines;
        private boolean hasPolygons;

        private int numPolylines;

        // The location of the central point, not scaled AFAIK
        private final int longitude;
        private final int latitude;

        // The width and the height in map units scaled by the bits-per-coordinate
        // that applies at the map level.
        private final int width;
        private final int height;

        private int number;

        // Set if this is the last one.
        private boolean last;

        private final List<Subdivision> divisions = new ArrayList<>();

        private int extTypeAreasOffset;
        private int extTypeLinesOffset;
        private int extTypePointsOffset;
        private int extTypeAreasSize;
        private int extTypeLinesSize;
        private int extTypePointsSize;

        /**
         * Subdivisions can not be created directly, use either the
         * {@link #topLevelSubdivision} or {@link #createSubdivision} factory
         * methods.
         *
         * @param ifiles The internal files.
         * @param area The area this subdivision should cover.
         * @param z The zoom level.
         */

        private Subdivision(InternalFiles ifiles, Area area, Zoom z) {
                this.lblFile = ifiles.getLblFile();
                this.rgnFile = ifiles.getRgnFile();

                this.zoomLevel = z;

                int shift = getShift();
                int mask = getMask();

                // Calculate the center, move it right and up so that it lies on a point
                // which is divisible by 2 ^shift
                this.latitude = Utils.roundUp((area.getMinLat() + area.getMaxLat())/2, shift);
                this.longitude = Utils.roundUp((area.getMinLong() + area.getMaxLong())/2, shift);
                int w = 2 * (longitude - area.getMinLong());
                int h = 2 * (latitude - area.getMinLat());
               
                // encode the values for the img format
                w = ((w + 1)/2 + mask) >> shift;
                h = ((h + 1)/2 + mask) >> shift;
               
                if (w > 0x7fff) {
                        log.warn("Subdivision width is " + w + " at " + getCenter());
                        w = 0x7fff;
                }

                if (h > 0xffff) {
                        log.warn("Subdivision height is " + h + " at " + getCenter());
                        h = 0xffff;
                }

                this.width = w;
                this.height = h;
        }

        private Subdivision(Zoom z, SubdivData data) {
                lblFile = null;
                rgnFile = null;
                zoomLevel = z;
                latitude = data.getLat();
                longitude = data.getLon();
                this.width = data.getWidth();
                this.height = data.getHeight();

                startRgnPointer = data.getRgnPointer();
                endRgnPointer = data.getEndRgnOffset();

                int elem = data.getFlags();
                if ((elem & 0x10) != 0)
                        setHasPoints(true);
                if ((elem & 0x20) != 0)
                        setHasIndPoints(true);
                if ((elem & 0x40) != 0)
                        setHasPolylines(true);
                if ((elem & 0x80) != 0)
                        setHasPolygons(true);
        }

        /**
         * Create a subdivision at a given zoom level.
         *
         * @param ifiles The RGN and LBL ifiles.
         * @param area The (unshifted) area that the subdivision covers.
         * @param zoom The zoom level that this division occupies.
         *
         * @return A new subdivision.
         */

        public Subdivision createSubdivision(InternalFiles ifiles,
                        Area area, Zoom zoom)
        {
                Subdivision div = new Subdivision(ifiles, area, zoom);
                zoom.addSubdivision(div);
                addSubdivision(div);
                return div;
        }

        /**
         * This should be called only once per map to create the top level
         * subdivision.  The top level subdivision covers the whole map and it
         * must be empty.
         *
         * @param ifiles The LBL and  RGN ifiles.
         * @param area The area bounded by the map.
         * @param zoom The zoom level which must be the highest (least detailed)
     * zoom in the map.
         *
         * @return The new subdivision.
         */

        public static Subdivision topLevelSubdivision(InternalFiles ifiles,
                        Area area, Zoom zoom)
        {
                Subdivision div = new Subdivision(ifiles, area, zoom);
                zoom.addSubdivision(div);
                return div;
        }

        /**
         * Create a subdivision that only contains the number.  This is only
         * used when reading cities and similar such usages that do not really
         * require the full subdivision to be present.
         * @param number The subdivision number.
         * @return An empty subdivision.  Any operation other than getting the
         * subdiv number is likely to fail.
         */

        public static Subdivision createEmptySubdivision(int number) {
                Subdivision sd = new Subdivision(null, new SubdivData(0,0,0,0,0,0,0));
                sd.setNumber(number);
                return sd;
        }

        public static Subdivision readSubdivision(Zoom zoom, SubdivData subdivData) {
                return new Subdivision(zoom, subdivData);
        }

        public Zoom getZoom() {
                return zoomLevel;
        }

        /**
         * Get the shift value, that is the number of bits to left shift by for
         * values that need to be saved shifted in the file.  Related to the
         * resolution.
         *
         * @return The shift value.  It is 24 minus the number of bits per coord.
         * @see #getResolution()
         */

        public final int getShift() {
                return 24 - zoomLevel.getResolution();
        }

        /**
         * Get the shift mask.  The bits that will be lost due to the resolution
         * shift level.
         *
         * @return A bit mask with the lower <i>shift</i> bits set.
         */

        protected int getMask() {
                return (1 << getShift()) - 1;
        }

        /**
         * Get the resolution of this division.  Resolution goes from 1 to 24
         * and the higher the number the more detail there is.
         *
         * @return The resolution.
         */

        public final int getResolution() {
                return zoomLevel.getResolution();
        }

        /**
         * Format this record to the file.
         *
         * @param file The file to write to.
         */

        public void write(ImgFileWriter file) {
                log.debug("write subdiv", latitude, longitude);
                file.put3(startRgnPointer);
                file.put(getType());
                file.put3(longitude);
                file.put3(latitude);
               
                assert width <= 0x7fff;
                assert height <= 0xffff;
                file.putChar((char) (width | ((last) ? 0x8000 : 0)));
                file.putChar((char) height);

                if (!divisions.isEmpty()) {
                        file.putChar((char) getNextLevel());
                }
        }

        public Point createPoint(String name) {
                Point p = new Point(this);
                Label label = lblFile.newLabel(name);

                p.setLabel(label);
                return p;
        }

        public Polyline createLine(String[] labels) {
                // don't be tempted to "trim()" the name as it zaps the highway shields
                Label label = lblFile.newLabel(labels[0]);
                String nameSansGC = Label.stripGarminCodes(labels[0]);
                Polyline pl = new Polyline(this);

                pl.setLabel(label);

                if(labels[1] != null) {
                        // ref may contain multiple ids separated by ";"
                        int maxSetIdx = 3;
                        if (labels[3] == null) {
                                if (labels[2] == null) {
                                        maxSetIdx = 1;
                                } else {
                                        maxSetIdx = 2;
                                }
                        }

                        String[] refs = Arrays.copyOfRange(labels, 1, maxSetIdx+1);
                        if(refs.length == 1) {
                                // don't bother to add a single ref that looks the
                                // same as the name (sans shield) because it doesn't
                                // change the routing directions
                                String tr = refs[0].trim();
                                String trSansGC = Label.stripGarminCodes(tr);
                                if(trSansGC.length() > 0 &&
                                                !trSansGC.equalsIgnoreCase(nameSansGC)) {
                                        pl.addRefLabel(lblFile.newLabel(tr));
                                }
                        }
                        else if (refs.length > 1){
                                // multiple refs, always add the first so that it will
                                // be used in routing instructions when the name has a
                                // shield prefix
                                pl.addRefLabel(lblFile.newLabel(refs[0].trim()));

                                // only add the remaining refs if they differ from the
                                // name (sans shield)
                                for(int i = 1; i < refs.length; ++i) {
                                        String tr = refs[i].trim();
                                        String trSansGC = Label.stripGarminCodes(tr);
                                        if(trSansGC.length() > 0 &&
                                                        !trSansGC.equalsIgnoreCase(nameSansGC)) {
                                                pl.addRefLabel(lblFile.newLabel(tr));
                                        }
                                }
                        }
                }
                return pl;
        }

        public void setPolylineNumber(Polyline pl) {
                pl.setNumber(++numPolylines);
        }

        public Polygon createPolygon(String name) {
                Label label = lblFile.newLabel(name);
                Polygon pg = new Polygon(this);

                pg.setLabel(label);
                return pg;
        }

        public void setNumber(int n) {
                number = n;
        }

        public void setLast(boolean last) {
                this.last = last;
        }

        public void setStartRgnPointer(int startRgnPointer) {
                this.startRgnPointer = startRgnPointer;
        }

        public int getStartRgnPointer() {
                return startRgnPointer;
        }

        public int getEndRgnPointer() {
                return endRgnPointer;
        }

        public int getLongitude() {
                return longitude;
        }

        public int getLatitude() {
                return latitude;
        }

        public void setHasPoints(boolean hasPoints) {
                this.hasPoints = hasPoints;
        }

        public void setHasIndPoints(boolean hasIndPoints) {
                this.hasIndPoints = hasIndPoints;
        }

        public void setHasPolylines(boolean hasPolylines) {
                this.hasPolylines = hasPolylines;
        }

        public void setHasPolygons(boolean hasPolygons) {
                this.hasPolygons = hasPolygons;
        }

        public boolean hasPoints() {
                return hasPoints;
        }

        public boolean hasIndPoints() {
                return hasIndPoints;
        }

        public boolean hasPolylines() {
                return hasPolylines;
        }

        public boolean hasPolygons() {
                return hasPolygons;
        }

        /**
         * Needed if it exists and is not first, ie there is a points
         * section.
         * @return true if pointer needed
         */

        public boolean needsIndPointPtr() {
                return hasIndPoints && hasPoints;
        }

        /**
         * Needed if it exists and is not first, ie there is a points or
         * indexed points section.
         * @return true if pointer needed.
         */

        public boolean needsPolylinePtr() {
                return hasPolylines && (hasPoints || hasIndPoints);
        }

        /**
         * As this is last in the list it is needed if it exists and there
         * is another section.
         * @return true if pointer needed.
         */

        public boolean needsPolygonPtr() {
                return hasPolygons && (hasPoints || hasIndPoints || hasPolylines);
        }

        public String toString() {
                return "Sub" + zoomLevel + '(' + getCenter().toOSMURL() + ')';
        }
        /**
         * Get a type that shows if this area has lines, points etc.
         *
         * @return A code showing what kinds of element are in this subdivision.
         */

        private byte getType() {
                byte b = 0;
                if (hasPoints)
                        b |= 0x10;
                if (hasIndPoints)
                        b |= 0x20;
                if (hasPolylines)
                        b |= 0x40;
                if (hasPolygons)
                        b |= 0x80;

                return b;
        }
        /**
         * Get the number of the first subdivision at the next level.
         * @return The first subdivision at the next level.
         */

        private int getNextLevel() {
                return divisions.get(0).getNumber();
        }

        public boolean hasNextLevel() {
                return !divisions.isEmpty();
        }

        public int getExtTypeAreasOffset() {
                return extTypeAreasOffset;
        }

        public int getExtTypeLinesOffset() {
                return extTypeLinesOffset;
        }

        public int getExtTypePointsOffset() {
                return extTypePointsOffset;
        }

        public int getExtTypeAreasSize() {
                return extTypeAreasSize;
        }

        public int getExtTypeLinesSize() {
                return extTypeLinesSize;
        }

        public int getExtTypePointsSize() {
                return extTypePointsSize;
        }

        public void startDivision() {
                rgnFile.startDivision(this);
                extTypeAreasOffset = rgnFile.getExtTypeAreasSize();
                extTypeLinesOffset = rgnFile.getExtTypeLinesSize();
                extTypePointsOffset = rgnFile.getExtTypePointsSize();
        }

        public void endDivision() {
                extTypeAreasSize = rgnFile.getExtTypeAreasSize() - extTypeAreasOffset;
                extTypeLinesSize = rgnFile.getExtTypeLinesSize() - extTypeLinesOffset;
                extTypePointsSize = rgnFile.getExtTypePointsSize() - extTypePointsOffset;
        }

        public void writeExtTypeOffsetsRecord(ImgFileWriter file) {
                file.putInt(extTypeAreasOffset);
                file.putInt(extTypeLinesOffset);
                file.putInt(extTypePointsOffset);
                int kinds = 0;
                if(extTypeAreasSize != 0)
                        ++kinds;
                if(extTypeLinesSize != 0)
                        ++kinds;
                if(extTypePointsSize != 0)
                        ++kinds;
                file.put((byte)kinds);
        }

        public void writeLastExtTypeOffsetsRecord(ImgFileWriter file) {
                file.putInt(rgnFile.getExtTypeAreasSize());
                file.putInt(rgnFile.getExtTypeLinesSize());
                file.putInt(rgnFile.getExtTypePointsSize());
                file.put((byte)0);
        }

        /**
         * Read offsets for extended type data and set sizes for predecessor sub-div.
         * Corresponds to {@link #writeExtTypeOffsetsRecord(ImgFileWriter)}
         * @param reader the reader
         * @param sdPrev the pred. sub-div or null
         */

        public void readExtTypeOffsetsRecord(ImgFileReader reader,
                        Subdivision sdPrev) {
                extTypeAreasOffset = reader.getInt();
                extTypeLinesOffset = reader.getInt();
                extTypePointsOffset = reader.getInt();
                reader.get();
                if (sdPrev != null){
                        sdPrev.extTypeAreasSize = extTypeAreasOffset - sdPrev.extTypeAreasOffset;
                        sdPrev.extTypeLinesSize = extTypeLinesOffset - sdPrev.extTypeLinesOffset;
                        sdPrev.extTypePointsSize = extTypePointsOffset - sdPrev.extTypePointsOffset;
                }
        }
        /**
         * Set the sizes for the extended type data. See {@link #writeLastExtTypeOffsetsRecord(ImgFileWriter)}
         */

        public void readLastExtTypeOffsetsRecord(ImgFileReader reader) {
                extTypeAreasSize = reader.getInt() - extTypeAreasOffset;
                extTypeLinesSize = reader.getInt() - extTypeLinesOffset;
                extTypePointsSize = reader.getInt() - extTypePointsOffset;
                byte test = reader.get();
                assert test == 0;
        }
       
        /**
         * Add this subdivision as our child at the next level.  Each subdivision
         * can be further divided into smaller divisions.  They form a tree like
         * arrangement.
         *
         * @param sd One of our subdivisions.
         */

        private void addSubdivision(Subdivision sd) {
                divisions.add(sd);
        }

        public int getNumber() {
                return number;
        }

        /**
         * We are starting to draw the points.  These must be done first.
         */

        public void startPoints() {
                if (lastMapElement > MAP_POINT)
                        throw new IllegalStateException("Points must be drawn first");

                lastMapElement = MAP_POINT;
        }

        /**
         * We are starting to draw the lines.  These must be done before
         * polygons.
         */

        public void startIndPoints() {
                if (lastMapElement > MAP_INDEXED_POINT)
                        throw new IllegalStateException("Indexed points must be done before lines and polygons");

                lastMapElement = MAP_INDEXED_POINT;

                rgnFile.setIndPointPtr();
        }

        /**
         * We are starting to draw the lines.  These must be done before
         * polygons.
         */

        public void startLines() {
                if (lastMapElement > MAP_LINE)
                        throw new IllegalStateException("Lines must be done before polygons");

                lastMapElement = MAP_LINE;

                rgnFile.setPolylinePtr();
        }

        /**
         * We are starting to draw the shapes.  This is done last.
         */

        public void startShapes() {

                lastMapElement = MAP_SHAPE;

                rgnFile.setPolygonPtr();
        }

        /**
         * Convert an absolute Lat to a local, shifted value
         */

        public int roundLatToLocalShifted(int absval) {
                int shift = getShift();
                int val = absval - getLatitude();
                val += ((1 << shift) / 2);
                return (val >> shift);
        }

        /**
         * Convert an absolute Lon to a local, shifted value
         */

        public int roundLonToLocalShifted(int absval) {
                int shift = getShift();
                int val = absval - getLongitude();
                val += ((1 << shift) / 2);
                return (val >> shift);
        }


        public Coord getCenter(){
                return new Coord(getLatitude(),getLongitude());
        }

        /**
         * Get the unshifted width of the subdivision.
         * @return The true (unshifted) width.
         */

        public int getWidth() {
                return width << getShift();
        }

        /**
         * Get the unshifted height of the subdivision.
         * @return The true (unshifted) height.
         */

        public int getHeight() {
                return height << getShift();
        }
}