mkgmap-r3660/0000755000076400007640000000000012651103764012163 5ustar stevestevemkgmap-r3660/extra/0000755000076400007640000000000012651103762013304 5ustar stevestevemkgmap-r3660/extra/README0000644000076400007640000000015411237536256014173 0ustar stevesteve This directory contains code that requires external libraries to work and so is not part of the core code. mkgmap-r3660/extra/src/0000755000076400007640000000000012651103762014073 5ustar stevestevemkgmap-r3660/extra/src/uk/0000755000076400007640000000000012651103762014512 5ustar stevestevemkgmap-r3660/extra/src/uk/me/0000755000076400007640000000000012651103762015113 5ustar stevestevemkgmap-r3660/extra/src/uk/me/parabola/0000755000076400007640000000000012651103762016674 5ustar stevestevemkgmap-r3660/extra/src/uk/me/parabola/mkgmap/0000755000076400007640000000000012651103762020150 5ustar stevestevemkgmap-r3660/extra/src/uk/me/parabola/mkgmap/ant/0000755000076400007640000000000012651103762020732 5ustar stevestevemkgmap-r3660/extra/src/uk/me/parabola/mkgmap/ant/MKGMapTask.java0000644000076400007640000000411311713757443023504 0ustar stevesteve /* * Copyright (C) 2009 Christian Gawron * * 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: Christian Gawron * Create date: 19-Jun-2009 */ package uk.me.parabola.mkgmap.ant; import java.util.ArrayList; import java.util.List; import uk.me.parabola.mkgmap.CommandArgsReader; import uk.me.parabola.mkgmap.main.Main; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Path; /** * This class provides an ant task for mkgmap. * Used like this: * * */ @SuppressWarnings({"UnusedDeclaration"}) public class MKGMapTask extends Task { private final ArrayList paths = new ArrayList(); private String configFile; public void addPath(Path path) { paths.add(path); } public void setOptions(String configFile) { this.configFile = configFile; } public void execute() { List args = new ArrayList(); try { CommandArgsReader argsReader = new CommandArgsReader(new Main()); if (configFile != null) args.add("--read-config=" + configFile); for (Path path : paths) { String[] includedFiles = path.list(); for (String filename : includedFiles) { log("processing " + filename); args.add("--input-file=" + filename); } } argsReader.readArgs(args.toArray(new String[args.size()])); } catch (Exception ex) { //log(ex, 1); throw new BuildException(ex); } } } mkgmap-r3660/extra/src/uk/me/parabola/util/0000755000076400007640000000000012651103762017651 5ustar stevestevemkgmap-r3660/extra/src/uk/me/parabola/util/TableIcuCreator.java0000644000076400007640000000617311400726372023532 0ustar stevesteve/* * 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 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.util; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import uk.me.parabola.imgfmt.Utils; import com.ibm.icu.text.Transliterator; /** * Call this with a unicode row number and it will produce an empty table * that can be modified. * * @author Steve Ratcliffe */ public class TableIcuCreator { private static Transliterator trans; public static void main(String[] args) { int count = 0; Enumeration targets = Transliterator.getAvailableIDs(); while (targets.hasMoreElements()) { String s = targets.nextElement(); System.out.println(s); count++; } System.out.println("number " + count); //System.exit(0); //trans = Transliterator.getInstance("Any-en_US; nfd; [\u0301\u0302\u0304\u0306\u0307\u0308\u030c\u0328] remove; nfc"); // [:nonspacing mark:] remove; nfc"); trans = Transliterator.getInstance("Any-Latin"); // [:nonspacing mark:] remove; nfc"); for (int row = 0; row < 256; row++) { String name = String.format("row%02x.trans", row); PrintWriter out = null; try { out = new PrintWriter(new FileWriter(name)); printRow(out, row); } catch (IOException e) { System.out.println("Could not open " + name + " for write"); } catch (UselessException e) { //System.out.println("Deleting " + name); File f = new File(name); f.delete(); } finally { Utils.closeFile(out); } } } private static void printRow(PrintWriter out, int row) throws UselessException { out.println("#"); out.println("# This is a table for transliterating characters."); out.println("# It was created using icu4j"); out.println("#"); out.println("# All resulting strings that contained characters outside the"); out.println("# range of iso 8859-1 are commented out"); out.println("#"); out.println(); int count = 0; for (int i = 0; i < 256; i++) { char c = (char) ((row << 8) + i); String single = "" + c; String result = trans.transliterate(single); if (result.length() == 1 && result.charAt(0) == c) result = "?"; else count++; boolean inRange = true; for (char rc : result.toCharArray()) { if (rc > 0xff) { //System.out.printf("out of range result %c for row %d\n", rc, row); inRange = false; break; } } if (!inRange) { count--; out.print("#"); } out.format("U+%02x%02x %-12.12s # Character %s", row, i, result, single); out.println(); } if (count == 0) throw new UselessException("count of 0"); } private static class UselessException extends Exception { private UselessException(String msg) { super(msg); } } }mkgmap-r3660/extra/src/uk/me/parabola/util/CollationRules.java0000644000076400007640000002457112346663602023471 0ustar stevesteve/* * 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.util; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.Formatter; import java.util.HashMap; import java.util.Map; import java.util.NavigableSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.ibm.icu.text.CollationElementIterator; import com.ibm.icu.text.Collator; import com.ibm.icu.text.RuleBasedCollator; //import java.text.CollationElementIterator; //import java.text.Collator; //import java.text.RuleBasedCollator; /** * Create a set of rules for a given code page. * * Should be usable, perhaps with a few tweaks. * Works with unicode too, need to choose which blocks to take for unicode. * * @author Steve Ratcliffe */ public class CollationRules { private CharsetDecoder decoder; private final NavigableSet positionMap = new TreeSet<>(); private final NavigableSet basePositionMap = new TreeSet<>(); private final Map charMap = new HashMap<>(); private boolean isUnicode; private Charset charset; public static void main(String[] args) { String charsetName = args[0]; CollationRules main = new CollationRules(); main.go(charsetName); } private void go(String charsetName) { RuleBasedCollator col = (RuleBasedCollator) Collator.getInstance(); charset = Charset.forName(charsetName); if (charsetName.equalsIgnoreCase("utf-8")) isUnicode = true; decoder = charset.newDecoder(); if (isUnicode) addUnicode(); else addBlock(col, 0); printCharMap(); printExpansions(); } private void addBlock(RuleBasedCollator col, int block) { for (int i = 0; i < 0x100; i++) { int ch = (block << 8) + i; String testString = getString(ch); char conv = testString.charAt(0); if (Character.getType(conv) == Character.UNASSIGNED || conv == 0xfffd) continue; CollationElementIterator it = col.getCollationElementIterator(testString); System.out.printf("# %s ", fmtChar(testString.charAt(0))); int next; int index = 0; CharPosition cp = new CharPosition(0); while ((next = it.next()) != CollationElementIterator.NULLORDER) { if (index == 0) { cp = new CharPosition(ch); cp.setOrder(next); } else { assert index < 3; if ((next & 0xffff0000) == 0) { cp.addOrder(next, index); } else { cp.addChar(new CharPosition(ch)); cp.setOrder(next); } } index++; } System.out.printf(" %s %d", cp, Character.getType(cp.getUnicode())); System.out.println(); tweak(cp); if (ch > 0) positionMap.add(cp); if (cp.nextChar == null) { basePositionMap.add(cp); charMap.put(conv, cp); } } } private void addUnicode() { Pattern pat = Pattern.compile("([0-9A-F]{4,5}) ? ; \\[[.*](.*)\\] #.*"); try (FileReader r = new FileReader("allkeys.txt")) { try (BufferedReader br = new BufferedReader(r)) { String line; while ((line = br.readLine()) != null) { Matcher matcher = pat.matcher(line); if (matcher.matches()) { String weights = matcher.group(2); int ch = Integer.parseInt(matcher.group(1), 16); if (ch > 0xffff) continue; System.out.printf("# %04x %s ", ch, fmtChar(ch)); String[] split = weights.split("]\\[[.*]"); int index = 0; CharPosition cp = new CharPosition(0); for (String s : split) { String[] ws = s.split("\\."); int next = Integer.parseInt(ws[0], 16) << 16 | ((Integer.parseInt(ws[1], 16) << 8) & 0xff00) | ((Integer.parseInt(ws[2], 16)) & 0xff); if (index == 0) { cp = new CharPosition(ch); cp.setOrder(next); } else { if ((next & 0xffff0000) == 0) { cp.addOrder(next, index); } else { cp.addChar(new CharPosition(ch)); cp.setOrder(next); } } index++; } System.out.printf(" %s %d\n", cp, Character.getType(cp.getUnicode())); tweak(cp); if (ch > 0) positionMap.add(cp); if (cp.nextChar == null) { basePositionMap.add(cp); charMap.put((char) ch, cp); } } else { System.out.println("# NOMATCH: " + line); } } } } catch (IOException e) { e.printStackTrace(); } } /** * Fix up a few characters that we always want to be in well known places. * * @param cp The position to change. */ private void tweak(CharPosition cp) { if (cp.val < 8) cp.third = cp.val + 7; if (!isUnicode) { switch (cp.getUnicode()) { case '¼': cp.nextChar = charMap.get('/').copy(); cp.nextChar.nextChar = charMap.get('4'); break; case '½': cp.nextChar = charMap.get('/').copy(); cp.nextChar.nextChar = charMap.get('2'); break; case '¾': cp.nextChar = charMap.get('/').copy(); cp.nextChar.nextChar = charMap.get('4'); break; } } switch (cp.getUnicode()) { case '˜': CharPosition tilde = charMap.get('~'); cp.first = tilde.first; cp.second = tilde.second + 1; cp.third = tilde.third + 1; cp.nextChar = null; break; } } private String getString(int i) { if (isUnicode) return new String(new char[]{(char) i}); else { byte[] b = {(byte) i}; return new String(b, 0, 1, charset); } } private void printCharMap() { Formatter chars = new Formatter(); chars.format("\n"); CharPosition last = new CharPosition(0); last.first = 0; for (CharPosition cp : positionMap) { if (cp.isExpansion()) continue; if (cp.first != last.first) { chars.format("\n < "); } else if (cp.second != last.second) { chars.format(" ; "); } else if (cp.third != last.third) { chars.format(","); } else { chars.format("="); } last = cp; int uni = toUnicode(cp.val); chars.format("%s", fmtChar(uni)); } System.out.println(chars); } private void printExpansions() { for (CharPosition cp : positionMap) { if (!cp.isExpansion()) continue; Formatter fmt = new Formatter(); //noinspection MalformedFormatString fmt.format("expand %c to", cp.getUnicode()); boolean ok = true; for (CharPosition cp2 = cp; cp2 != null; cp2 = cp2.nextChar) { cp2.second = 0x50000; int top = (cp2.third >> 16) & 0xff; cp2.third = (top == 0x9e || top == 0xa2 || top == 0x2b) ? 0x9b0000 : 0; CharPosition floor = basePositionMap.ceiling(cp2); if (floor == null || floor.getUnicode() == 0xfffd) { fmt.format(" NF"); ok = false; } else { fmt.format(" %s", fmtChar(floor.getUnicode())); } } System.out.println((ok ? "" : "# ") + fmt.toString()); // Print comments to help find problems. for (CharPosition cp2 = cp; cp2 != null; cp2 = cp2.nextChar) { CharPosition floor = basePositionMap.ceiling(cp2); if (floor == null) { System.out.println("#FIX: NF ref=" + cp2); } else { System.out.println("#floor is " + fmtChar(toUnicode(floor.val)) + ", " + floor + ", ref is " + cp2); } } } } private String fmtChar(int val) { boolean asChar = true; switch (val) { case '<': case ';': case ',': case '=': case '#': asChar = false; break; default: switch (Character.getType(val)) { case Character.UNASSIGNED: case Character.NON_SPACING_MARK: case Character.FORMAT: case Character.CONTROL: case Character.SPACE_SEPARATOR: case Character.LINE_SEPARATOR: case Character.PARAGRAPH_SEPARATOR: asChar = false; } } if (asChar) { //noinspection MalformedFormatString return String.format("%c", val); } else { return String.format("%04x", val); } } private int toUnicode(int c) { if (isUnicode) return c; ByteBuffer b = ByteBuffer.allocate(1); b.put((byte) c); b.flip(); try { CharBuffer chars = decoder.decode(b); return chars.charAt(0); } catch (CharacterCodingException e) { return '?'; } } class CharPosition implements Comparable { private final int val; private int first; private int second; private int third; private CharPosition nextChar; public CharPosition(int charValue) { this.val = charValue; } public int compareTo(CharPosition other) { if (other.first == first) return compareSecond(other); else if (first < other.first) return -1; else return 1; } private int compareSecond(CharPosition c2) { if (c2.second == second) return compareThird(c2); else if (second < c2.second) return -1; else return 1; } private int compareThird(CharPosition c2) { if (third == c2.third) return new Integer(val).compareTo(c2.val); else if (third < c2.third) return -1; else return 1; } public String toString() { Formatter fmt = new Formatter(); toString(fmt); return fmt.toString(); } private void toString(Formatter fmt) { fmt.format("[%04x %02x %02x]", first, second, third); if (nextChar != null) nextChar.toString(fmt); } public void setOrder(int next) { if (nextChar != null) { nextChar.setOrder(next); return; } first = (next >> 16) & 0xffff; second = (next << 8) & 0xff0000; third = (next << 16) & 0xff0000; } public void addOrder(int next, int count) { assert ((next >>> 16) & 0xffff) == 0; if (this.nextChar != null) { this.nextChar.addOrder(next, count); return; } second += ((next >> 8) & 0xff) << (2-count)*8; third += ((next) & 0xff) << (2-count)*8; } public boolean isExpansion() { return nextChar != null; } public void addChar(CharPosition pos) { if (nextChar != null) { nextChar.addChar(pos); return; } nextChar = pos; } public int getUnicode() { return toUnicode(val); } public CharPosition copy() { CharPosition cp = new CharPosition(this.val); cp.first = this.first; cp.second = this.second; cp.third = this.third; return cp; } } } mkgmap-r3660/README0000644000076400007640000000521712101051755013040 0ustar stevesteve Making maps for Garmin GPS units ================================ The goal of the project is to take the map data from OpenStreetMap and to generate a map in the Garmin file format so that it can be loaded onto Garmin GPS units. The original motivation was to help plan mapping sessions, but now the maps are becoming useful in their own right. Using ===== This program requires Java 1.6 or above to run. Producing a map is simple. Save OpenStreetMap data from JOSM or by any other method to a file and copy it to the mkgmap directory. In the following examples this file is called data.osm. Run the command: java -jar mkgmap.jar data.osm This will produce a file called 63240001.img. You can copy the map to your Garmin GPS unit in any way you know how. It is best to use a SD card, since then if anything goes wrong you can remove it from the unit and all should be well again. Copy it to the file "Garmin/gmapsupp.img" on the card. On many modern Garmin devices, you can use a different name so that you can have more than one set of maps. *NOTE* this will overwrite any other map you have on there, make sure that you are not overwriting a valuable map. Another way would be to use a USB memory card writer and for a large map this is quicker as many GPS's have a slow USB connection. There are also various programs that can send a map to the device. You should (depending on the particular Garmin model) see a OSM copyright message on starting up, and the map name 'OSM Street map' should appear in the map setup section. For more help see: http://www.mkgmap.org.uk/doc/index.html Invoking mkgmap -------------- Most of the default names mentioned in the previous section can be altered by suitable options. Run java -jar mkgmap.jar --help=options to obtain an up to date and complete listing of options. Processing more than one file at a time --------------------------------------- The Garmin map format was designed so that a map is made of a number of tiles, and if your map has more data than can fit into a single tile, you will have to split the map. See: http://www.mkgmap.org.uk/doc/splitter.html for a program that can do this. You can compile all of the map tiles that are created by splitter all at once, by simply listing them all on the command line. Acknowledgements ================ This project is almost entirely based on the file format specification document written by John Mechalas at the SourceForge project at http://sourceforge.net/projects/garmin-img. The 'imgdecode' program from the same source was also very important in checking that I was on the right lines to producing a good file. Thanks. Steve mkgmap-r3660/src/0000755000076400007640000000000012651103762012750 5ustar stevestevemkgmap-r3660/src/uk/0000755000076400007640000000000012651103762013367 5ustar stevestevemkgmap-r3660/src/uk/me/0000755000076400007640000000000012651103762013770 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/0000755000076400007640000000000012651103764015553 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/tdbfmt/0000755000076400007640000000000012651103762017031 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/tdbfmt/Block.java0000644000076400007640000000613210763340724020733 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import uk.me.parabola.log.Logger; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.io.StructuredOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; /** * A block within the tdb file. Really just a type and the contents. * * @author Steve Ratcliffe */ class Block { private static final Logger log = Logger.getLogger(Block.class); private final int blockId; private int blockLength; private byte[] body; private StructuredInputStream istream; private ByteArrayOutputStream arrayBody; private StructuredOutputStream ostream; /** * Create a block that is going to be written to. * @param blockId The id for this block. */ Block(int blockId) { this.blockId = blockId; } /** * Create a block from data that is read in from a file. * @param type The block type. * @param body The raw bytes in the block. */ Block(int type, byte[] body) { blockId = type; this.body = body; this.blockLength = body.length; ByteArrayInputStream stream = new ByteArrayInputStream(body); this.istream = new StructuredInputStream(stream); } public int getBlockId() { return blockId; } /** * Get the raw bytes for this block. The source depends on if this block * was constructed from file data, or is being created from program calls * so that it can be written. * * @return A byte array of the raw bytes representing this block. */ byte[] getBody() { if (body == null && arrayBody != null) { byte[] bytes = arrayBody.toByteArray(); blockLength = bytes.length - 3; // Fill in the length in the space that we left earlier. bytes[1] = (byte) (blockLength & 0xff); bytes[2] = (byte) ((blockLength >> 8) & 0xff); return bytes; } return body; } /** * Get a stream for the body of this block. * * @return A structured stream that can be used to read the body of this * block. */ public StructuredInputStream getInputStream() { arrayBody = null; return this.istream; } public StructuredOutputStream getOutputStream() { if (ostream == null) { arrayBody = new ByteArrayOutputStream(); body = null; ostream = new StructuredOutputStream(arrayBody); try { ostream.write(blockId); ostream.write2(0); // This will be filled in later. } catch (IOException e) { log.warn("failed writing to array"); } } return ostream; } public void write(OutputStream stream) throws IOException { byte[] b = getBody(); if (b != null) stream.write(b); } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/TdbFile.java0000644000076400007640000001511412175724544021217 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.zip.CRC32; import java.util.zip.CheckedOutputStream; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.io.EndOfFileException; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.log.Logger; /** * The TDB file. See the package documentation for more details. * * @author Steve Ratcliffe */ public class TdbFile { private static final Logger log = Logger.getLogger(TdbFile.class); public static final int TDB_V407 = 407; private static final int BLOCK_OVERVIEW = 0x42; private static final int BLOCK_HEADER = 0x50; private static final int BLOCK_COPYRIGHT = 0x44; private static final int BLOCK_DETAIL = 0x4c; private static final int BLOCK_R = 0x52; private static final int BLOCK_T = 0x54; // The version number of the TDB format private int tdbVersion; // The blocks that go to make up the file. private HeaderBlock headerBlock; private CopyrightBlock copyrightBlock = new CopyrightBlock(); private OverviewMapBlock overviewMapBlock; private final List detailBlocks = new ArrayList(); private final RBlock rblock = new RBlock(); private final TBlock tblock = new TBlock(); private String overviewDescription; public TdbFile() { } public TdbFile(int tdbVersion) { this.tdbVersion = tdbVersion; } /** * Read in a TDB file from the disk. * * @param name The file name to load. * @return A TdbFile instance. * @throws IOException For problems reading the file. */ public static TdbFile read(String name) throws IOException { TdbFile tdb = new TdbFile(); InputStream is = new BufferedInputStream(new FileInputStream(name)); try { StructuredInputStream ds = new StructuredInputStream(is); tdb.load(ds); } finally { is.close(); } return tdb; } public void setProductInfo(int familyId, int productId, short productVersion, String seriesName, String familyName, String overviewDescription, byte enableProfile) { headerBlock = new HeaderBlock(tdbVersion); headerBlock.setFamilyId((short) familyId); headerBlock.setProductId((short) productId); headerBlock.setProductVersion(productVersion); headerBlock.setSeriesName(seriesName); headerBlock.setFamilyName(familyName); headerBlock.setEnableProfile(enableProfile); this.overviewDescription = overviewDescription; } public void setCodePage(int codePage) { headerBlock.setCodePage(codePage); } /** * Add a copyright segment to the file. * @param msg The message to add. */ public void addCopyright(String msg) { CopyrightSegment seg = new CopyrightSegment(CopyrightSegment.CODE_COPYRIGHT_TEXT_STRING, 3, msg); copyrightBlock.addSegment(seg); } /** * Set the overview information. Basically the overall size of the map * set. * @param bounds The bounds for the map. */ public void setOverview(Area bounds, String number) { overviewMapBlock = new OverviewMapBlock(); overviewMapBlock.setArea(bounds); overviewMapBlock.setMapName(number); overviewMapBlock.setDescription(overviewDescription); } /** * Add a detail block. This describes and names one of the maps in the * map set. * @param detail The detail to add. */ public void addDetail(DetailMapBlock detail) { detailBlocks.add(detail); } public void write(String name) throws IOException { CheckedOutputStream stream = new CheckedOutputStream( new BufferedOutputStream(new FileOutputStream(name)), new CRC32()); if (headerBlock == null || overviewMapBlock == null) throw new IOException("Attempting to write file without being fully set up"); try { Block block = new Block(BLOCK_HEADER); headerBlock.write(block); block.write(stream); block = new Block(BLOCK_COPYRIGHT); copyrightBlock.write(block); block.write(stream); if (tdbVersion >= TDB_V407) { block = new Block(BLOCK_R); rblock.write(block); block.write(stream); } block = new Block(BLOCK_OVERVIEW); overviewMapBlock.write(block); block.write(stream); for (DetailMapBlock detail : detailBlocks) { block = new Block(BLOCK_DETAIL); detail.write(block); block.write(stream); } if (tdbVersion >= TDB_V407) { tblock.setSum(stream.getChecksum().getValue()); block = new Block(BLOCK_T); tblock.write(block); block.write(stream); } } finally { stream.close(); } } /** * Load from the given file name. * * @param ds The stream to read from. * @throws IOException For problems reading the file. */ private void load(StructuredInputStream ds) throws IOException { while (!ds.testEof()) { Block block = readBlock(ds); switch (block.getBlockId()) { case BLOCK_HEADER: headerBlock = new HeaderBlock(block); log.info("header block seen", headerBlock); break; case BLOCK_COPYRIGHT: log.info("copyright block"); copyrightBlock = new CopyrightBlock(block); break; case BLOCK_OVERVIEW: overviewMapBlock = new OverviewMapBlock(block); log.info("overview block", overviewMapBlock); break; case BLOCK_DETAIL: DetailMapBlock db = new DetailMapBlock(block); log.info("detail block", db); detailBlocks.add(db); break; default: log.warn("Unknown block in tdb file"); break; } } } /** * The file is divided into blocks. This reads a single block. * * @param is The input stream. * @return A block from the file. * @throws IOException For problems reading the file. */ private Block readBlock(StructuredInputStream is) throws IOException { int blockType = is.read(); if (blockType == -1) throw new EndOfFileException(); int blockLength = is.read2(); byte[] body = new byte[blockLength]; int n = is.read(body); if (n < 0) throw new IOException("failed to read block"); return new Block(blockType, body); } public int getTdbVersion() { return headerBlock.getTdbVersion(); } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/RBlock.java0000644000076400007640000000167611110054726021054 0ustar stevesteve/* * Copyright (C) 2008 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: 23-Jun-2008 */ package uk.me.parabola.tdbfmt; import java.io.IOException; import uk.me.parabola.io.StructuredOutputStream; /** * @author Steve Ratcliffe */ public class RBlock { private final String previewDescription = "Test preview map"; public void write(Block block) throws IOException { StructuredOutputStream os = block.getOutputStream(); os.write(0xc3); os.writeString(previewDescription); } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/TBlock.java0000644000076400007640000000256311033117613021051 0ustar stevesteve/* * Copyright (C) 2008 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: 23-Jun-2008 */ package uk.me.parabola.tdbfmt; import java.io.IOException; import uk.me.parabola.io.StructuredOutputStream; /** * @author Steve Ratcliffe */ public class TBlock { private long sum; public void write(Block block) throws IOException { StructuredOutputStream os = block.getOutputStream(); // If you change A,B,C or D the maps // will not load, you can change the rest without easily visible // problems although I suppose they must do something. // // A,B,C,D is a standard crc32 sum of the rest of the file. // (Andrzej Popowsk) os.write2(0); os.write((int) (sum >> 24)); // A os.write3(0); os.write3(0); os.write((int) (sum >> 16)); // B os.write2(0); os.write((int) (sum >> 8)); // C os.write4(0); os.write((int) sum); // D os.write2(0); } public void setSum(long sum) { this.sum = sum; } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/HeaderBlock.java0000644000076400007640000000725512175724544022060 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import java.io.IOException; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.io.StructuredOutputStream; /** * The header block. Identifies the particular map set. * * @author Steve Ratcliffe */ class HeaderBlock { /** The map family. */ private short familyId; /** A unique number associated with the map product */ private short productId; /** The version of TDB */ private final int tdbVersion; /** The series name is an overall name eg 'US Topo' */ private String seriesName; /** The version number of the map product */ private short productVersion; /** * Identifies a map within the series * @see #seriesName */ private String familyName; private byte enableProfile; private int codePage; HeaderBlock(int tdbVersion) { this.tdbVersion = tdbVersion; } HeaderBlock(Block block) throws IOException { StructuredInputStream ds = block.getInputStream(); productId = (short) ds.read2(); familyId = (short) ds.read2(); tdbVersion = ds.read2(); seriesName = ds.readString(); productVersion = (short) ds.read2(); familyName = ds.readString(); } public void write(Block block) throws IOException { StructuredOutputStream os = block.getOutputStream(); os.write2(productId); os.write2(familyId); os.write2(tdbVersion); os.writeString(seriesName); os.write2(productVersion); os.writeString(familyName); if (tdbVersion >= TdbFile.TDB_V407) { // Unknown purpose os.write(0); os.write(0x12); // lowest map level os.write(1); os.write(1); os.write(1); os.write4(0); os.write(0); os.write(0x18); // highest routable? 19 no, 21 ok os.write4(0); os.write4(0); os.write4(0); os.write4(0); os.write3(0); os.write4(codePage); os.write4(10000); os.write(1); // map is routable if (enableProfile == 1) os.write(1); // map has profile information else os.write(0); os.write(0); // map has DEM sub files } } // good //os.write(0); //os.write(0x12); //os.write(1); //os.write(1); //os.write(1); //os.write4(0); //os.write(0); //os.write(0x15); //os.write4(0); //os.write4(0); //os.write4(0); //os.write4(0); //os.write3(0); //os.write4(1252); //os.write4(10000); //os.write(1); //os.write(0); //os.write(0); public String toString() { return "TDB header: " + productId + " version=" + tdbVersion + ", series:" + seriesName + ", family:" + familyName + ", ver=" + productVersion ; } public void setProductId(short productId) { this.productId = productId; } public void setSeriesName(String seriesName) { this.seriesName = seriesName; } public void setFamilyName(String familyName) { this.familyName = familyName; } public void setProductVersion(short productVersion) { this.productVersion = productVersion; } public void setFamilyId(short familyId) { this.familyId = familyId; } void setCodePage(int codePage) { this.codePage = codePage; } public int getTdbVersion() { return tdbVersion; } public void setEnableProfile(byte enableProfile) { this.enableProfile = enableProfile; } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/package.html0000644000076400007640000000056211154317465021321 0ustar stevesteve

The TDB file format

The TDB file contains summary information about individual IMG files that make up a map set. It is usual for a map to be broken down into a set of tiles and the TDB file gives an overview of how they all fit together.

Acknowledgments

Based on the document Garmin TDB File Format by John Mechalas

mkgmap-r3660/src/uk/me/parabola/tdbfmt/CopyrightBlock.java0000644000076400007640000000336510763340724022631 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import uk.me.parabola.log.Logger; import uk.me.parabola.io.StructuredInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.HashSet; /** * A copyright block consists of a number of copyright segments. * * @author Steve Ratcliffe */ class CopyrightBlock { private static final Logger log = Logger.getLogger(CopyrightBlock.class); private final List segments = new ArrayList(); private final Set copySet = new HashSet(); CopyrightBlock() { } CopyrightBlock(Block block) throws IOException { StructuredInputStream ds = block.getInputStream(); while (!ds.testEof()) { CopyrightSegment segment = new CopyrightSegment(ds); log.info("segment: " + segment); segments.add(segment); } } public void write(Block block) throws IOException { for (CopyrightSegment seg : segments) { seg.write(block); } } /** * Add a copyright segment. We only add unique ones. * @param seg The copyright segment to add. */ public void addSegment(CopyrightSegment seg) { if (copySet.add(seg)) segments.add(seg); } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/DetailMapBlock.java0000644000076400007640000000715511532723256022522 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import java.io.IOException; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.io.StructuredOutputStream; /** * Details of a single .img file that is part of the map set. There will be * one of these for each .img file. * * @author Steve Ratcliffe */ public class DetailMapBlock extends OverviewMapBlock { private int tdbVersion; private String innername; // Sizes of the regions. It is possible that rgn and tre are reversed? private int rgnDataSize; private int treDataSize; private int lblDataSize; private int netDataSize; private int nodDataSize; public DetailMapBlock(int tdbVersion) { assert tdbVersion > 0; this.tdbVersion = tdbVersion; } /** * Initialise this block from the raw block given. * @param block The raw block read from the file. * @throws IOException For io problems. */ public DetailMapBlock(Block block) throws IOException { super(block); StructuredInputStream ds = block.getInputStream(); // First there are a couple of fields that we ignore. int junk = ds.read2(); assert junk == 4; junk = ds.read2(); assert junk == 3; // Sizes of the data rgnDataSize = ds.read4(); treDataSize = ds.read4(); lblDataSize = ds.read4(); // Another ignored field junk = ds.read(); assert junk == 1; } /** * Write into the given block. * * @param block The block that will have been initialised to be a detail * block. * @throws IOException Problems writing, probably can't really happen as * we use an array backed stream. */ public void write(Block block) throws IOException { super.write(block); StructuredOutputStream os = block.getOutputStream(); int n = 3; if (tdbVersion >= TdbFile.TDB_V407) { if (netDataSize > 0) n++; if (nodDataSize > 0) n++; } os.write2(n+1); os.write2(n); os.write4(treDataSize); os.write4(rgnDataSize); os.write4(lblDataSize); if (tdbVersion >= TdbFile.TDB_V407) { if (n > 3) os.write4(netDataSize); if (n > 4) os.write4(nodDataSize); //01 c3 00 ff os.write4(0xff00c301); os.write(0); os.write(0); os.write(0); String mn = getInnername(); os.writeString(mn + ".TRE"); os.writeString(mn + ".RGN"); os.writeString(mn + ".LBL"); if (n > 3) os.writeString(mn + ".NET"); if (n > 4) os.writeString(mn + ".NOD"); } else { os.write(1); } } public String getInnername() { return innername; } public void setInnername(String innername) { this.innername = innername; } public void setRgnDataSize(int rgnDataSize) { this.rgnDataSize = rgnDataSize; } public void setTreDataSize(int treDataSize) { this.treDataSize = treDataSize; } public void setLblDataSize(int lblDataSize) { this.lblDataSize = lblDataSize; } public void setNetDataSize(int netDataSize) { this.netDataSize = netDataSize; } public void setNodDataSize(int nodDataSize) { this.nodDataSize = nodDataSize; } public String toString() { return super.toString() + ", rgn size=" + rgnDataSize + ", tre size=" + treDataSize + ", lbl size" + lblDataSize ; } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/OverviewMapBlock.java0000644000076400007640000000620611532723256023122 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import java.io.IOException; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.io.StructuredOutputStream; /** * The overview map provides a low-detail image for the detailed maps. It * allows you to see what areas the detail maps cover so they can be selected * in programs such as QLandkarte and Garmin's MapSource. * * In addition to a low detail map, the overview map contains a number of type * 0x4a polygons. These definition areas a labeled after and correspond to * the detail map img files. * * The detail maps contain a background polygon (type 0x4b) that matches the * definition area in the overview map. * * @author Steve Ratcliffe */ public class OverviewMapBlock { private int mapNumber; private String mapName; private int parentMapNumber; private String description; private int maxLat; private int maxLong; private int minLat; private int minLong; public OverviewMapBlock() { description = "overview map"; } public OverviewMapBlock(Block block) throws IOException { StructuredInputStream ds = block.getInputStream(); mapNumber = ds.read4(); parentMapNumber = ds.read4(); maxLat = ds.read4(); maxLong = ds.read4(); minLat = ds.read4(); minLong = ds.read4(); description = ds.readString(); } public void write(Block block) throws IOException { StructuredOutputStream os = block.getOutputStream(); os.write4(mapNumber); os.write4(parentMapNumber); os.write4(maxLat); os.write4(maxLong); os.write4(minLat); os.write4(minLong); os.writeString(description); } public String toString() { return "Overview: " + mapNumber + ", parent=" + parentMapNumber + " covers " + '(' + toDegrees(minLat) + ',' + toDegrees(minLong) + ')' + '(' + toDegrees(maxLat) + ',' + toDegrees(maxLong) + ')' + " : " + description ; } private double toDegrees(int tdbunits) { return (double) tdbunits * 360 / Math.pow(2, 32); } public void setArea(Area bounds) { minLat = bounds.getMinLat() << 8; minLong = bounds.getMinLong() << 8; maxLat = bounds.getMaxLat() << 8; maxLong = bounds.getMaxLong() << 8; } public void setDescription(String description) { this.description = description; } public void setMapName(String mapName) { this.mapName = mapName; try { this.mapNumber = Integer.parseInt(mapName); } catch (NumberFormatException e) { this.mapNumber = 0; } } protected String getMapName() { return mapName; } public void setParentMapNumber(int parentMapNumber) { this.parentMapNumber = parentMapNumber; } } mkgmap-r3660/src/uk/me/parabola/tdbfmt/CopyrightSegment.java0000644000076400007640000000572011503736442023175 0ustar stevesteve/* * Copyright (C) 2007 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: 23-Sep-2007 */ package uk.me.parabola.tdbfmt; import uk.me.parabola.io.StructuredInputStream; import uk.me.parabola.io.StructuredOutputStream; import java.io.IOException; /** * One copyright that is within the copyright block. * * @author Steve Ratcliffe */ class CopyrightSegment { /** * Source information text string. Describes what data sources were used * in generating the map. */ public static final int CODE_SOURCE_INFORMATION = 0x00; /** Copyright information from the map manufacturer. */ public static final int CODE_COPYRIGHT_TEXT_STRING = 0x06; /** * A filename that contains a BMP image to be printed along with * the map. */ public static final int CODE_COPYRIGHT_BITMAP_REFERENCE = 0x07; /** * A code that shows what kind of copyright information is * contained in this segment. * The field {@link #extraProperties} can be used too as extra information. */ private final byte copyrightCode; private final byte whereCode; private final short extraProperties; private final String copyright; CopyrightSegment(StructuredInputStream ds) throws IOException { copyrightCode = (byte) ds.read(); whereCode = (byte) ds.read(); extraProperties = (short) ds.read2(); copyright = ds.readString(); } CopyrightSegment(int code, int where, String msg) { this.copyrightCode = (byte) code; this.whereCode = (byte) where; this.copyright = msg; this.extraProperties = 0; } public void write(Block block) throws IOException { StructuredOutputStream os = block.getOutputStream(); os.write(copyrightCode); os.write(whereCode); os.write2(extraProperties); os.writeString(copyright); } public String toString() { return "Copyright: " + copyrightCode + ", where=" + whereCode + ", extra=" + extraProperties + ": " + copyright ; } public boolean equals(Object o) { if (this == o) return true; if (o == null || !(o instanceof CopyrightSegment)) return false; CopyrightSegment that = (CopyrightSegment) o; if (copyrightCode != that.copyrightCode) return false; if (extraProperties != that.extraProperties) return false; if (whereCode != that.whereCode) return false; if (!copyright.equals(that.copyright)) return false; return true; } public int hashCode() { int result = (int) copyrightCode; result = 31 * result + (int) whereCode; result = 31 * result + (int) extraProperties; result = 31 * result + copyright.hashCode(); return result; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/0000755000076400007640000000000012651103763017035 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/fs/0000755000076400007640000000000012651103763017445 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/fs/FileSystem.java0000644000076400007640000000566011266100545022377 0ustar stevesteve/* * 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: 26-Nov-2006 */ package uk.me.parabola.imgfmt.fs; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import uk.me.parabola.imgfmt.FileExistsException; import uk.me.parabola.imgfmt.FileSystemParam; /** * File system operations. * * @author Steve Ratcliffe */ public interface FileSystem extends Closeable { /** * Create a new file it must not already exist. * @param name The file name. * @return A directory entry for the new file. * @throws FileExistsException If the file already exists. */ public ImgChannel create(String name) throws FileExistsException; /** * Open a file. The returned file object can be used to read and write the * underlying file. * * @param name The file name to open. * @param mode Either "r" for read access, "w" for write access or "rw" * for both read and write. * @return A file descriptor. * @throws FileNotFoundException When the file does not exist. */ public ImgChannel open(String name, String mode) throws FileNotFoundException; /** * Lookup the file and return a directory entry for it. * * @param name The filename to look up. * @return A directory entry. * @throws IOException If an error occurs reading the directory. */ public DirectoryEntry lookup(String name) throws IOException; /** * List all the files in the directory. * @return A List of directory entries. */ public List list() ; /** * Get the filesystem / archive parameters. Things that are stored in * the header. * * @return The filesystem parameters. */ public FileSystemParam fsparam(); /** * Reconfigure the filesystem with the given parameters. * Only some parameters can be changed and the may only be changeable * at certain points in the construction of a file system for example. * @param param The new parameters. * @throws IllegalStateException If the changes cannot be made (for example * if the file system is already written). */ public void fsparam(FileSystemParam param); /** * Sync with the underlying file. All unwritten data is written out to * the underlying file. * @throws IOException If an error occurs during the write. */ public void sync() throws IOException; /** * Close the filesystem. Any saved data is flushed out. It is better * to explicitly sync the data out first, to be sure that it has worked. */ void close(); } mkgmap-r3660/src/uk/me/parabola/imgfmt/fs/package.html0000644000076400007640000000057110637204532021727 0ustar stevesteve

Interfaces for accessing the filesystem

These are the way that application code should access the filesystem parts of the code.

The main interface is {@link uk.me.parabola.imgfmt.fs.FileSystem} which contains all the operations that can be performed on the system.

See also

The Garmin img project mkgmap-r3660/src/uk/me/parabola/imgfmt/fs/ImgChannel.java0000644000076400007640000000243111503736442022316 0ustar stevesteve/* * 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: 02-Dec-2006 */ package uk.me.parabola.imgfmt.fs; import java.nio.channels.ByteChannel; /** * An extension of ByteChannel that allows us to know the position. It may * evolve to have several of the methods of the FileChannel class and plays * a similar role. * * @author Steve Ratcliffe */ public interface ImgChannel extends ByteChannel { /** * Get the file position. Note that this is a logical position relative * to the beginning of the file (the file within the .img file, not the * beginning of the .img file itself). * * @return The offset in bytes from the beginning of the file. */ public long position(); /** * Set the position within the file. * * @param pos The position to set. */ public void position(long pos); } mkgmap-r3660/src/uk/me/parabola/imgfmt/fs/DirectoryEntry.java0000644000076400007640000000275111226342005023272 0ustar stevesteve/* * 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: 02-Dec-2006 */ package uk.me.parabola.imgfmt.fs; /** * Interface used for directory entries used to represent files. * A directory entry has the file name, its extension (its a 8+3 filename) * and the size of the file. * * @author Steve Ratcliffe */ public interface DirectoryEntry { public static final int ENTRY_SIZE = 512; public static final int SLOTS_PER_ENTRY = 240; /** * Get the file name. * @return The file name. */ public String getName(); /** * Get the file extension. * @return The file extension. */ public String getExt(); /** * Get the full name. That is name + extension. * * @return The full name as NAME.EXT */ public String getFullName(); /** * Get the file size. * @return The size of the file in bytes. */ public int getSize(); /** * If this is a special 'hidden' file. True for the all-spaces 'file'. * * @return True if this is not a regular file. */ public boolean isSpecial(); } mkgmap-r3660/src/uk/me/parabola/imgfmt/Utils.java0000644000076400007640000002526512371067413021012 0ustar stevesteve/* * 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: 03-Dec-2006 */ package uk.me.parabola.imgfmt; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Calendar; import java.util.Date; import java.util.zip.GZIPInputStream; import uk.me.parabola.imgfmt.app.Coord; /** * Some miscellaneous functions that are used within the .img code. * * @author Steve Ratcliffe */ public class Utils { /** * Routine to convert a string to bytes and pad with a character up * to a given length. * Only to be used for strings that are expressible in latin1. * * @param s The original string. * @param len The length to pad to. * @param pad The byte used to pad. * @return An array created from the string. */ public static byte[] toBytes(String s, int len, byte pad) { if (s == null) throw new IllegalArgumentException("null string provided"); byte[] out = new byte[len]; for (int i = 0; i < len; i++) { if (i > s.length()) { out[i] = pad; } else { out[i] = (byte) s.charAt(i); } } return out; } public static byte[] toBytes(String s) { return toBytes(s, s.length(), (byte) 0); } /** * Convert from bytes to a string. Only to be used when the character set * is ascii or latin1. * * @param buf A byte buffer to get the bytes from. Should be ascii or latin1. * @param off The offset into buf. * @param len The length to get. * @return A string. */ public static String bytesToString(ByteBuffer buf, int off, int len) { if (buf == null) throw new IllegalArgumentException("null byte buffer provided"); byte[] bbuf = new byte[len]; buf.position(off); buf.get(bbuf); char[] cbuf = new char[len]; for (int i = 0; i < bbuf.length; i++) { cbuf[i] = (char) bbuf[i]; } return new String(cbuf); } /** * Set the creation date. Note that the year is encoded specially. * * @param buf The buffer to write into. It must have been properly positioned * beforehand. * @param date The date to set. */ public static void setCreationTime(ByteBuffer buf, Date date) { Calendar cal = Calendar.getInstance(); if (date != null) cal.setTime(date); fillBufFromTime(buf, cal); } /** * A map unit is an integer value that is 1/(2^24) degrees of latitude or * longitude. * * @param l The lat or long as decimal degrees. * @return An integer value in map units. */ public static int toMapUnit(double l) { double DELTA = 360.0D / (1 << 24) / 2; //Correct rounding if (l > 0) return (int) ((l + DELTA) * (1 << 24)/360); else return (int) ((l - DELTA) * (1 << 24)/360); } /** * Convert a date into the in-file representation of a date. * * @param date The date. * @return A byte stream in .img format. */ public static byte[] makeCreationTime(Date date) { Calendar cal = Calendar.getInstance(); if (date != null) cal.setTime(date); byte[] ret = new byte[7]; ByteBuffer buf = ByteBuffer.wrap(ret); buf.order(ByteOrder.LITTLE_ENDIAN); fillBufFromTime(buf, cal); return ret; } private static void fillBufFromTime(ByteBuffer buf, Calendar cal) { buf.putChar((char) cal.get(Calendar.YEAR)); buf.put((byte) (cal.get(Calendar.MONTH)+1)); buf.put((byte) cal.get(Calendar.DAY_OF_MONTH)); buf.put((byte) cal.get(Calendar.HOUR_OF_DAY)); buf.put((byte) cal.get(Calendar.MINUTE)); buf.put((byte) cal.get(Calendar.SECOND)); } /** * Make a date from the garmin representation. * @param date The bytes representing the date. * @return A java date. */ public static Date makeCreationTime(byte[] date) { Calendar cal = Calendar.getInstance(); int y = ((date[1] & 0xff) << 8) + (date[0] & 0xff); cal.set(y, date[2]-1, date[3], date[4], date[5], date[6]); return cal.getTime(); } /** * Convert an angle in map units to degrees. */ public static double toDegrees(int val) { return (double) val * (360.0 / (1 << 24)); } /** * Convert an angle in map units to radians. */ public static double toRadians(int mapunits) { return toDegrees(mapunits) * (Math.PI / 180); } public static void closeFile(Closeable f) { if (f != null) { try { f.close(); } catch (IOException e) { e.printStackTrace(); } } } /** * Open a file and apply filters necessary for reading it such as * decompression. * * @param name The file to open. * @return A stream that will read the file, positioned at the beginning. * @throws FileNotFoundException If the file cannot be opened for any reason. */ public static InputStream openFile(String name) throws FileNotFoundException { InputStream is = new FileInputStream(name); if (name.endsWith(".gz")) { try { is = new GZIPInputStream(is); } catch (IOException e) { throw new FileNotFoundException( "Could not read as compressed file"); } } return is; } public static String joinPath(String dir, String basename, String ext) { return joinPath(dir, basename + "." + ext); } public static String joinPath(String dir, String basename) { File file = new File(dir, basename); return file.getAbsolutePath(); } /** * Rounds an integer up to the nearest multiple of {@code 2^shift}. * Works with both positive and negative integers. * @param val the integer to round up. * @param shift the power of two to round up to. * @return the rounded integer. */ public static int roundUp(int val, int shift) { return (val + (1 << shift) - 1) >>> shift << shift; } /** * Calculates the angle between the two segments (c1,c2),(c2,c3). * It is assumed that the segments are rhumb lines, not great circle paths. * @param c1 first point * @param c2 second point * @param c3 third point * @return angle between the two segments in degree [-180;180] */ public static double getAngle(Coord c1, Coord c2, Coord c3) { double a = c2.bearingTo(c1); double b = c2.bearingTo(c3); double angle = b - (a - 180); while(angle > 180) angle -= 360; while(angle < -180) angle += 360; return angle; } /** * Calculates the angle between the two segments (c1,c2),(c2,c3) * using the coords in map units. * @param c1 first point * @param c2 second point * @param c3 third point * @return angle between the two segments in degree [-180;180] */ public static double getDisplayedAngle(Coord c1, Coord c2, Coord c3) { return getAngle(c1.getDisplayedCoord(), c2.getDisplayedCoord(), c3.getDisplayedCoord()); } public final static int NOT_STRAIGHT = 0; public final static int STRAIGHT_SPIKE = 1; public final static int STRICTLY_STRAIGHT = 2; /** * Checks if the two segments (c1,c2),(c2,c3) form a straight line. * @param c1 first point * @param c2 second point * @param c3 third point * @return NOT_STRAIGHT, STRAIGHT_SPIKE or STRICTLY_STRAIGHT */ public static int isStraight(Coord c1, Coord c2, Coord c3) { if (c1.equals(c3)) return STRAIGHT_SPIKE; long area; // calculate the area that is enclosed by the three points // (as if a closing line is drawn from c3 back to c1) area = ((long)c1.getLongitude() * c2.getLatitude() - (long)c2.getLongitude() * c1.getLatitude()); area += ((long)c2.getLongitude() * c3.getLatitude() - (long)c3.getLongitude() * c2.getLatitude()); area += ((long)c3.getLongitude() * c1.getLatitude() - (long)c1.getLongitude() * c3.getLatitude()); if (area == 0){ // area is empty-> points lie on a straight line int delta1 = c1.getLatitude() - c2.getLatitude(); int delta2 = c2.getLatitude() - c3.getLatitude(); if (delta1 < 0 && delta2 > 0 || delta1 > 0 && delta2 < 0) return STRAIGHT_SPIKE; delta1 = c1.getLongitude() - c2.getLongitude(); delta2 = c2.getLongitude() - c3.getLongitude(); if (delta1 < 0 && delta2 > 0 || delta1 > 0 && delta2 < 0) return STRAIGHT_SPIKE; return STRICTLY_STRAIGHT; } // line is not straight return NOT_STRAIGHT; } /** * Checks if the two segments (c1,c2),(c2,c3) form a straight line * using high precision coordinates. * @param c1 first point * @param c2 second point * @param c3 third point * @return NOT_STRAIGHT, STRAIGHT_SPIKE or STRICTLY_STRAIGHT */ public static int isHighPrecStraight(Coord c1, Coord c2, Coord c3) { if (c1.highPrecEquals(c3)) return STRAIGHT_SPIKE; long area; long c1Lat = c1.getHighPrecLat(); long c2Lat = c2.getHighPrecLat(); long c3Lat = c3.getHighPrecLat(); long c1Lon = c1.getHighPrecLon(); long c2Lon = c2.getHighPrecLon(); long c3Lon = c3.getHighPrecLon(); // calculate the area that is enclosed by the three points // (as if a closing line is drawn from c3 back to c1) area = c1Lon * c2Lat - c2Lon * c1Lat; area += c2Lon * c3Lat - c3Lon * c2Lat; area += c3Lon * c1Lat - c1Lon * c3Lat; if (area == 0){ // area is empty-> points lie on a straight line long delta1 = c1Lat - c2Lat; long delta2 = c2Lat - c3Lat; if (delta1 < 0 && delta2 > 0 || delta1 > 0 && delta2 < 0) return STRAIGHT_SPIKE; delta1 = c1Lon - c2Lon; delta2 = c2Lon - c3Lon; if (delta1 < 0 && delta2 > 0 || delta1 > 0 && delta2 < 0) return STRAIGHT_SPIKE; return STRICTLY_STRAIGHT; } // line is not straight return NOT_STRAIGHT; } /** * approximate atan2, much faster than Math.atan2() * Based on a 50-year old arctan approximation due to Hastings */ private final static double PI_BY_2 = Math.PI / 2; // |error| < 0.005 public static double atan2_approximation( double y, double x ) { if ( x == 0.0f ) { if ( y > 0.0f ) return PI_BY_2 ; if ( y == 0.0f ) return 0.0f; return -PI_BY_2 ; } double atan; double z = y/x; if ( Math.abs( z ) < 1.0f ) { atan = z/(1.0f + 0.28f*z*z); if ( x < 0.0f ) { if ( y < 0.0f ) return atan - Math.PI; return atan + Math.PI; } } else { atan = PI_BY_2 - z/(z*z + 0.28f); if ( y < 0.0f ) return atan - Math.PI; } return atan; } /** * calculate a long value for the latitude and longitude of a coord * in high precision. * @param co * @return a long that can be used as a key in HashMaps */ public static long coord2Long(Coord co){ int lat30 = co.getHighPrecLat(); int lon30 = co.getHighPrecLon(); return (long)(lat30 & 0xffffffffL) << 32 | (lon30 & 0xffffffffL); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/MapFailedException.java0000644000076400007640000000531612333157050023401 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt; import uk.me.parabola.log.Logger; /** * Used for cases where the current map has failed to compile, but the error * is expected to be specific to the map (eg it is too big etc). When this * error is thrown it may be possible for other maps on the command line to * succeed. * * If the error is such that processing further maps is not likely to be * successful then use {@link ExitException} instead. * * @author Steve Ratcliffe */ public class MapFailedException extends RuntimeException { private static final Logger log = Logger.getLogger(MapFailedException.class); /** * Constructs a new runtime exception with the specified detail message. * The cause is not initialized, and may subsequently be initialized by a * call to {@link #initCause}. * * @param message the detail message. The detail message is saved for * later retrieval by the {@link #getMessage()} method. */ public MapFailedException(String message) { super(message); log(message); } /** * Constructs a new runtime exception with the specified detail message and * cause.

Note that the detail message associated with * cause is not automatically incorporated in * this runtime exception's detail message. * * @param message the detail message (which is saved for later retrieval * by the {@link #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) * @since 1.4 */ public MapFailedException(String message, Throwable cause) { super(message, cause); log(message); } private static void log(String message){ String thrownBy = ""; try{ StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); int callerPosInStack = 3; String[] caller = stackTraceElements[callerPosInStack].getClassName().split("\\."); thrownBy = "(thrown in " + caller[caller.length-1]+ "." +stackTraceElements[callerPosInStack].getMethodName() + "()) "; } catch(Exception e){ log.info(e); } log.error(thrownBy + message); } }mkgmap-r3660/src/uk/me/parabola/imgfmt/FormatException.java0000644000076400007640000000301511503736442023007 0ustar stevesteve/* * 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: 18-Dec-2006 */ package uk.me.parabola.imgfmt; /** * Thrown for any kind of malformed input to the mapping program. * * @author Steve Ratcliffe */ public class FormatException extends RuntimeException { /** * Constructs a new exception with the specified detail message and * cause.

Note that the detail message associated with * cause is not automatically incorporated in * this exception's detail message. * * @param message the detail message (which is saved for later retrieval * by the {@link #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) * @since 1.4 */ public FormatException(String message, Throwable cause) { super(message, cause); } public FormatException(String message) { super(message); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mdxfmt/0000755000076400007640000000000012651103763020334 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/mdxfmt/MdxFileReader.java0000644000076400007640000000300111532723256023646 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.mdxfmt; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * For reading the MDX file. * * @author Steve Ratcliffe */ public class MdxFileReader { private final ImgFileReader reader; private int numberOfMaps; private final List maps = new ArrayList(); public MdxFileReader(ImgChannel chan) { this.reader = new BufferedImgFileReader(chan); readHeader(); readMaps(); } private void readMaps() { for (int i = 0; i < numberOfMaps; i++) { MapInfo info = new MapInfo(); info.setHexMapname(reader.getInt()); info.setProductId(reader.getChar()); info.setFamilyId(reader.getChar()); info.setMapname(reader.getInt()); maps.add(info); } } private void readHeader() { reader.getInt(); reader.getChar(); reader.getInt(); numberOfMaps = reader.getInt(); } public List getMaps() { return maps; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mdxfmt/MapInfo.java0000644000076400007640000000330311532723256022531 0ustar stevesteve/* * 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 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.imgfmt.mdxfmt; import java.nio.ByteBuffer; /** * Represents an individual file in the MDX file. * * I don't really understand the difference between what I call hex mapname * and mapname. We shall always make them equal. * * There is no good reason to call it 'hexMapname' its just a name that stuck * I still don't know what the difference is. We always make them the same * but they can differ. */ public class MapInfo { private int hexMapname; private int mapname; private char familyId; private char productId; private String filename; void write(ByteBuffer os) { os.putInt(hexMapname); os.putChar(productId); os.putChar(familyId); os.putInt(mapname); } public int getHexMapname() { return hexMapname; } public void setHexMapname(int hexMapname) { this.hexMapname = hexMapname; } public void setMapname(int mapname) { this.mapname = mapname; } public void setFamilyId(char familyId) { this.familyId = familyId; } public void setProductId(char productId) { this.productId = productId; } public int getMapname() { return mapname; } public String getFilename() { return filename; } public void setFilename(String filename) { this.filename = filename; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mdxfmt/MdxFile.java0000644000076400007640000000733511532723256022541 0ustar stevesteve/* * 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 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.imgfmt.mdxfmt; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * The MDX index file. Used with the global index. This is located * at the family level in the windows registry and can perhaps index * across different products (and maybe families), although such a thing * hasn't been seen. * * @author Steve Ratcliffe */ public class MdxFile { private final char familyId; private final char productId; private final List maps = new ArrayList(); /** * Create with default family and product ids. * @param familyId The default family id that will be used if no other one * is supplied. * @param productId The default product id for the maps indexed by this * file. */ public MdxFile(int familyId, int productId) { this.familyId = (char) familyId; this.productId = (char) productId; } /** * Add a map with the default family and product id's and with equal * name and hex name. * @param name The map name (from the filename of the map) as an integer. * @param hexname The map id that is inside the TRE header * @param filename The file name of the map being added. Mainly for diagnostics, * it is not needed for the file. */ public void addMap(int name, int hexname, String filename) { MapInfo info = new MapInfo(); info.setHexMapname(hexname); info.setMapname(name); info.setFamilyId(familyId); info.setProductId(productId); info.setFilename(filename); maps.add(info); } /** * Write the file out to the given filename. */ public void write(String filename) throws IOException { FileOutputStream stream = new FileOutputStream(filename); FileChannel chan = stream.getChannel(); ByteBuffer buf = ByteBuffer.allocate(1024); buf.order(ByteOrder.LITTLE_ENDIAN); try { writeHeader(chan, buf); writeBody(chan, buf); } finally { chan.close(); } } private void writeHeader(WritableByteChannel chan, ByteBuffer buf) throws IOException { try { buf.put("Midx".getBytes("ascii")); } catch (UnsupportedEncodingException e) { throw new IOException("Could not write header"); } buf.putChar((char) 100); buf.putInt(12); buf.putInt(maps.size()); buf.flip(); chan.write(buf); } private void writeBody(WritableByteChannel chan, ByteBuffer buf) throws IOException { // Sort the maps by the hex number. Collections.sort(maps, new Comparator() { public int compare(MapInfo o1, MapInfo o2) { if (o1.getHexMapname() == o2.getHexMapname()) return 0; else if (o1.getHexMapname() < o2.getHexMapname()) return -1; else return 1; } }); for (MapInfo info : maps) { // Although its not necessarily wrong for them to be zero, it probably // sign that something is wrong. if (info.getHexMapname() == 0 || info.getMapname() == 0) System.err.println("Invalid mapname for " + info.getFilename() + ", perhaps it is not a .img file"); buf.compact(); info.write(buf); buf.flip(); chan.write(buf); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/0000755000076400007640000000000012651103763017653 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/sys/ImgFS.java0000644000076400007640000002464111763457463021506 0ustar stevesteve/* * 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: 26-Nov-2006 */ package uk.me.parabola.imgfmt.sys; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.List; import uk.me.parabola.imgfmt.FileExistsException; import uk.me.parabola.imgfmt.FileNotWritableException; import uk.me.parabola.imgfmt.FileSystemParam; import uk.me.parabola.imgfmt.fs.DirectoryEntry; import uk.me.parabola.imgfmt.fs.FileSystem; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The img file is really a filesystem containing several files. * It is made up of a header, a directory area and a data area which * occur in the filesystem in that order. * * @author steve */ public class ImgFS implements FileSystem { private static final Logger log = Logger.getLogger(ImgFS.class); // The directory is just like any other file, but with a name of 8+3 spaces static final String DIRECTORY_FILE_NAME = " . "; // This is the read or write channel to the real file system. private final FileChannel file; private boolean readOnly = true; // The header contains general information. private ImgHeader header; // There is only one directory that holds all filename and block allocation // information. private Directory directory; // The filesystem is responsible for allocating blocks private BlockManager fileBlockManager; // The header entries are written in 512 blocks, regardless of the block size of the file itself. private static final long ENTRY_BLOCK_SIZE = 512L; private BlockManager headerBlockManager; private byte xorByte; // if non-zero, all bytes are XORed with this /** * Private constructor, use the static {@link #createFs} and {@link #openFs} * routines to make a filesystem. * * @param chan The open file. */ private ImgFS(FileChannel chan) { file = chan; } /** * Create an IMG file from its external filesystem name and optionally some * parameters. * * @param filename The name of the file to be created. * @param params File system parameters. Can not be null. * @throws FileNotWritableException If the file can not be written to. */ public static FileSystem createFs(String filename, FileSystemParam params) throws FileNotWritableException { params.setFilename(filename); try { RandomAccessFile rafile = new RandomAccessFile(filename, "rw"); return createFs(rafile.getChannel(), params); } catch (FileNotFoundException e) { throw new FileNotWritableException("Could not create file: " + params.getFilename(), e); } } private static FileSystem createFs(FileChannel chan, FileSystemParam params) throws FileNotWritableException { assert params != null; // Truncate the file, because extra bytes beyond the end make for a // map that doesn't work on the GPS (although its likely to work in // other software viewers). try { chan.truncate(0); } catch (IOException e) { throw new FileNotWritableException("Failed to truncate file", e); } ImgFS fs = new ImgFS(chan); fs.createInitFS(chan, params); return fs; } /** * Open an existing IMG file system. * @param name The file name to open. * @return A File system that can be used lookup the internal files. * @throws FileNotFoundException When the file doesn't exist or can't be * read. */ public static FileSystem openFs(String name) throws FileNotFoundException { RandomAccessFile rafile = new RandomAccessFile(name, "r"); return openFs(name, rafile.getChannel()); } private static FileSystem openFs(String name, FileChannel chan) throws FileNotFoundException { ImgFS fs = new ImgFS(chan); try { fs.readInitFS(chan); } catch (IOException e) { throw new FileNotFoundException(name + ": " + e.getMessage()); } return fs; } /** * Create a new file, it must not already exist. * * @param name The file name. * @return A directory entry for the new file. */ public ImgChannel create(String name) throws FileExistsException { Dirent dir = directory.create(name, fileBlockManager); return new FileNode(file, dir, "w"); } /** * Open a file. The returned file object can be used to read and write the * underlying file. * * @param name The file name to open. * @param mode Either "r" for read access, "w" for write access or "rw" * for both read and write. * @return A file descriptor. * @throws FileNotFoundException When the file does not exist. */ public ImgChannel open(String name, String mode) throws FileNotFoundException { if (name == null || mode == null) throw new IllegalArgumentException("null argument"); if (mode.indexOf('r') >= 0) { Dirent ent = internalLookup(name); FileNode fn = new FileNode(file, ent, "r"); if(xorByte != 0) fn.setXorByte(xorByte); return fn; } else if (mode.indexOf('w') >= 0) { Dirent ent; try { ent = internalLookup(name); } catch (FileNotFoundException e) { try { ent = directory.create(name, fileBlockManager); } catch (FileExistsException e1) { // This shouldn't happen as we have just checked. throw new FileNotFoundException("Attempt to duplicate a file name"); } } return new FileNode(file, ent, "w"); } else { throw new IllegalArgumentException("Invalid mode given"); } } /** * Lookup the file and return a directory entry for it. * * @param name The filename to look up. * @return A directory entry. * @throws FileNotFoundException If an error occurs looking for the file, * including it not existing. */ public DirectoryEntry lookup(String name) throws FileNotFoundException { return internalLookup(name); } /** * List all the files in the directory. * * @return A List of directory entries. */ public List list() { return directory.getEntries(); } public FileSystemParam fsparam() { return header.getParams(); } public void fsparam(FileSystemParam param) { int reserved = param.getReservedDirectoryBlocks() + 2; fileBlockManager.setCurrentBlock(reserved); headerBlockManager.setMaxBlock(reserved); } /** * Sync with the underlying file. All unwritten data is written out to * the underlying file. * * @throws IOException If an error occurs during the write. */ public void sync() throws IOException { if (readOnly) return; header.setNumBlocks(fileBlockManager.getMaxBlockAllocated()); header.sync(); directory.sync(); } /** * Close the filesystem. Any saved data is flushed out. It is better * to explicitly sync the data out first, to be sure that it has worked. */ public void close() { try { sync(); } catch (IOException e) { log.debug("could not sync filesystem"); } finally { try { file.close(); } catch (IOException e) { log.warn("Could not close file"); } } } /** * Set up and ImgFS that has just been created. * * @param chan The real underlying file to write to. * @param params The file system parameters. * @throws FileNotWritableException If the file cannot be written for any * reason. */ private void createInitFS(FileChannel chan, FileSystemParam params) throws FileNotWritableException { readOnly = false; // The block manager allocates blocks for files. headerBlockManager = new BlockManager(params.getBlockSize(), 0); headerBlockManager.setMaxBlock(params.getReservedDirectoryBlocks()); // This bit is tricky. We want to use a regular ImgChannel to write // to the header and directory, but to create one normally would involve // it already existing, so it is created by hand. try { directory = new Directory(headerBlockManager, params.getDirectoryStartEntry()); Dirent ent = directory.create(DIRECTORY_FILE_NAME, headerBlockManager); ent.setSpecial(true); ent.setInitialized(true); FileNode f = new FileNode(chan, ent, "w"); directory.setFile(f); header = new ImgHeader(f); header.createHeader(params); } catch (FileExistsException e) { throw new FileNotWritableException("Could not create img file directory", e); } fileBlockManager = new BlockManager(params.getBlockSize(), params.getReservedDirectoryBlocks()); assert header != null; } /** * Initialise a filesystem that is going to be read from. We need to read * in the header including directory. * * @param chan The file channel to read from. * @throws IOException If the file cannot be read. */ private void readInitFS(FileChannel chan) throws IOException { ByteBuffer headerBuf = ByteBuffer.allocate(512); headerBuf.order(ByteOrder.LITTLE_ENDIAN); chan.read(headerBuf); xorByte = headerBuf.get(0); if(xorByte != 0) { byte[] headerBytes = headerBuf.array(); for(int i = 0; i < headerBytes.length; ++i) headerBytes[i] ^= xorByte; } if (headerBuf.position() < 512) throw new IOException("File too short or corrupted"); header = new ImgHeader(null); header.setHeader(headerBuf); FileSystemParam params = header.getParams(); BlockManager headerBlockManager = new BlockManager(params.getBlockSize(), 0); headerBlockManager.setMaxBlock(params.getReservedDirectoryBlocks()); directory = new Directory(headerBlockManager, params.getDirectoryStartEntry()); directory.setStartPos(params.getDirectoryStartEntry() * ENTRY_BLOCK_SIZE); Dirent ent = directory.create(DIRECTORY_FILE_NAME, headerBlockManager); FileNode f = new FileNode(chan, ent, "r"); header.setFile(f); directory.setFile(f); directory.readInit(xorByte); } /** * Lookup the file and return a directory entry for it. * * @param name The filename to look up. * @return A directory entry. * @throws FileNotFoundException If an error occurs reading the directory. */ private Dirent internalLookup(String name) throws FileNotFoundException { if (name == null) throw new IllegalArgumentException("null name argument"); Dirent ent = (Dirent) directory.lookup(name); if (ent == null) throw new FileNotFoundException(name + " not found"); return ent; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/FileNode.java0000644000076400007640000002156611256647151022221 0ustar stevesteve/* * 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: 03-Dec-2006 */ package uk.me.parabola.imgfmt.sys; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.NonReadableChannelException; import java.nio.channels.NonWritableChannelException; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The internal representation of a file in the file system. In use it * should only be referred to by the {@link ImgChannel} interface. * * @author Steve Ratcliffe */ public class FileNode implements ImgChannel { private static final Logger log = Logger.getLogger(FileNode.class); private boolean open; private boolean writeable; private boolean readable; private final FileChannel file; private final BlockManager blockManager; private final Dirent dirent; // The position in this file private long position; private byte xorByte; /** * Creates a new file in the file system. You can treat this just like * a regular file and write or read from it. * Operations to two different files may not be interleaved although * it may be possible to implement this. * * @param file The handle to the underlying file. * @param dir The directory entry associated with this file. * @param mode The mode "rw" for read and write etc. */ public FileNode(FileChannel file, Dirent dir, String mode) { this.file = file; this.dirent = dir; if (mode.indexOf('r') >= 0) readable = true; if (mode.indexOf('w') >= 0) writeable = true; if (!(readable || writeable)) throw new IllegalArgumentException("File must be readable or writeable"); blockManager = dir.getBlockManager(); if (blockManager == null) throw new IllegalArgumentException("no file system supplied"); open = true; } /** * Closes this channel. *

*

After a channel is closed, any further attempt to invoke I/O * operations upon it will cause a {@link ClosedChannelException} to be * thrown. *

*

If this channel is already closed then invoking this method has no * effect. *

*

This method may be invoked at any time. If some other thread has * already invoked it, however, then another invocation will block until * the first invocation is complete, after which it will return without * effect.

* * @throws IOException If an I/O error occurs */ public void close() throws IOException { if (!open) return; sync(); open = false; readable = false; writeable = false; } /** * Tells whether or not this channel is open.

* * @return true if, and only if, this channel is open */ public boolean isOpen() { return open; } /** * Reads a sequence of bytes from this channel into the given buffer. * * @param dst The buffer into which bytes are to be transferred * * @return The number of bytes read, possibly zero, or -1 if the * channel has reached end-of-stream * * @throws NonReadableChannelException If this channel was not opened for reading * @throws ClosedChannelException If this channel is closed * @throws AsynchronousCloseException If another thread closes this channel * while the read operation is in progress * @throws ClosedByInterruptException If another thread interrupts the * current thread while the read operation is in progress, thereby closing * the channel and setting the current thread's interrupt status * @throws IOException If some other I/O error occurs */ public int read(ByteBuffer dst) throws IOException { if (!open) throw new ClosedChannelException(); if (!readable) throw new NonReadableChannelException(); int blockSize = blockManager.getBlockSize(); long size = dst.remaining(); long fileSize = dirent.getSize(); if (position >= fileSize) return -1; size = Math.min(size, fileSize - position); int totalRead = 0; while (size > 0) { // Tet the logical block number, as we see it in our file. int lblock = (int) (position / blockSize); // Get the physical block number, the actual block number in // the underlying file. int pblock = dirent.getPhysicalBlock(lblock); if (pblock == 0xffff) { // We are at the end of the file. log.debug("at eof"); break; } // Position the underlying file int off = (int) (position - lblock*blockSize); file.position((long) pblock * blockSize + off); int n = (int) size; if (n > blockSize) n = blockSize; if (off != 0) n = Math.min(n, blockSize - off); dst.limit(dst.position() + n); int pos = dst.position(); int nr = file.read(dst); if (nr == -1) return -1; if (nr == 0) throw new IOException("Read nothing"); if(xorByte != 0) { byte[] bufBytes = dst.array(); for(int i = pos + n - 1; i >= pos; --i) bufBytes[i] ^= xorByte; } // Update the file positions size -= nr; position += nr; totalRead += nr; } log.debug("read ret", totalRead); return totalRead; } /** * Writes a sequence of bytes to this channel from the given buffer. *

*

An attempt is made to write up to r bytes to the channel, * where r is the number of bytes remaining in the buffer, that is, * dst.remaining(), at the moment this method is invoked. *

The logical block has to be converted to a physical block in the * underlying file. * * @param src The buffer from which bytes are to be retrieved * @return The number of bytes written, possibly zero * @throws NonWritableChannelException * If this channel was not opened for writing * @throws ClosedChannelException * If this channel is closed * @throws IOException If some other I/O error occurs */ public int write(ByteBuffer src) throws IOException { if (!open) throw new ClosedChannelException(); int blockSize = blockManager.getBlockSize(); // Get the size of this write int size = src.remaining(); // Loop over each block, this is to support the case (which we may // not implement) of non-contiguous blocks. int totalWritten = 0; while (size > 0) { // Get the logical block, ie the block as we see it in our file. int lblock = (int) (position/blockSize); // First need to allocate enough blocks for this write. First check // if the block exists already int pblock = dirent.getPhysicalBlock(lblock); log.debug("lblock / pblock", lblock, '/', pblock); if (pblock == 0xffff) { log.debug("allocating new block"); pblock = blockManager.allocate(); dirent.addBlock(pblock); } // Position the underlying file, so that it is in the correct place. int off = (int) (position - lblock*blockSize); file.position((long) pblock * blockSize + off); int n = size; if (n > blockSize) n = blockSize; if (off != 0) n = Math.min(n, blockSize - off); src.limit(src.position() + n); // Write to the underlying file. int nw = file.write(src); if (nw == 0) throw new IOException("Wrote nothing"); // Update the file positions size -= nw; position += nw; totalWritten += nw; // Update file size. if (position > dirent.getSize()) dirent.setSize((int) position); } return totalWritten; } public long position() { return position; } public void position(long pos) { int blockSize = blockManager.getBlockSize(); while (pos > position) { long lblock = position / blockSize; int pblock = dirent.getPhysicalBlock((int) lblock); if (pblock == 0xffff) { if (writeable) { log.debug("setting position allocating new block", lblock); pblock = blockManager.allocate(); dirent.addBlock(pblock); } } position = (lblock+1) * blockSize; } this.position = pos; } /** * Write out any unsaved data to disk. * * @throws IOException If there is an error writing to disk. */ private void sync() throws IOException { if (!writeable) return; // Ensure that a complete block is written out. int bs = blockManager.getBlockSize(); long rem = bs - (file.position() % bs); ByteBuffer buf = ByteBuffer.allocate(blockManager.getBlockSize()); // Complete any partial block. for (int i = 0; i < rem; i++) buf.put((byte) 0); buf.flip(); file.write(buf); } public void setXorByte(byte xorByte) { this.xorByte = xorByte; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/BlockManager.java0000644000076400007640000000447511562201364023051 0ustar stevesteve/* * Copyright (C) 2007 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: 25-Oct-2007 */ package uk.me.parabola.imgfmt.sys; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.log.Logger; /** * This is used to allocate blocks for files in the filesystem/archive. * * @author Steve Ratcliffe */ class BlockManager { private static final Logger log = Logger.getLogger(BlockManager.class); private final int blockSize; private int currentBlock; private int maxBlock = 0xfffe; private int maxBlockAllocated; private final int initialBlock; BlockManager(int blockSize, int initialBlock) { this.blockSize = blockSize; this.currentBlock = initialBlock; this.initialBlock = initialBlock; this.maxBlockAllocated = initialBlock; } /** * Well the algorithm is pretty simple - you just get the next unused block * number. * * @return A block number that is free to be used. */ public int allocate() { int n = currentBlock++; if (maxBlock > 0 && n > maxBlock) { log.error("overflowed directory with max block " + maxBlock + ", current=" + n); // This problem is fixable so give some useful advice on what // to do about it String message = String.format("Too many blocks." + " Use a larger block size with an option such as" + " --block-size=%d or --block-size=%d", blockSize * 2, blockSize * 4); throw new MapFailedException(message); } maxBlockAllocated++; return n; } public int getBlockSize() { return blockSize; } public int getMaxBlock() { return maxBlock; } public void setMaxBlock(int maxBlock) { this.maxBlock = maxBlock; } public void setCurrentBlock(int n) { if (maxBlockAllocated != initialBlock) throw new IllegalStateException("Blocks already allocated"); currentBlock = n; maxBlockAllocated = n; } public int getMaxBlockAllocated() { return maxBlockAllocated; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/Dirent.java0000644000076400007640000001543011503736442021747 0ustar stevesteve/* * 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: 30-Nov-2006 */ package uk.me.parabola.imgfmt.sys; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.fs.DirectoryEntry; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * An entry within a directory. This holds its name and a list * of blocks that go to make up this file. * * A directory entry may take more than block in the file system. * *

All documentation seems to point to the block numbers having to be * contiguous, but seems strange so I shall experiment. * *

Entries are in blocks of 512 bytes, regardless of the block size. * * @author Steve Ratcliffe */ class Dirent implements DirectoryEntry { protected static final Logger log = Logger.getLogger(Dirent.class); // Constants. static final int MAX_FILE_LEN = 8; static final int MAX_EXT_LEN = 3; // Offsets static final int OFF_FILE_USED = 0x00; static final int OFF_NAME = 0x01; static final int OFF_EXT = 0x09; static final int OFF_FLAG = 0x10; static final int OFF_FILE_PART = 0x11; private static final int OFF_SIZE = 0x0c; // File names are a base+extension private String name; private String ext; // The file size. private int size; private final BlockManager blockManager; // The block table holds all the blocks that belong to this file. The // documentation suggests that block numbers are always contiguous. private final BlockTable blockTable; private boolean special; private static final int OFF_USED_FLAG = 0; private boolean initialized; Dirent(String name, BlockManager blockManager) { this.blockManager = blockManager; int dot = name.lastIndexOf('.'); if (dot >= 0) { setName(name.substring(0, dot)); setExt(name.substring(dot+1)); } else throw new IllegalArgumentException("Filename did not have dot"); blockTable = new BlockTable(); } /** * Write this entry out to disk. Note that these are 512 bytes, regardless * of the block size. * * @param file The file to write to. * @throws IOException If writing fails for any reason. */ void sync(ImgChannel file) throws IOException { int ntables = blockTable.getNBlockTables(); ByteBuffer buf = ByteBuffer.allocate(ENTRY_SIZE * ntables); buf.order(ByteOrder.LITTLE_ENDIAN); for (int part = 0; part < ntables; part++) { log.debug("position at part", part, "is", buf.position()); buf.put((byte) 1); buf.put(Utils.toBytes(name, MAX_FILE_LEN, (byte) ' ')); buf.put(Utils.toBytes(ext, MAX_EXT_LEN, (byte) ' ')); // Size is only present in the first part if (part == 0) { log.debug("dirent", name, '.', ext, "size is going to", size); buf.putInt(size); } else { buf.putInt(0); } buf.put((byte) (special? 0x3: 0)); buf.putChar((char) part); // Write out the allocation of blocks for this entry. buf.position(ENTRY_SIZE * part + 0x20); blockTable.writeTable(buf, part); } buf.flip(); file.write(buf); } /** * Get the file name. * * @return The file name. */ public String getName() { return name; } /** * Get the file extension. * * @return The file extension. */ public String getExt() { return ext; } /** * The full name is of the form 8+3 with a dot in between the name and * extension. The full name is used as the index in the directory. * * @return The full name. */ public String getFullName() { return name + '.' + ext; } /** * Read in the block numbers from the given buffer. If this is the first * directory block for this file, then the size is set too. * * @param buf The data as read from the file. */ void initBlocks(ByteBuffer buf) { byte used = buf.get(OFF_USED_FLAG); if (used != 1) return; int part = buf.get(OFF_FILE_PART) & 0xff; if (part == 0 || (isSpecial() && part == 3)) size = buf.getInt(OFF_SIZE); blockTable.readTable(buf); initialized = true; } /** * Get the file size. * * @return The size of the file in bytes. */ public int getSize() { return size; } /** * Set the file name. The name should be exactly eight characters long * and it is truncated or left padded with zeros to make this true. * * @param name The file name. */ private void setName(String name) { int len = name.length(); if (len > MAX_FILE_LEN) { this.name = name.substring(0, 8); } else if (len < MAX_FILE_LEN) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < MAX_FILE_LEN - len; i++) { sb.append('0'); } sb.append(name); this.name = sb.toString(); } else this.name = name; } /** * Set the file extension. Can't be longer than three characters. * @param ext The file extension. */ private void setExt(String ext) { log.debug("ext len", ext.length()); if (ext.length() != MAX_EXT_LEN) throw new IllegalArgumentException("File extension is wrong size"); this.ext = ext; } /** * The number of blocks that the header covers. The header includes * the directory for the purposes of this routine. * * @return The total number of header basic blocks (blocks of 512 bytes). */ int numberHeaderBlocks() { return blockTable.getNBlockTables(); } void setSize(int size) { if (log.isDebugEnabled()) log.debug("setting size", getName(), getExt(), "to", size); this.size = size; } /** * Add a block without increasing the size of the file. * * @param n The block number. */ void addBlock(int n) { blockTable.addBlock(n); } /** * Set for the first directory entry that covers the header and directory * itself. * * @param special Set to true to mark as the special first entry. */ public void setSpecial(boolean special) { this.special = special; } public boolean isSpecial() { return special; } /** * Converts from a logical block to a physical block. If the block does * not exist then 0xffff will be returned. * * @param lblock The logical block in the file. * @return The corresponding physical block in the filesystem. */ public int getPhysicalBlock(int lblock) { return blockTable.physFromLogical(lblock); } public BlockManager getBlockManager() { return blockManager; } protected void setInitialized(boolean initialized) { this.initialized = initialized; } protected boolean isInitialized() { return initialized; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/FileImgChannel.java0000644000076400007640000000465111532723256023333 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 14, 2007 */ package uk.me.parabola.imgfmt.sys; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * This is an implementation of ImgChannel that delegates to a regular channel. * It can therefore be used to read or write regular files on the file system. * * @author Steve Ratcliffe */ public class FileImgChannel implements ImgChannel { private final FileChannel channel; private long position; public FileImgChannel(String filename, String mode) { RandomAccessFile raf; try { raf = new RandomAccessFile(filename, mode); } catch (FileNotFoundException e) { throw new ReadFailedException("Could not open " + filename, e); } this.channel = raf.getChannel(); } public FileImgChannel(FileChannel channel) { this.channel = channel; } public int read(ByteBuffer dst) throws IOException { int n = channel.read(dst); if (n > 0) position += n; return n; } public boolean isOpen() { return channel.isOpen(); } public void close() throws IOException { channel.close(); } public int write(ByteBuffer src) throws IOException { int n = channel.write(src); position += n; return n; } /** * Get the file position. Note that this is a logical position relative to the * beginning of the file (the file within the .img file, not the beginning of the * .img file itself). * * @return The offset in bytes from the beginning of the file. */ public long position() { return position; } /** * Set the position within the file. * * @param pos The position to set. */ public void position(long pos) { try { channel.position(pos); position = pos; } catch (IOException e) { throw new ReadFailedException("Could not seek", e); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/package.html0000644000076400007640000000056611154317465022146 0ustar stevesteve

Filesystem implementation

This holds the filesystem implementation for the .img format. I refer through out to a file system containing files, rather than using the term sub-files as in John Mechalas' document.

Another way to look at it (and probably a better one) is that it a kind of archive format just like a .zip file.

mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/BlockTable.java0000644000076400007640000000713111503736442022523 0ustar stevesteve/* * Copyright (C) 2007 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: 03-Feb-2007 */ package uk.me.parabola.imgfmt.sys; import uk.me.parabola.log.Logger; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Holds block numbers for a file. It is part of the directory. For a file * that needs more than one block several directory entries exist. Each of * these has the header with the file name etc. in it, but the first one has * extra flags and info. * *

What is important here is that only part of a full block is used to * hold block numbers. * *

The entries are 512 bytes regardless of the block size. * * @author Steve Ratcliffe */ class BlockTable { private static final Logger log = Logger.getLogger(BlockTable.class); // Offset of the block table in the directory entry block. private static final int BLOCKS_TABLE_START = 0x20; private static final int ENTRY_SIZE = 512; private static final int TABLE_SIZE = (ENTRY_SIZE - BLOCKS_TABLE_START)/2; //private final int tableSize; private int curroff; private final List blocks; private char[] currTable; BlockTable() { blocks = new ArrayList(200); } /** * Write out the specified table to the given buffer. * * @param buf The buffer to write to. * @param n The number of the block table to write out. */ public void writeTable(ByteBuffer buf, int n) { char[] cbuf = blocks.get(n); log.debug("block with length", cbuf.length); for (char c : cbuf) { buf.putChar(c); } } /** * Read a block table from the given buffer. The table is added to the * list. * @param buf The buffer to read from. */ public void readTable(ByteBuffer buf) { buf.position(BLOCKS_TABLE_START); buf.limit(ENTRY_SIZE); char[] cbuf = newTable(); for (int i = 0; i < cbuf.length; i++) { char c = buf.getChar(); cbuf[i] = c; } } /** * Add the given block number to this directory. * * @param n The block number to add. */ public void addBlock(int n) { char[] thisTable = currTable; if (curroff >= TABLE_SIZE || currTable == null) thisTable = newTable(); thisTable[curroff++] = (char) n; } /** * Given a logical block number, return the physical block number. * * @param lblock The logical block number, ie with respect to the file. * @return The physical block number in the file system. */ public int physFromLogical(int lblock) { int blockNum = lblock / TABLE_SIZE; int offset = lblock - blockNum * TABLE_SIZE; if (blockNum >= blocks.size()) return 0xffff; char[] cbuf = blocks.get(blockNum); return cbuf[offset]; } /** * Get the number of block tables. This is the number of blocks that * will be used in the on disk directory structure. * * @return The number of blocks tables. */ public int getNBlockTables() { return blocks.size(); } /** * Allocate a new block to hold more directory block numbers. * * @return Array for more numbers. */ private char[] newTable() { char[] table = new char[TABLE_SIZE]; Arrays.fill(table, (char) 0xffff); curroff = 0; blocks.add(table); currTable = table; return table; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/Directory.java0000644000076400007640000001465712323435234022474 0ustar stevesteve/* * 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: 26-Nov-2006 */ package uk.me.parabola.imgfmt.sys; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.FileExistsException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.fs.DirectoryEntry; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The directory. There is only one directory and it contains the * file names and block information. On disk each entry is a * multiple of the block size. * * @author Steve Ratcliffe */ class Directory { private static final Logger log = Logger.getLogger(Directory.class); //private final FileChannel file; private ImgChannel chan; private final BlockManager headerBlockManager; private final int startEntry; private long startPos; // The list of files themselves. private final Map entries = new LinkedHashMap(); Directory(BlockManager headerBlockManager, int startEntry) { this.headerBlockManager = headerBlockManager; this.startEntry = startEntry; } /** * Create a new file in the directory. * * @param name The file name. Must be 8+3 characters. * @param blockManager To allocate blocks for the created file entry. * @return The new directory entity. * @throws FileExistsException If the entry already * exists. */ Dirent create(String name, BlockManager blockManager) throws FileExistsException { // Check to see if it is already there. if (entries.get(name) != null) throw new FileExistsException("File " + name + " already exists"); Dirent ent; if (name.equals(ImgFS.DIRECTORY_FILE_NAME)) { ent = new HeaderDirent(name, blockManager); } else { ent = new Dirent(name, blockManager); } addEntry(ent); return ent; } /** * Initialise the directory for reading the file. The whole directory * is read in. * * @throws IOException If it cannot be read. */ void readInit(byte xorByte) throws IOException { assert chan != null; ByteBuffer buf = ByteBuffer.allocate(512); buf.order(ByteOrder.LITTLE_ENDIAN); chan.position(startPos); Dirent current = null; while ((chan.read(buf)) > 0) { buf.flip(); if(xorByte != 0) { byte[] bufBytes = buf.array(); for(int i = 0; i < bufBytes.length; ++i) bufBytes[i] ^= xorByte; } int used = buf.get(Dirent.OFF_FILE_USED); if (used != 1) continue; String name = Utils.bytesToString(buf, Dirent.OFF_NAME, Dirent.MAX_FILE_LEN); String ext = Utils.bytesToString(buf, Dirent.OFF_EXT, Dirent.MAX_EXT_LEN); log.debug("readinit name", name, ext); int flag = buf.get(Dirent.OFF_FLAG); int part = buf.get(Dirent.OFF_FILE_PART) & 0xff; if (flag == 3 && current == null) { current = (Dirent) entries.get(ImgFS.DIRECTORY_FILE_NAME); current.initBlocks(buf); } else if (part == 0) { current = create(name + '.' + ext, headerBlockManager); current.initBlocks(buf); } else { assert current != null; current.initBlocks(buf); } buf.clear(); } } /** * Write out the directory to the file. The file should be correctly * positioned by the caller. * * @throws IOException If there is a problem writing out any * of the directory entries. */ public void sync() throws IOException { // The first entry can't really be written until the rest of the directory is // so we have to step through once to calculate the size and then again // to write it out. int headerEntries = 0; for (DirectoryEntry dir : entries.values()) { Dirent ent = (Dirent) dir; log.debug("ent size", ent.getSize()); int n = ent.numberHeaderBlocks(); headerEntries += n; } // Save the current position long dirPosition = chan.position(); int blockSize = headerBlockManager.getBlockSize(); // Get the number of blocks required for the directory entry representing the header. // First calculate the number of blocks required for the directory entries. int headerBlocks = (int) Math.ceil((startEntry + 1.0 + headerEntries) * DirectoryEntry.ENTRY_SIZE / blockSize); int forHeader = (headerBlocks + DirectoryEntry.ENTRY_SIZE - 1)/DirectoryEntry.ENTRY_SIZE; log.debug("header blocks needed", forHeader); // There is nothing really wrong with larger values (perhaps, I don't // know for sure!) but the code is written to make it 1, so make sure that it is. assert forHeader == 1; // Write the blocks that will will contain the header blocks. chan.position(dirPosition + (long) forHeader * DirectoryEntry.ENTRY_SIZE); for (DirectoryEntry dir : entries.values()) { Dirent ent = (Dirent) dir; if (!ent.isSpecial()) { log.debug("wrting ", dir.getFullName(), " at ", chan.position()); log.debug("ent size", ent.getSize()); ent.sync(chan); } } long end = (long) blockSize * headerBlockManager.getMaxBlock(); ByteBuffer buf = ByteBuffer.allocate((int) (end - chan.position())); for (int i = 0; i < buf.capacity(); i++) buf.put((byte) 0); buf.flip(); chan.write(buf); // Now go back and write in the directory entry for the header. chan.position(dirPosition); Dirent ent = (Dirent) entries.values().iterator().next(); log.debug("ent header size", ent.getSize()); ent.sync(chan); } /** * Get the entries. Used for listing the directory. * * @return A list of the directory entries. They will be in the same * order as in the file. */ public List getEntries() { return new ArrayList(entries.values()); } /** * Add an entry to the directory. * * @param ent The entry to add. */ private void addEntry(DirectoryEntry ent) { entries.put(ent.getFullName(), ent); } public void setFile(ImgChannel chan) { this.chan = chan; } public void setStartPos(long startPos) { this.startPos = startPos; } public DirectoryEntry lookup(String name) { return entries.get(name); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/ImgHeader.java0000644000076400007640000003056511771656621022364 0ustar stevesteve/* * 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: 26-Nov-2006 */ package uk.me.parabola.imgfmt.sys; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Calendar; import java.util.Date; import uk.me.parabola.imgfmt.FileSystemParam; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; import static java.util.Arrays.asList; /** * The header at the very beginning of the .img filesystem. It has the * same signature as a DOS partition table, although I don't know * exactly how much the partition concepts are used. * * @author Steve Ratcliffe */ class ImgHeader { private static final Logger log = Logger.getLogger(ImgHeader.class); // Offsets into the header. private static final int OFF_XOR = 0x0; private static final int OFF_UPDATE_MONTH = 0xa; private static final int OFF_UPDATE_YEAR = 0xb; // +1900 for val >= 0x63, +2000 for less private static final int OFF_SUPP = 0xe; // Appears to be set for gmapsupp files private static final int OFF_CHECKSUM = 0xf; private static final int OFF_SIGNATURE = 0x10; private static final int OFF_UNK_1 = 0x17; // If this was a real boot sector these would be the meanings private static final int OFF_SECTORS = 0x18; private static final int OFF_HEADS = 0x1a; private static final int OFF_CYLINDERS = 0x1c; private static final int OFF_CREATION_DATE = 0x39; // The block number where the directory starts. private static final int OFF_DIRECTORY_START_BLOCK = 0x40; private static final int OFF_MAP_FILE_INTENTIFIER = 0x41; private static final int OFF_MAP_DESCRIPTION = 0x49; // 0x20 padded private static final int OFF_HEADS2 = 0x5d; private static final int OFF_SECTORS2 = 0x5f; private static final int OFF_BLOCK_SIZE_EXPONENT1 = 0x61; private static final int OFF_BLOCK_SIZE_EXPONENT2 = 0x62; private static final int OFF_BLOCK_SIZE = 0x63; // private static final int OFF_UKN_3 = 0x63; private static final int OFF_MAP_NAME_CONT = 0x65; // 'Partition table' offsets. private static final int OFF_START_HEAD = 0x1bf; private static final int OFF_START_SECTOR = 0x1c0; private static final int OFF_START_CYLINDER = 0x1c1; private static final int OFF_SYSTEM_TYPE = 0x1c2; private static final int OFF_END_HEAD = 0x1c3; private static final int OFF_END_SECTOR = 0x1c4; private static final int OFF_END_CYLINDER = 0x1c5; private static final int OFF_REL_SECTORS = 0x1c6; private static final int OFF_NUMBER_OF_SECTORS = 0x1ca; private static final int OFF_PARTITION_SIG = 0x1fe; // Lengths of some of the fields private static final int LEN_MAP_NAME_CONT = 30; private static final int LEN_MAP_DESCRIPTION = 20; private FileSystemParam fsParams; private final ByteBuffer header = ByteBuffer.allocate(512); private ImgChannel file; private Date creationTime; private int sectorsPerTrack; private int headsPerCylinder; // Signatures. private static final byte[] FILE_ID = { 'G', 'A', 'R', 'M', 'I', 'N', '\0'}; private static final byte[] SIGNATURE = { 'D', 'S', 'K', 'I', 'M', 'G', '\0'}; private int numBlocks; ImgHeader(ImgChannel chan) { this.file = chan; header.order(ByteOrder.LITTLE_ENDIAN); } /** * Create a header from scratch. * @param params File system parameters. */ void createHeader(FileSystemParam params) { this.fsParams = params; header.put(OFF_XOR, (byte) 0); // Set the block size. 2^(E1+E2) where E1 is always 9. int exp = 9; int bs = params.getBlockSize(); for (int i = 0; i < 32; i++) { bs >>>= 1; if (bs == 0) { exp = i; break; } } if (exp < 9) throw new IllegalArgumentException("block size too small"); header.put(OFF_BLOCK_SIZE_EXPONENT1, (byte) 0x9); header.put(OFF_BLOCK_SIZE_EXPONENT2, (byte) (exp - 9)); header.position(OFF_SIGNATURE); header.put(SIGNATURE); header.position(OFF_MAP_FILE_INTENTIFIER); header.put(FILE_ID); header.put(OFF_UNK_1, (byte) 0x2); // Actually this may not be the directory start block, I am guessing - // always assume it is 2 anyway. header.put(OFF_DIRECTORY_START_BLOCK, (byte) fsParams.getDirectoryStartEntry()); header.position(OFF_CREATION_DATE); Utils.setCreationTime(header, creationTime); setDirectoryStartEntry(params.getDirectoryStartEntry()); // Set the times. Date date = new Date(); setCreationTime(date); setUpdateTime(date); setDescription(params.getMapDescription()); // Checksum is not checked. header.put(OFF_CHECKSUM, (byte) 0); } /** * Write out the values associated with the partition sizes. * * @param blockSize Block size. */ private void writeSizeValues(int blockSize) { int endSector = (int) (((numBlocks+1L) * blockSize + 511) / 512); //System.out.printf("end sector %d %x\n", endSector, endSector); // We have three maximum values for sectors, heads and cylinders. We attempt to find values // for them that are larger than the sectorsPerTrack = 32; // 6 bit value headsPerCylinder = 128; int cyls = 0x400; // Try out various values of h, s and c until we find a combination that is large enough. // I'm not entirely sure about the valid values, but it seems that only certain values work // which is why we use values from a list. // See: http://www.win.tue.nl/~aeb/partitions/partition_types-2.html for justification for the h list out: for (int h : asList(16, 32, 64, 128, 256)) { for (int s : asList(4, 8, 16, 32)) { for (int c : asList(0x20, 0x40, 0x80, 0x100, 0x200, 0x3ff)) { log.info("shc=", s + "," + h + "," + c, "end=", endSector); //System.out.println("shc=" + s + "," + h + "," + c + "end=" + endSector); if (s * h * c > endSector) { headsPerCylinder = h; sectorsPerTrack = s; cyls = c; break out; } } } } // This sectors, head, cylinders stuff appears to be used by mapsource // and they have to be larger than the actual size of the map. It // doesn't appear to have any effect on a garmin device or other software. header.putShort(OFF_SECTORS, (short) sectorsPerTrack); header.putShort(OFF_SECTORS2, (short) sectorsPerTrack); header.putShort(OFF_HEADS, (short) headsPerCylinder); header.putShort(OFF_HEADS2, (short) headsPerCylinder); header.putShort(OFF_CYLINDERS, (short) cyls); // Since there are only 2 bytes here it can overflow, if it // does we replace it with 0xffff. int blocks = (int) (endSector * 512L / blockSize); char shortBlocks = blocks > 0xffff ? 0xffff : (char) blocks; header.putChar(OFF_BLOCK_SIZE, shortBlocks); header.put(OFF_PARTITION_SIG, (byte) 0x55); header.put(OFF_PARTITION_SIG + 1, (byte) 0xaa); // Partition starts at zero. This is 0,0,1 in CHS terms. header.put(OFF_START_HEAD, (byte) 0); header.put(OFF_START_SECTOR, (byte) 1); header.put(OFF_START_CYLINDER, (byte) 0); header.put(OFF_SYSTEM_TYPE, (byte) 0); // Now calculate the CHS address of the last sector of the partition. CHS chs = new CHS(endSector - 1); header.put(OFF_END_HEAD, (byte) (chs.h)); header.put(OFF_END_SECTOR, (byte) ((chs.s) | ((chs.c >> 2) & 0xc0))); header.put(OFF_END_CYLINDER, (byte) (chs.c & 0xff)); // Write the LBA block address of the beginning and end of the partition. header.putInt(OFF_REL_SECTORS, 0); header.putInt(OFF_NUMBER_OF_SECTORS, endSector); log.info("number of blocks", endSector - 1); } void setHeader(ByteBuffer buf) { buf.flip(); header.put(buf); byte exp1 = header.get(OFF_BLOCK_SIZE_EXPONENT1); byte exp2 = header.get(OFF_BLOCK_SIZE_EXPONENT2); log.debug("header exponent", exp1, exp2); fsParams = new FileSystemParam(); fsParams.setBlockSize(1 << (exp1 + exp2)); fsParams.setDirectoryStartEntry(header.get(OFF_DIRECTORY_START_BLOCK)); StringBuffer sb = new StringBuffer(); sb.append(Utils.bytesToString(buf, OFF_MAP_DESCRIPTION, LEN_MAP_DESCRIPTION)); sb.append(Utils.bytesToString(buf, OFF_MAP_NAME_CONT, LEN_MAP_NAME_CONT)); fsParams.setMapDescription(sb.toString().trim()); byte h = header.get(OFF_END_HEAD); byte sc1 = header.get(OFF_END_SECTOR); byte sc2 = header.get(OFF_END_CYLINDER); CHS chs = new CHS(); chs.setFromPartition(h, sc1, sc2); int lba = chs.toLba(); log.info("partition sectors", lba); // ... more to do } void setFile(ImgChannel file) { this.file = file; } FileSystemParam getParams() { return fsParams; } /** * Sync the header to disk. * @throws IOException If an error occurs during writing. */ public void sync() throws IOException { setUpdateTime(new Date()); writeSizeValues(fsParams.getBlockSize()); header.rewind(); file.position(0); file.write(header); file.position(fsParams.getDirectoryStartEntry() * 512L); } /** * Set the update time. * @param date The date to use. */ protected void setUpdateTime(Date date) { Calendar cal = Calendar.getInstance(); cal.setTime(date); header.put(OFF_UPDATE_YEAR, toYearCode(cal.get(Calendar.YEAR))); header.put(OFF_UPDATE_MONTH, (byte) (cal.get(Calendar.MONTH)+1)); } /** * Set the description. It is spread across two areas in the header. * @param desc The description. */ protected void setDescription(String desc) { int len = desc.length(); if (len > 50) throw new IllegalArgumentException("Description is too long (max 50)"); String part1, part2; if (len > LEN_MAP_DESCRIPTION) { part1 = desc.substring(0, LEN_MAP_DESCRIPTION); part2 = desc.substring(LEN_MAP_DESCRIPTION, len); } else { part1 = desc.substring(0, len); part2 = ""; } header.position(OFF_MAP_DESCRIPTION); header.put(toByte(part1)); for (int i = len; i < LEN_MAP_DESCRIPTION; i++) header.put((byte) ' '); header.position(OFF_MAP_NAME_CONT); header.put(toByte(part2)); for (int i = Math.max(len - LEN_MAP_DESCRIPTION, 0); i < LEN_MAP_NAME_CONT; i++) header.put((byte) ' '); header.put((byte) 0); // really? } /** * Convert a string to a byte array. * @param s The string * @return A byte array. */ private byte[] toByte(String s) { // NB: what character set should be used? return s.getBytes(); } /** * Convert to the one byte code that is used for the year. * If the year is in the 1900, then subtract 1900 and add the result to 0x63, * else subtract 2000. * Actually looks simpler, just subtract 1900.. * @param y The year in real-world format eg 2006. * @return A one byte code representing the year. */ private byte toYearCode(int y) { return (byte) (y - 1900); } protected void setDirectoryStartEntry(int directoryStartEntry) { header.put(OFF_DIRECTORY_START_BLOCK, (byte) directoryStartEntry); fsParams.setDirectoryStartEntry(directoryStartEntry); } protected void setCreationTime(Date date) { this.creationTime = date; } public void setNumBlocks(int numBlocks) { this.numBlocks = numBlocks; } /** * Represent a block number in the chs format. * * Note that this class uses the headsPerCylinder and sectorsPerTrack values * from the enclosing class. * * @see Logical block addressing */ private class CHS { private int h; private int s; private int c; private CHS() { } public CHS(int lba) { toChs(lba); } /** * Calculate the CHS values from the the given logical block address. * @param lba Input logical block address. */ private void toChs(int lba) { h = (lba / sectorsPerTrack) % headsPerCylinder; s = (lba % sectorsPerTrack) + 1; c = lba / (sectorsPerTrack * headsPerCylinder); } /** * Set from a partition table entry. * * The cylinder is 10 bits and is split between the top 2 bit of the sector * value and its own byte. * * @param h The h value. * @param sc1 The s value (6 bits) and top 2 bits of c. * @param sc2 The bottom 8 bits of c. */ public void setFromPartition(byte h, byte sc1, byte sc2) { this.h = h; this.s = (sc1 & 0x3f) + ((sc2 >> 2) & 0xc0); this.c = sc2 & 0xff; } public int toLba() { return (c * headsPerCylinder + h) * sectorsPerTrack + (s - 1); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/sys/HeaderDirent.java0000644000076400007640000000405211511612275023052 0ustar stevesteve/* * Copyright (C) 2007 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: 27-Oct-2007 */ package uk.me.parabola.imgfmt.sys; /** * This is a special class for the header. It makes it easier to bootstrap * the directory by having a special implementation that starts up by knowing * that the blocks in the header start from 0. * * @author Steve Ratcliffe */ class HeaderDirent extends Dirent { HeaderDirent(String name, BlockManager blockManager) { super(name, blockManager); } /** * Converts from a logical block to a physical block. This is a special * version that returns the logical block number when the {@link Dirent} is not * set up. This allows us to bootstrap the reading of the header blocks. * The header blocks always logical and physical blocks the same. * * @param lblock The logical block in the file. * @return The corresponding physical block in the filesystem. */ public int getPhysicalBlock(int lblock) { if (isInitialized()) { log.debug("gpb (ok)"); return super.getPhysicalBlock(lblock); } else { log.debug("gpb (not setup)"); return lblock; } } /** * Get the file size. The file appears large until the first blocks are * read in and then it will take on its actual size. * * @return The size of the file in bytes. */ public int getSize() { if (isInitialized()) return super.getSize(); else return getBlockManager().getBlockSize() * 32; } /** * Always returns true as this is only used for the special header * directory entry. * * @return Always returns true. */ public boolean isSpecial() { return true; } }mkgmap-r3660/src/uk/me/parabola/imgfmt/FileExistsException.java0000644000076400007640000000222710666506051023642 0ustar stevesteve/* * 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: 02-Dec-2006 */ package uk.me.parabola.imgfmt; import java.io.IOException; /** * Thrown when a file already exists and would be overwritten. * * @author Steve Ratcliffe */ public class FileExistsException extends IOException { /** * Constructs a new exception with the specified detail message. The * cause is not initialized, and may subsequently be initialized by * a call to {@link #initCause}. * * @param message the detail message. The detail message is saved for * later retrieval by the {@link #getMessage()} method. */ public FileExistsException(String message) { super(message); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/ReadFailedException.java0000644000076400007640000000253610766754612023557 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 14, 2007 */ package uk.me.parabola.imgfmt; /** * @author Steve Ratcliffe */ public class ReadFailedException extends RuntimeException { /** * Constructs a new runtime exception with the specified detail message and * cause.

Note that the detail message associated with cause * is not automatically incorporated in this runtime exception's detail * message. * * @param message the detail message (which is saved for later retrieval by the * {@link #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the {@link * #getCause()} method). (A null value is permitted, and indicates * that the cause is nonexistent or unknown.) * @since 1.4 */ public ReadFailedException(String message, Throwable cause) { super(message, cause); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/0000755000076400007640000000000012651103763017634 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/mps/Block.java0000644000076400007640000000400611110050256021514 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 19, 2007 */ package uk.me.parabola.imgfmt.mps; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.io.StructuredOutputStream; /** * All the blocks in the file have a type and a length. * * @author Steve Ratcliffe */ public abstract class Block { private final int type; private final ByteArrayOutputStream output = new ByteArrayOutputStream(); protected Block(int type) { this.type = type; } public void write(ImgChannel chan) throws IOException { // First write the body to the byte buffer so that we know its length. writeBody(new StructuredOutputStream(output)); ByteBuffer buf = ByteBuffer.allocate(16); buf.order(ByteOrder.LITTLE_ENDIAN); buf.put((byte) type); char len = getLength(); buf.putChar(len); // write the header. buf.flip(); chan.write(buf); // write the body. buf = ByteBuffer.allocate(len); buf.put(output.toByteArray()); buf.flip(); chan.write(buf); } /** * Writes the body to the output stream given. * * @param out The stream to write to. */ protected abstract void writeBody(StructuredOutputStream out) throws IOException; /** * This is only valid after everything is written to the block. * * @return The length of the block (or the amount written already). */ private char getLength() { int len = output.toByteArray().length; assert len <= 0xffff; return (char) len; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/MapsetBlock.java0000644000076400007640000000215510763340724022710 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 19, 2007 */ package uk.me.parabola.imgfmt.mps; import uk.me.parabola.io.StructuredOutputStream; import java.io.IOException; /** * Block describing the map set. * * @author Steve Ratcliffe */ public class MapsetBlock extends Block { private static final int BLOCK_TYPE = 0x56; private String name = "OSM map set"; public MapsetBlock() { super(BLOCK_TYPE); } protected void writeBody(StructuredOutputStream out) throws IOException { out.writeString(name); out.write(0); // unknown } public void setName(String name) { if (name != null) this.name = name; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/MpsFile.java0000644000076400007640000000357511315746733022056 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 19, 2007 */ package uk.me.parabola.imgfmt.mps; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * This file is a description of the map set that is loaded into the * gmapsupp.img file and an index of the maps that it contains. * * It is different than all the other files that fit inside the gmapsupp file * in that it doesn't contain the common header. So it does not extend ImgFile. * * @author Steve Ratcliffe */ public class MpsFile { private String mapsetName = "OSM map set"; private final Set products = new HashSet(); private final List maps = new ArrayList(); private final ImgChannel chan; public MpsFile(ImgChannel chan) { this.chan = chan; } public void sync() throws IOException { for (MapBlock map : maps) map.write(chan); for (ProductBlock block : products) block.write(chan); MapsetBlock mapset = new MapsetBlock(); mapset.setName(mapsetName); mapset.write(chan); } public void addMap(MapBlock map) { maps.add(map); } public void addProduct(ProductBlock pb) { products.add(pb); } public void setMapsetName(String mapsetName) { this.mapsetName = mapsetName; } public void close() throws IOException { chan.close(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/MpsFileReader.java0000644000076400007640000000523212075561467023174 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 19, 2007 */ package uk.me.parabola.imgfmt.mps; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * This file is a description of the map set that is loaded into the * gmapsupp.img file and an index of the maps that it contains. * * It is different than all the other files that fit inside the gmapsupp file * in that it doesn't contain the common header. So it does not extend ImgFile. * * @author Steve Ratcliffe */ public class MpsFileReader implements Closeable { private final List maps = new ArrayList(); private final List products = new ArrayList(); private final ImgChannel chan; private final ImgFileReader reader; public MpsFileReader(ImgChannel chan) { this.chan = chan; this.reader = new BufferedImgFileReader(chan); readBlocks(); } private void readBlocks() { byte type; while ((type = reader.get()) > 0) { int len = reader.getChar(); switch (type) { case 0x4c: readMapBlock(); break; case 0x46: readProductBlock(); break; default: // We always know the length, so just read over it reader.get(len); break; } } } private void readMapBlock() { MapBlock block = new MapBlock(); int val = reader.getInt(); block.setIds(val >>> 16, val & 0xffff); block.setMapNumber(reader.getInt()); block.setSeriesName(reader.getZString()); block.setMapDescription(reader.getZString()); block.setAreaName(reader.getZString()); block.setHexNumber(reader.getInt()); reader.getInt(); maps.add(block); } private void readProductBlock() { ProductBlock block = new ProductBlock(); block.setProductId(reader.getChar()); block.setFamilyId(reader.getChar()); block.setDescription(reader.getZString()); products.add(block); } public List getMaps() { return maps; } public List getProducts() { return products; } public void close() throws IOException { chan.close(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/MapBlock.java0000644000076400007640000000437311532723256022200 0ustar stevesteve/* * Copyright (C) 2007,2011. * * 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.imgfmt.mps; import java.io.IOException; import uk.me.parabola.io.StructuredOutputStream; /** * A block describing an individual map. * * The family id, product id, series name, area name and map description can * be set per map. * * @author Steve Ratcliffe */ public class MapBlock extends Block { private static final int BLOCK_TYPE = 0x4c; private int familyId; private int productId; private int mapNumber; private int hexNumber; private String seriesName; private String mapDescription; private String areaName; public MapBlock() { super(BLOCK_TYPE); } protected void writeBody(StructuredOutputStream out) throws IOException { out.write2(productId); out.write2(familyId); out.write4(mapNumber); out.writeString(seriesName); out.writeString(mapDescription); out.writeString(areaName); out.write4(hexNumber); out.write4(0); } public void setIds(int familyId, int productId) { this.familyId = familyId; this.productId = productId; } public void setSeriesName(String seriesName) { this.seriesName = seriesName; } public void setMapNumber(int mapNumber) { this.mapNumber = mapNumber; } public void setHexNumber(int hexNumber) { this.hexNumber = hexNumber; } public void setMapDescription(String mapDescription) { this.mapDescription = mapDescription; } public void setAreaName(String areaName) { this.areaName = areaName; } public int getFamilyId() { return familyId; } public int getProductId() { return productId; } public int getMapNumber() { return mapNumber; } public int getHexNumber() { return hexNumber; } public String getSeriesName() { return seriesName; } public String getMapDescription() { return mapDescription; } public String getAreaName() { return areaName; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/ProductBlock.java0000644000076400007640000000367611315746733023114 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 19, 2007 */ package uk.me.parabola.imgfmt.mps; import java.io.IOException; import uk.me.parabola.io.StructuredOutputStream; /** * A block describing a particular product. Not sure how this relates * to the map set. * * @author Steve Ratcliffe */ public class ProductBlock extends Block { private static final int BLOCK_TYPE = 0x46; private int familyId; private int productId; private String description = "OSM maps"; public ProductBlock() { super(BLOCK_TYPE); } protected void writeBody(StructuredOutputStream out) throws IOException { out.write2(productId); out.write2(familyId); out.writeString(description); } public void setFamilyId(int familyId) { this.familyId = familyId; } public int getFamilyId() { return familyId; } public void setProductId(int productId) { this.productId = productId; } public int getProductId() { return productId; } public void setDescription(String description) { this.description = description; } public String getDescription() { return description; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ProductBlock that = (ProductBlock) o; if (familyId != that.familyId) return false; if (productId != that.productId) return false; return true; } public int hashCode() { int result = familyId; result = 31 * result + productId; return result; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/mps/package.html0000644000076400007640000000156312152046274022121 0ustar stevesteve

The MPS file

This file is similar to the TDB file in that it is a list of maps along with other information describing the set. It is inserted into the gmapsupp.img file.

A map will work without this file, but it is essential when you have maps from different families loaded together as it allows you to see the names of the map families and turn them on and off separately.

It isn't like the other files in the app package, in that it doesn't begin with the common header. It should perhaps live with the TDB format or in a similar top level place. It is here because it is included inside the gmapsupp.img file and doesn't normally have a separate existance.

The names and lengths of the fields were obtained from the program gpsexplorer by the author identified as 'garminmaploader@yahoo.com' and released under GPL2+. mkgmap-r3660/src/uk/me/parabola/imgfmt/FileNotWritableException.java0000644000076400007640000000167711000461742024613 0ustar stevesteve/* * Copyright (C) 2007 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: 02-Sep-2007 */ package uk.me.parabola.imgfmt; import java.io.IOException; /** * If a file cannot be created, or written to, then this exception is thrown. * @author Steve Ratcliffe */ public class FileNotWritableException extends IOException { public FileNotWritableException(String s, Exception e) { // super class does not have the constructor that takes an exception super(s + ' ' + e.getMessage()); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/0000755000076400007640000000000012651103763017615 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/ImgReader.java0000644000076400007640000000243211400726372022316 0ustar stevesteve/* * 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 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.imgfmt.app; import java.io.Closeable; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.util.Configurable; /** * Base class for all the img sub file reading classes. * * @author Steve Ratcliffe */ public abstract class ImgReader implements Closeable, Configurable { private CommonHeader header; private ImgFileReader reader; public void close() { Utils.closeFile(reader); } protected long position() { return reader.position(); } protected void position(long pos) { reader.position(pos); } public CommonHeader getHeader() { return header; } protected final void setHeader(CommonHeader header) { this.header = header; } protected ImgFileReader getReader() { return reader; } protected void setReader(ImgFileReader reader) { this.reader = reader; } }mkgmap-r3660/src/uk/me/parabola/imgfmt/app/ImgFileReader.java0000644000076400007640000000575411532723256023134 0ustar stevesteve/* * 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; import java.io.Closeable; import uk.me.parabola.imgfmt.ReadFailedException; /** * For reading subfiles from the img. The focus of mkgmap is on writing, * but some limited reading is needed for several operations. * * @author Steve Ratcliffe */ public interface ImgFileReader extends Closeable { /** * Get the position. Needed because may not be reflected in the underlying * file if being buffered. * * @return The logical position within the file. */ public long position(); /** * Set the position of the file. * @param pos The new position in the file. */ void position(long pos); /** * Read in a single byte. * @return The byte that was read. */ public byte get() throws ReadFailedException; /** * Read in two bytes. Done in the correct byte order. * @return The 2 byte integer that was read. */ public char getChar() throws ReadFailedException; /** * Get a 3byte signed quantity. * * @return The value read. * @throws ReadFailedException When the file cannot be read. */ public int get3() throws ReadFailedException; /** * Get a 3byte unsigned quantity. * * @return The value read. * @throws ReadFailedException When the file cannot be read. */ public int getu3() throws ReadFailedException; /** * Read in a 4 byte value. * @return A 4 byte integer. */ public int getInt() throws ReadFailedException; /** * Read a variable sized integer. The size is given. * @param n The size of the integer to read. Must be 1 to 4. * @return The integer which will not be sign extended if it is less * than 4 bytes long. */ public int getUint(int n) throws ReadFailedException; /** * Read in an arbitrary length sequence of bytes. * * @param len The number of bytes to read. */ public byte[] get(int len) throws ReadFailedException; /** * Read a zero terminated string from the file. * @return A string * @throws ReadFailedException For failures. */ public String getZString() throws ReadFailedException; /** * Read in a string of digits in the compressed base 11 format that is used * for phone numbers in the POI section. * @param delimiter This will replace all digit 11 characters. Usually a * '-' to separate numbers in a telephone. No doubt there is a different * standard in each country. * @return A phone number possibly containing the delimiter character. */ public String getBase11str(byte firstChar, char delimiter); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/FileBackedImgFileWriter.java0000644000076400007640000001265111642611367025073 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * Write img file data to a temporary file. On a call to sync() the data * is copied to the output channel. * * @author Steve Ratcliffe */ public class FileBackedImgFileWriter implements ImgFileWriter{ private final ImgChannel outputChan; private final File tmpFile; private final BufferedOutputStream file; private final FileChannel tmpChannel; public FileBackedImgFileWriter(ImgChannel chan, File outputDir) { this.outputChan = chan; try { tmpFile = File.createTempFile("img", null, outputDir); tmpFile.deleteOnExit(); FileOutputStream out = new FileOutputStream(tmpFile); tmpChannel = out.getChannel(); file = new BufferedOutputStream(out, 16*1024); } catch (IOException e) { throw new MapFailedException("Could not create mdr temporary file"); } } /** * Maps the temporary file and copies to the output channel. * * @throws IOException If there is an error writing. */ public void sync() throws IOException { file.close(); FileInputStream is = null; try { is = new FileInputStream(tmpFile); FileChannel channel = is.getChannel(); channel.transferTo(0, channel.size(), outputChan); channel.close(); } finally { Utils.closeFile(is); if (!tmpFile.delete()) System.err.println("Could not delete mdr img temporary file"); } } /** * Get the position. Have to flush the buffer before getting the position. * * @return The logical position within the file. */ public int position() { try { file.flush(); return (int) tmpChannel.position(); } catch (IOException e) { return 0; } } /** * Set the position of the file. * The buffer has to be flushed first. * * @param pos The new position in the file. */ public void position(long pos) { try { file.flush(); tmpChannel.position(pos); } catch (IOException e) { throw new MapFailedException("Could not set position in mdr tmp file"); } } /** * Write out a single byte. * * @param b The byte to write. */ public void put(byte b) { try { file.write(b); } catch (IOException e) { throw new MapFailedException("could not write byte to mdr tmp file"); } } /** * Write out two bytes. Can't use writeChar() since need to reverse the byte * order. * * @param c The value to write. */ public void putChar(char c) { try { file.write(c); file.write(c >> 8); } catch (IOException e) { throw new MapFailedException("could not write char to mdr tmp file"); } } /** * Write out three bytes. Done in the little endian byte order. * * @param val The value to write, only the bottom three bytes will be written. */ public void put3(int val) { try { file.write(val); file.write(val >> 8); file.write(val >> 16); } catch (IOException e) { throw new MapFailedException("could not write3 to mdr tmp file"); } } /** * Write out 4 byte value. * * @param val The value to write. */ public void putInt(int val) { try { file.write(val); file.write(val >> 8); file.write(val >> 16); file.write(val >> 24); } catch (IOException e) { throw new MapFailedException("could not write int to mdr tmp file"); } } /** * Write out an arbitrary length sequence of bytes. * * @param val The values to write. */ public void put(byte[] val) { try { file.write(val); } catch (IOException e) { throw new MapFailedException("could not write bytes to mdr tmp file"); } } /** * Write out part of a byte array. * * @param src The array to take bytes from. * @param start The start position. * @param length The number of bytes to write. */ public void put(byte[] src, int start, int length) { try { file.write(src, start, length); } catch (IOException e) { throw new MapFailedException("could not write bytes to mdr tmp file"); } } /** * Write out a complete byte buffer. * * @param src The buffer to write. */ public void put(ByteBuffer src) { try { file.flush(); tmpChannel.write(src); } catch (IOException e) { throw new MapFailedException("could not write buffer to mdr tmp file"); } } /** * Returns the size of the file. * * @return The file size in bytes. */ public long getSize() { try { file.flush(); return tmpChannel.size(); } catch (IOException e) { throw new MapFailedException("could not get size of mdr tmp file"); } } /** * Closes this stream and releases any system resources associated with it. If the stream is already closed then * invoking this method has no effect. * * @throws IOException if an I/O error occurs */ public void close() throws IOException { outputChan.close(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/0000755000076400007640000000000012651103763020431 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypPoint.java0000644000076400007640000000371211665675646023107 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.charset.CharsetEncoder; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Represents a POI in the typ file. * * @author Steve Ratcliffe */ public class TypPoint extends TypElement { private Xpm nightXpm; private static final byte F_BITMAP = 0x1; private static final byte F_NIGHT_XPM = 0x2; private static final byte F_LABEL = 0x4; private static final byte F_EXTENDED_FONT = 0x8; public void write(ImgFileWriter writer, CharsetEncoder encoder) { offset = writer.position(); byte flags = F_BITMAP; if (nightXpm != null) flags |= F_NIGHT_XPM; if (!labels.isEmpty()) flags |= F_LABEL; if (fontStyle != 0 || dayFontColour != null || nightFontColour != null) flags |= F_EXTENDED_FONT; writer.put(flags); // Width and height is the same for day and night images, so it is written once only. ColourInfo colourInfo = xpm.getColourInfo(); writer.put((byte) colourInfo.getWidth()); writer.put((byte) colourInfo.getHeight()); // Day or only image writeImage(writer, xpm); if ((flags & F_NIGHT_XPM) != 0) writeImage(writer, nightXpm); if ((flags & F_LABEL) != 0) writeLabelBlock(writer, encoder); if ((flags & F_EXTENDED_FONT) != 0) writeExtendedFontInfo(writer); } public void setNightXpm(Xpm nightXpm) { this.nightXpm = nightXpm; } /** * Points have full pixmaps with multiple colours, including 24 full colour images. */ public boolean simpleBitmap() { return false; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/ShapeStacking.java0000644000076400007640000000313711670361040024016 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 15, 2007 */ package uk.me.parabola.imgfmt.app.typ; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Holds the shape stacking section. * * Deals with sorting everything correctly, so no need to sort in the input file. * * @author Steve Ratcliffe */ public class ShapeStacking { private final SortedMap bar = new TreeMap(); public void addPolygon(int level, int type, int subtype) { int levelType = (level << 16) + type; DrawOrder order = bar.get(levelType); if (order == null) { order = new DrawOrder(type); bar.put(levelType, order); } order.addSubtype(subtype); } public void write(ImgFileWriter writer) { int lastLevel = 1; DrawOrder empty = new DrawOrder(0); for (Map.Entry ent : bar.entrySet()) { int level = (ent.getKey() >> 16) & 0xffff; DrawOrder order = ent.getValue(); if (level != lastLevel) { empty.write(writer); lastLevel = level; } order.write(writer); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TrueImage.java0000644000076400007640000000675411711515370023167 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * A true colour image. * * The image is represented by an array of int, with each int in RGBA format. * * @author Steve Ratcliffe */ public class TrueImage implements Image { private final ColourInfo colourInfo; private final int[] image; // If this is mode 16, then the transparent colour is set. private int transparentPixel; public TrueImage(ColourInfo colourInfo, int[] image) { analyzeColours(image, colourInfo); this.colourInfo = colourInfo; this.image = image; } /** * Write out the image. It is a set of pixel values that are full RGB values, rather than * table driven as in the other image type. If the colour mode is 32 the colours have an * extra 4 bit opacity value following. * * If the colour mode is 16, then the transparent pixel is written just before the image * itself. */ public void write(ImgFileWriter writer) { int width = colourInfo.getWidth(); int height = colourInfo.getHeight(); int mode = colourInfo.getColourMode(); // For mode 16, the transparent pixel precedes the pixmap data. if (mode == 16) { writer.put((byte) (transparentPixel>>8)); writer.put((byte) (transparentPixel>>16)); writer.put((byte) (transparentPixel>>24)); } boolean hasAlpha = mode == 32; // Unlike the xpm based images, the true-colour image format does not appear to // have any padding so write as a continuous block. BitWriter bitWriter = new BitWriter(); for (int h = 0; h < height; h++) { for (int w = 0; w < width; w++) { int col = image[h * width + w]; bitWriter.putn(col>>8 & 0xff, 8); bitWriter.putn(col>>16 & 0xff, 8); bitWriter.putn(col>>24 & 0xff, 8); if (hasAlpha) { int alpha = 0xff - (col & 0xff); alpha = ColourInfo.alphaRound4(alpha); bitWriter.putn(alpha, 4); } } } writer.put(bitWriter.getBytes(), 0, bitWriter.getLength()); } /** * Analyze the colours and determine if this should be a mode 16 or 32 image. * * By default it will be a mode 0 image. If there is any transparency the appropriate * colour mode will be selected. * * @param image An images as an array of integers. Each integer is a colour in RGBA format. * @param colourInfo The colour mode will be set in this. */ private void analyzeColours(int[] image, ColourInfo colourInfo) { boolean hasTransparent = false; boolean hasAlpha = false; int nPixels = colourInfo.getWidth() * colourInfo.getHeight(); for (int i = 0; i < nPixels; i++) { int col = image[i]; int a = col & 0xff; if (a == 0) { // Completely transparent, change all transparent pixels to the same value if (hasTransparent) image[i] = transparentPixel; else transparentPixel = image[i]; hasTransparent = true; } else if (a < 255) { // Partially transparent hasAlpha = true; } } if (hasAlpha) colourInfo.setColourMode(32); else if (hasTransparent) colourInfo.setColourMode(16); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TYPHeader.java0000644000076400007640000001311111665675646023100 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 14, 2007 */ package uk.me.parabola.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; /** * The header for the TYP file. * * @author Thomas Lußnig */ public class TYPHeader extends CommonHeader { public static final int HEADER_LEN = 0x9c; // 0x6e; private char familyId; private char productId; private char codePage; private final Section pointData = new Section(); private final Section lineData = new Section(pointData); private final Section polygonData = new Section(lineData); private final Section pointIndex = new Section(polygonData, (char) 2); private final Section lineIndex = new Section(pointIndex, (char) 2); private final Section polygonIndex = new Section(lineIndex, (char) 2); private final Section shapeStacking = new Section(polygonIndex, (char) 5); private final Section iconData = new Section(polygonIndex); private final Section iconIndex = new Section(iconData, (char) 3); private final Section labels = new Section(iconIndex); private final Section stringIndex = new Section(labels); private final Section typeIndex = new Section(stringIndex); public TYPHeader() { super(HEADER_LEN, "GARMIN TYP"); } /** * Read the rest of the header. Specific to the given file. It is guaranteed * that the file position will be set to the correct place before this is * called. * * @param reader The header is read from here. */ protected void readFileHeader(ImgFileReader reader) { // Reset position for the real header reading code. reader.position(COMMON_HEADER_LEN); codePage = reader.getChar(); // 1252 pointData.setPosition(reader.getInt()); pointData.setSize(reader.getInt()); lineData.setPosition(reader.getInt()); lineData.setSize(reader.getInt()); polygonData.setPosition(reader.getInt()); polygonData.setSize(reader.getInt()); familyId = reader.getChar(); productId = reader.getChar(); pointIndex.setPosition(reader.getInt()); pointIndex.setItemSize(reader.getChar()); pointIndex.setSize(reader.getInt()); lineIndex.setPosition(reader.getInt()); lineIndex.setItemSize(reader.getChar()); lineIndex.setSize(reader.getInt()); polygonIndex.setPosition(reader.getInt()); polygonIndex.setItemSize(reader.getChar()); polygonIndex.setSize(reader.getInt()); shapeStacking.setPosition(reader.getInt()); shapeStacking.setItemSize(reader.getChar()); shapeStacking.setSize(reader.getInt()); } /** * Write the rest of the header. It is guaranteed that the writer will be set * to the correct position before calling. * * This header appears to have a different layout to most other headers. * * @param writer The header is written here. */ protected void writeFileHeader(ImgFileWriter writer) { writer.putChar(codePage); pointData.writeSectionInfo(writer); lineData.writeSectionInfo(writer); polygonData.writeSectionInfo(writer); writer.putChar(familyId); writer.putChar(productId); // Can't use Section.writeSectionInfo here as there is an unusual layout. writeSectionInfo(writer, pointIndex); writeSectionInfo(writer, lineIndex); writeSectionInfo(writer, polygonIndex); writeSectionInfo(writer, shapeStacking); if (getHeaderLength() > 0x5b) { writeSectionInfo(writer, iconIndex); writer.put((byte) 0x13); iconData.writeSectionInfo(writer); writer.putInt(0); } if (getHeaderLength() > 0x6e) { labels.writeSectionInfo(writer); // not known, guessing. Different layout to other files. writer.putInt(stringIndex.getItemSize()); writer.putInt(0x1b); writer.putInt(stringIndex.getPosition()); writer.putInt(stringIndex.getSize()); writer.putInt(typeIndex.getItemSize()); writer.putInt(0x1b); writer.putInt(typeIndex.getPosition()); writer.putInt(typeIndex.getSize()); writer.putChar((char) 0); } } /** * There is an unusual layout of the section pointers in the TYP file for the sections * that have an item size. */ private void writeSectionInfo(ImgFileWriter writer, Section section) { writer.putInt(section.getPosition()); writer.putChar(section.getItemSize()); writer.putInt(section.getSize()); } void setCodePage(char codePage) { this.codePage = codePage; } Section getPointData() { return pointData; } void setFamilyId(char familyId) { this.familyId = familyId; } void setProductId(char productId) { this.productId = productId; } Section getPointIndex() { return pointIndex; } Section getShapeStacking() { return shapeStacking; } public Section getPolygonData() { return polygonData; } public Section getPolygonIndex() { return polygonIndex; } public Section getLineData() { return lineData; } public Section getLineIndex() { return lineIndex; } public Section getIconData() { return iconData; } public Section getIconIndex() { return iconIndex; } public Section getLabels() { return labels; } public Section getStringIndex() { return stringIndex; } public Section getTypeIndex() { return typeIndex; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypLabel.java0000644000076400007640000000204211665675646023030 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; /** * @author Steve Ratcliffe */ public class TypLabel { private final int lang; private final String text; public TypLabel(String in) { String[] split = in.split(",", 2); int l; String s; try { l = Integer.decode(split[0]); s = split[1]; } catch (NumberFormatException e) { l = 0; s = in; } this.lang = l; this.text = s; } public TypLabel(int lang, String text) { this.lang = lang; this.text = text; } public int getLang() { return lang; } public String getText() { return text; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypLabelException.java0000644000076400007640000000163311665675646024714 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; /** * Used when a label cannot be converted. An error is thrown indicating a charset to try * instead of the default system one. * * @author Steve Ratcliffe */ public class TypLabelException extends RuntimeException { private String charsetName; public TypLabelException(String charsetName) { this.charsetName = charsetName; } public String getCharsetName() { return charsetName; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypElement.java0000644000076400007640000001215512072370014023356 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Base routines and data used by points, lines and polygons. * * If fact they are all very similar, so there is very little extra in the * subclasses apart from the write routine. * * @author Steve Ratcliffe */ public abstract class TypElement implements Comparable { private int type; private int subType; protected final List labels = new ArrayList(); protected Xpm xpm; protected int fontStyle; protected Rgb dayFontColour; protected Rgb nightFontColour; protected int offset; public void setType(int type) { this.type = type; } public void setSubType(int subType) { this.subType = subType; } public int getType() { return type; } /** * We sort these by type. * Only the index needs to be sorted (probably) but we don't create the index separately. * * @param o The other object to compare against. * @return The usual -1, 0, 1 for the other object being less than, equal, greater than than this. */ public int compareTo(TypElement o) { int t1 = getTypeForFile(); int t2 = o.getTypeForFile(); if (t1 == t2) return 0; else if (t1 < t2) return -1; else return 1; } /** * Get the type in the format required for writing in the typ file sections. */ public int getTypeForFile() { return (type << 5) | (subType & 0x1f); } public void addLabel(String text) { labels.add(new TypLabel(text)); } public void setXpm(Xpm xpm) { this.xpm = xpm; } public void setFontStyle(int font) { this.fontStyle = font; } public void setDayFontColor(String value) { dayFontColour = new Rgb(value); } public void setNightCustomColor(String value) { nightFontColour = new Rgb(value); } public abstract void write(ImgFileWriter writer, CharsetEncoder encoder); public int getOffset() { return offset; } /** * Does this element have two colour bitmaps, with possible automatic night colours. For lines and polygons. * * Overridden for points and icons. */ public boolean simpleBitmap() { return true; } /** * Make the label block separately as we need its length before we write it out properly. * * @param encoder For encoding the strings as bytes. * @return A byte buffer with position set to the length of the block. */ protected ByteBuffer makeLabelBlock(CharsetEncoder encoder) { ByteBuffer out = ByteBuffer.allocate(256 * labels.size()); for (TypLabel tl : labels) { out.put((byte) tl.getLang()); CharBuffer cb = CharBuffer.wrap(tl.getText()); try { ByteBuffer buffer = encoder.encode(cb); out.put(buffer); } catch (CharacterCodingException ignore) { String name = encoder.charset().name(); //System.out.println("cs " + name); throw new TypLabelException(name); } out.put((byte) 0); } return out; } /** * Write the label block, this is the same for all element types. * @param encoder To properly encode the labels. */ protected void writeLabelBlock(ImgFileWriter writer, CharsetEncoder encoder) { ByteBuffer out = makeLabelBlock(encoder); int len = out.position(); // The length is encoded as a variable length integer with the length indicated by a suffix. len = (len << 1) + 1; int mask = ~0xff; while ((len & mask) != 0) { mask <<= 8; len <<= 1; } // write out the length, I'm assuming that it will be 1 or 2 bytes if (len > 0xff) writer.putChar((char) len); else writer.put((byte) len); // Prepare and write buffer out.flip(); writer.put(out); } /** * Write out extended font information, colour and size. * * This is the same for each element type. */ protected void writeExtendedFontInfo(ImgFileWriter writer) { byte fontExt = (byte) fontStyle; if (dayFontColour != null) fontExt |= 0x8; if (nightFontColour != null) fontExt |= 0x10; writer.put(fontExt); if (dayFontColour != null) dayFontColour.write(writer, (byte) 0x10); if (nightFontColour != null) nightFontColour.write(writer, (byte) 0x10); } /** * Write out an image. The width and height are written separately, because they are not * repeated for the night image. * * @param xpm Either the day or night XPM. */ protected void writeImage(ImgFileWriter writer, Xpm xpm) { ColourInfo colourInfo = xpm.getColourInfo(); writer.put((byte) colourInfo.getNumberOfSColoursForCM()); writer.put((byte) colourInfo.getColourMode()); colourInfo.write(writer); xpm.writeImage(writer); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/DrawOrder.java0000644000076400007640000000135011670361040023156 0ustar stevestevepackage uk.me.parabola.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Writeable; /** * The drawing order for a type and a set of subtypes. * * The drawing order is specified by the order of these within the file, rather than anything * actually in the item. */ public class DrawOrder implements Writeable { private final byte type; private int subTypes; private boolean hasSubtypes; public DrawOrder(int type) { this.type = (byte) (type & 0xff); if (type >= 0x100) hasSubtypes = true; } public void write(ImgFileWriter writer) { writer.put(type); writer.putInt(subTypes); } public void addSubtype(int subtype) { if (hasSubtypes) subTypes |= 1 << subtype; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypLine.java0000644000076400007640000000543211665675646022706 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.charset.CharsetEncoder; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * A line as read from a typ.txt file. * * @author Steve Ratcliffe */ public class TypLine extends TypElement { private static final int F_LABEL = 0x1; private static final int F_USE_ROTATION = 0x2; private static final int F_EXTENDED = 0x4; private boolean useOrientation; private byte lineWidth; private byte borderWidth; /** * This is slightly different to the polygon case, but not much. * * The line width is held in the first byte along with the type. * The colour scheme does not have a bit to say if a bitmap is used, * as you can always have one. * * There is a border width that can be specified. * * @param encoder For the labels. */ public void write(ImgFileWriter writer, CharsetEncoder encoder) { offset = writer.position(); byte flags = 0; if (!labels.isEmpty()) flags |= F_LABEL; if (fontStyle != 0 || dayFontColour != null) flags |= F_EXTENDED; if (!useOrientation) flags |= F_USE_ROTATION; int height = 0; if (xpm.hasImage()) height = xpm.getColourInfo().getHeight(); ColourInfo colourInfo = xpm.getColourInfo(); int scheme = colourInfo.getColourScheme() & 0x7; writer.put((byte) ((scheme & 0x7) | (height << 3))); writer.put(flags); colourInfo.write(writer); if (xpm.hasImage()) xpm.writeImage(writer); if (height == 0) { writer.put(lineWidth); if ((scheme&~1) != 6) writer.put((byte) (lineWidth + 2*borderWidth)); } // The labels have a length byte to show the number of bytes following. There is // also a flag in the length. The strings have a language number proceeding them. // The strings themselves are null terminated. if ((flags & F_LABEL) != 0) writeLabelBlock(writer, encoder); // The extension section hold font style and colour information for the labels. if ((flags & F_EXTENDED) != 0) writeExtendedFontInfo(writer); } public void setUseOrientation(boolean useOrientation) { this.useOrientation = useOrientation; } public void setLineWidth(int val) { lineWidth = (byte) val; } public void setBorderWidth(int borderWidth) { this.borderWidth = (byte) borderWidth; } public void finish() { if (borderWidth != 0) xpm.getColourInfo().setHasBorder(true); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TYPFile.java0000644000076400007640000001503412072370014022543 0ustar stevesteve/* * 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: 03-Dec-2006 * Change: Thomas Lußnig */ package uk.me.parabola.imgfmt.app.typ; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; import uk.me.parabola.imgfmt.app.SectionWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The TYP file. * * @author Thomas Lußnig * @author Steve Ratcliffe */ public class TYPFile extends ImgFile { private static final Logger log = Logger.getLogger(TYPFile.class); private final TYPHeader header = new TYPHeader(); private TypData data; private final Map strToType = new TreeMap(); private final Map typeToStr = new TreeMap(); public TYPFile(ImgChannel chan) { setHeader(header); setWriter(new BufferedImgFileWriter(chan)); position(TYPHeader.HEADER_LEN); } public void write() { ImgFileWriter writer = getWriter(); writer.position(TYPHeader.HEADER_LEN); writeSection(writer, header.getPolygonData(), header.getPolygonIndex(), data.getPolygons()); writeSection(writer, header.getLineData(), header.getLineIndex(), data.getLines()); writeSection(writer, header.getPointData(), header.getPointIndex(), data.getPoints()); SectionWriter subWriter = header.getShapeStacking().makeSectionWriter(writer); data.getStacking().write(subWriter); Utils.closeFile(subWriter); writeSection(writer, header.getIconData(), header.getIconIndex(), data.getIcons()); writeLabels(writer); writeStrIndex(writer); writerTypeIndex(writer); zapZero(header.getShapeStacking(), header.getLabels(), header.getStringIndex(), header.getTypeIndex()); log.debug("syncing TYP file"); position(0); getHeader().writeHeader(getWriter()); } private void writeLabels(ImgFileWriter in) { if (data.getIcons().isEmpty()) return; SectionWriter writer = header.getLabels().makeSectionWriter(in); List> keys = new ArrayList>(); Sort sort = data.getSort(); for (TypIconSet icon : data.getIcons()) { String label = icon.getLabel(); if (label != null) { SortKey key = sort.createSortKey(icon, label); keys.add(key); } } Collections.sort(keys); // Offset 0 is reserved to mean no label. writer.put((byte) 0); for (SortKey key : keys) { int off = writer.position(); TypIconSet icon = key.getObject(); int type = icon.getTypeForFile(); String label = icon.getLabel(); if (label != null) { CharBuffer cb = CharBuffer.wrap(label); CharsetEncoder encoder = data.getEncoder(); try { ByteBuffer buffer = encoder.encode(cb); writer.put(buffer); // If we succeeded then note offsets for indexes strToType.put(off, type); typeToStr.put(type, off); } catch (CharacterCodingException ignore) { String name = encoder.charset().name(); throw new TypLabelException(name); } writer.put((byte) 0); } } Utils.closeFile(writer); } private void writeStrIndex(ImgFileWriter in) { SectionWriter writer = header.getStringIndex().makeSectionWriter(in); int psize = ptrSize(header.getLabels().getSize()); header.getStringIndex().setItemSize((char) (3 + psize)); for (Map.Entry ent : strToType.entrySet()) { putN(writer, psize, ent.getKey()); putN(writer, 3, ent.getValue()); } Utils.closeFile(writer); } private void writerTypeIndex(ImgFileWriter in) { SectionWriter writer = header.getTypeIndex().makeSectionWriter(in); int psize = ptrSize(header.getLabels().getSize()); header.getTypeIndex().setItemSize((char) (3 + psize)); for (Map.Entry ent : typeToStr.entrySet()) { putN(writer, 3, ent.getKey()); putN(writer, psize, ent.getValue()); } Utils.closeFile(writer); } private void writeSection(ImgFileWriter writer, Section dataSection, Section indexSection, List elementData) { Collections.sort(elementData); SectionWriter subWriter = dataSection.makeSectionWriter(writer); CharsetEncoder encoder = data.getEncoder(); for (TypElement elem : elementData) elem.write(subWriter, encoder); Utils.closeFile(subWriter); int size = dataSection.getSize(); int typeSize = indexSection.getItemSize(); int psize = ptrSize(size); indexSection.setItemSize((char) (typeSize + psize)); subWriter = indexSection.makeSectionWriter(writer); for (TypElement elem : elementData) { int offset = elem.getOffset(); int type = elem.getTypeForFile(); putN(subWriter, typeSize, type); putN(subWriter, psize, offset); } Utils.closeFile(subWriter); zapZero(dataSection, indexSection); } private void zapZero(Section... sect) { for (Section s : sect) { if (s.getSize() == 0) { s.setPosition(0); s.setItemSize((char) 0); } } } private int ptrSize(int size) { int psize = 1; if (size > 0xffffff) psize = 4; else if (size > 0xffff) psize = 3; else if (size > 0xff) psize = 2; return psize; } protected void putN(ImgFileWriter writer, int n, int value) { switch (n) { case 1: writer.put((byte) value); break; case 2: writer.putChar((char) value); break; case 3: writer.put3(value); break; case 4: writer.putInt(value); break; default: // Don't write anything. assert false; break; } } public void setData(TypData data) { this.data = data; TypParam param = data.getParam(); header.setCodePage((char) param.getCodePage()); header.setFamilyId((char) param.getFamilyId()); header.setProductId((char) param.getProductId()); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypData.java0000644000076400007640000000515312254147734022653 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.srt.Sort; /** * Holds all the data for a typ file. * * @author Steve Ratcliffe */ public class TypData { private final ShapeStacking stacking = new ShapeStacking(); private final TypParam param = new TypParam(); private final List polygons = new ArrayList(); private final List lines = new ArrayList(); private final List points = new ArrayList(); private final List icons = new ArrayList(); private Sort sort; private CharsetEncoder encoder; public void addPolygonStackOrder(int level, int type, int subtype) { stacking.addPolygon(level, type, subtype); } public Sort getSort() { return sort; } public void setSort(Sort sort) { if (sort == null) return; if (this.sort != null) { int origCodepage = this.sort.getCodepage(); if (origCodepage != 0) { if (origCodepage != sort.getCodepage()) { // This is just a warning, not a definite problem System.out.println("WARNING: SortCode in TYP txt file different from" + " command line setting"); } } } this.sort = sort; encoder = sort.getCharset().newEncoder(); param.setCodePage(sort.getCodepage()); } public void setFamilyId(int val) { param.setFamilyId(val); } public void setProductId(int val) { param.setProductId(val); } public ShapeStacking getStacking() { return stacking; } public TypParam getParam() { return param; } public void addPolygon(TypPolygon polygon) { polygons.add(polygon); } public CharsetEncoder getEncoder() { return encoder; } public List getPolygons() { return polygons; } public void addLine(TypLine line) { lines.add(line); } public List getLines() { return lines; } public void addPoint(TypPoint point) { points.add(point); } public List getPoints() { return points; } public void addIcon(TypIconSet current) { icons.add(current); } public List getIcons() { return icons; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/Rgb.java0000644000076400007640000000340211665675646022027 0ustar stevestevepackage uk.me.parabola.imgfmt.app.typ; import uk.me.parabola.imgfmt.FormatException; import uk.me.parabola.imgfmt.app.ImgFileWriter; public class Rgb { private final int b; private final int g; private final int r; private final int a; public Rgb(int r, int g, int b, int a) { this.r = r; this.g = g; this.b = b; this.a = a; } public Rgb(int r, int g, int b) { this(r, g, b, 0xff); } /** * Initialise from a string. * * The format is #RRGGBB and without the '#'. You can also append * an alpha value. FF for fully opaque, and 00 for fully transparent. * The typ file only deals with fully transparent. * * @param in The string form of the color. */ public Rgb(String in) { String colour = in; if (colour.startsWith("#")) colour = colour.substring(1); r = Integer.parseInt(colour.substring(0, 2), 16); g = Integer.parseInt(colour.substring(2, 4), 16); b = Integer.parseInt(colour.substring(4, 6), 16); if (colour.length() > 6) a = Integer.parseInt(colour.substring(6, 8), 16); else a = 0xff; } /** * Create a new Rgb from the given one, adding the given alpha channel value. */ public Rgb(Rgb rgb, int alpha) { this(rgb.r, rgb.g, rgb.b, alpha); } public void write(ImgFileWriter writer, byte type) { if (type != 0x10) throw new FormatException("Invalid color deep"); writer.put((byte) b); writer.put((byte) g); writer.put((byte) r); } public boolean isTransparent() { return a == 0; } public String toString() { if (a == 0xff) return String.format("#%02x%02x%02x", r, g, b); else return String.format("#%02x%02x%02x%02x", r, g, b, a); } public int getB() { return b; } public int getG() { return g; } public int getR() { return r; } public int getA() { return a; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/Xpm.java0000644000076400007640000000210511665675646022060 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Holds everything read from an XPM value in the typ txt file. * * @author Steve Ratcliffe */ public class Xpm { private ColourInfo colourInfo; private Image image; public ColourInfo getColourInfo() { return colourInfo; } public void setColourInfo(ColourInfo colourInfo) { this.colourInfo = colourInfo; } public void setImage(Image image) { this.image = image; } public boolean hasImage() { return image != null; } public void writeImage(ImgFileWriter writer) { image.write(writer); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/AlphaAdder.java0000644000076400007640000000202011711515370023250 0ustar stevesteve/* * Copyright (C) 2012. * * 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.imgfmt.app.typ; /** * Interface for adding an alpha value to a previously saved colour value. * * The current TYP editors place the alpha value after the colour definition, eg: * * "#992299" alpha 5 * * so we need to add the alpha value to a colour that has already been read * in. * * @author Steve Ratcliffe */ public interface AlphaAdder { /** * Add an alpha value to the last colour that was saved. * * @param alpha A true alpha value ie 0 is transparent, 255 opaque. */ public void addAlpha(int alpha); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/RgbWithTag.java0000644000076400007640000000161011665675646023316 0ustar stevesteve/* * Copyright (C) 2008 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 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.imgfmt.app.typ; /** * @author Steve Ratcliffe */ public class RgbWithTag extends Rgb { private final String tag; public RgbWithTag(String tag, Rgb rgb) { super(rgb.getR(), rgb.getG(), rgb.getB(), rgb.getA()); this.tag = tag; } public RgbWithTag(RgbWithTag rgb, int alpha) { super(rgb, alpha); this.tag = rgb.getTag(); } public String getTag() { return tag; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/package.html0000644000076400007640000000037710735217321022716 0ustar stevesteve

The TYP file for custom rendering styles

This file contains definitions of how a map element will be rendered on the GPS which allows you to display things in a different way. Not all devices support the TYP file however.

mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypIconSet.java0000644000076400007640000000370511665675646023364 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.charset.CharsetEncoder; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * The new multiple icon format. * There can be several icons at different resolutions. * * @author Steve Ratcliffe */ public class TypIconSet extends TypElement { private final List icons = new ArrayList(); public void write(ImgFileWriter writer, CharsetEncoder encoder) { offset = writer.position(); // Start with the number of icons writer.put((byte) icons.size()); for (Xpm xpm : icons) { ColourInfo colourInfo = xpm.getColourInfo(); int nbits = calcBits(colourInfo); writer.putChar((char) (nbits/2)); writer.put((byte) 1); writer.put((byte) colourInfo.getWidth()); writer.put((byte) colourInfo.getHeight()); writeImage(writer, xpm); } } private int calcBits(ColourInfo colourInfo) { int bits = 0; int bpp = colourInfo.getBitsPerPixel(); bits += colourInfo.getWidth() * colourInfo.getHeight() * bpp; bits += colourInfo.getNumberOfSColoursForCM() * 3 * 8; if (colourInfo.getNumberOfColours() == 0 && colourInfo.getColourMode() == 0x10) bits += 3*8; bits += 0x2c; return bits; } public void addIcon(Xpm xpm) { icons.add(xpm); } public String getLabel() { if (labels.isEmpty()) return null; return labels.get(0).getText(); } /** * Icon sets can have full colour pixmaps. */ public boolean simpleBitmap() { return false; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/BitmapImage.java0000644000076400007640000000313111665675646023473 0ustar stevestevepackage uk.me.parabola.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Holds a bitmap image for the typ file. * * There are a number of different formats allowed. * * Based on code by Thomas Lußnig, but type and colour information separated out and * deals with more than just points. */ public class BitmapImage implements Image { private final ColourInfo colourInfo; private final String image; public BitmapImage(ColourInfo colourInfo, String image) { this.colourInfo = colourInfo; this.image = image; } public void write(ImgFileWriter writer) { final int bitSize = colourInfo.getBitsPerPixel(); int cpp = colourInfo.getCharsPerPixel(); int width = colourInfo.getWidth(); int height = colourInfo.getHeight(); int i = 0; for (int h = 0; h < height; h++) { // Each row is padded to a byte boundary, creating a new bit writer for every // row ensures that happens. BitWriter bitWriter = new BitWriter(); for (int w = 0; w < width; w++) { String idx = image.substring(i, i + cpp); i += cpp; int val = colourInfo.getIndex(idx); bitWriter.putn(val, bitSize); } writer.put(bitWriter.getBytes(), 0, bitWriter.getLength()); } } public int compare(BitmapImage a, BitmapImage b) { throw new UnsupportedOperationException(); //if (a == null) // return 1; //if (b == null) // return -1; //if (a.typ < b.typ) // return -1; //if (a.typ > b.typ) // return 1; //if (a.dayNight < b.dayNight) // return -1; //if (a.dayNight > b.dayNight) // return 1; //return 0; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypParam.java0000644000076400007640000000205211657205245023033 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; /** * General TYP file parameters. * * @author Steve Ratcliffe */ public class TypParam { private int familyId; private int productId; private int codePage; public int getFamilyId() { return familyId; } public void setFamilyId(int familyId) { this.familyId = familyId; } public int getProductId() { return productId; } public void setProductId(int productId) { this.productId = productId; } public int getCodePage() { return codePage; } public void setCodePage(int codePage) { this.codePage = codePage; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/Image.java0000644000076400007640000000126611665675646022345 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import uk.me.parabola.imgfmt.app.Writeable; /** * Interface for the different image types. * * @author Steve Ratcliffe */ public interface Image extends Writeable { } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/TypPolygon.java0000644000076400007640000000322711677054151023427 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.nio.charset.CharsetEncoder; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Holds the data for a polygon style. * * @author Steve Ratcliffe */ public class TypPolygon extends TypElement { private static final int F_LABEL = 0x10; private static final int F_EXTENDED = 0x20; public void write(ImgFileWriter writer, CharsetEncoder encoder) { offset = writer.position(); ColourInfo colourInfo = xpm.getColourInfo(); int scheme = colourInfo.getColourScheme(); if (!labels.isEmpty()) scheme |= F_LABEL; if (fontStyle != 0 || dayFontColour != null) scheme |= F_EXTENDED; writer.put((byte) scheme); colourInfo.write(writer); if (xpm.hasImage()) xpm.writeImage(writer); // The labels have a length byte to show the number of bytes following. There is // also a flag in the length. The strings have a language number proceeding them. // The strings themselves are null terminated. if ((scheme & F_LABEL) != 0) writeLabelBlock(writer, encoder); // The extension section hold font style and colour information for the labels. if ((scheme & F_EXTENDED) != 0) { writeExtendedFontInfo(writer); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/typ/ColourInfo.java0000644000076400007640000002215211711515370023352 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.typ; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Writeable; /** * Holds colour information for elements in the typ file. * * The Colour information can relate to a bitmap or solid shapes. * * @author Steve Ratcliffe */ public class ColourInfo implements Writeable, AlphaAdder { private static final int S_NIGHT = 1; private static final int S_DAY_TRANSPARENT = 0x2; private static final int S_NIGHT_TRANSPARENT = 0x4; private static final int S_HAS_BITMAP = 0x8; private int numberOfColours; private int numberOfSolidColours; private boolean hasBitmap; private boolean hasBorder; private final List colours = new ArrayList(); private final Map indexMap = new HashMap(); private char width; private char height; private char charsPerPixel; private boolean simple = true; private char colourMode; /** * Add a colour for this element. * @param tag The xpm tag that represents the colour. * @param rgb The actual colour. */ public void addColour(String tag, Rgb rgb) { RgbWithTag cwt = new RgbWithTag(tag, rgb); colours.add(cwt); } /** * Add a transparent colour. Convenience routine. */ public void addTransparent(String colourTag) { addColour(colourTag, new Rgb(0, 0, 0, 0)); } public void setHasBitmap(boolean hasBitmap) { this.hasBitmap = hasBitmap; } /** * The colour scheme in use. This is a bitmask that has the following bits: * 0 - Has night colour * 1 - day background colour is transparent * 2 - night background colour is transparent * 3 - has bitmap * * If there is no night colour, then set the night background colour bit to be the same as * the day one. * * @return The colour scheme bitmask. The term colour scheme is historical, it doesn't really * describe it. */ public int getColourScheme() { if (numberOfColours == 0) numberOfColours = colours.size(); int scheme = 0; if (hasBitmap) scheme |= S_HAS_BITMAP; if (numberOfColours == 4) scheme |= S_NIGHT; if (!hasBitmap && !hasBorder && numberOfColours == 2) scheme |= S_NIGHT | S_DAY_TRANSPARENT | S_NIGHT_TRANSPARENT; if (numberOfColours < 2 || colours.get(1).isTransparent()) scheme |= S_DAY_TRANSPARENT; if (numberOfColours == 4 && (colours.get(3).isTransparent())) scheme |= S_NIGHT_TRANSPARENT; if ((scheme & S_NIGHT) == 0) if ((scheme & S_DAY_TRANSPARENT) != 0) scheme |= S_NIGHT_TRANSPARENT; return scheme; } /** * Get the number of bits per pixel that will be used in the written bitmap. * * This depends on the colour mode and number of colours to be represented. */ public int getBitsPerPixel() { if (simple) return 1; // number of colours includes the transparent pixel in colormode=0x10 so this // works for all colour modes. int nc = numberOfColours; if (nc == 0) return 24; else if (nc < 2) return 1; else if (nc < 4) return 2; else if (nc < 16) return 4; else return 8; } /** * Write out the colours only. */ public void write(ImgFileWriter writer) { if (colourMode == 0x20) { writeColours20(writer); } else { for (Rgb rgb : colours) { if (!rgb.isTransparent()) rgb.write(writer, (byte) 0x10); } } } /** * Write out the colours in the colormode=x20 case. */ private void writeColours20(ImgFileWriter writer) { BitWriter bw = new BitWriter(); for (Rgb rgb : colours) { bw.putn(rgb.getB(), 8); bw.putn(rgb.getG(), 8); bw.putn(rgb.getR(), 8); int alpha = 0xff - rgb.getA(); alpha = alphaRound4(alpha); bw.putn(alpha, 4); } writer.put(bw.getBytes(), 0, bw.getLength()); } /** * Round alpha value to four bits. * @param alpha The original alpha value eg 0xf0. * @return Rounded alpha to four bits eg 0xe. */ static int alphaRound4(int alpha) { int top = (alpha >> 4) & 0xf; int low = alpha & 0xf; int diff = low-top; if (diff > 8) top++; else if (diff < -8) top--; return top; } public int getIndex(String tag) { Integer ind = indexMap.get(tag); // If this is a simple bitmap (for line or polygon), then the foreground colour is // first and so has index 0, but we want the foreground to have index 1, so reverse. if (simple) ind = ~ind; return ind; } public void setWidth(int width) { this.width = (char) width; } public void setHeight(int height) { this.height = (char) height; } public void setNumberOfColours(int numberOfColours) { this.numberOfColours = numberOfColours; } public void setCharsPerPixel(int charsPerPixel) { this.charsPerPixel = (char) (charsPerPixel == 0 ? 1 : charsPerPixel); } public int getNumberOfColours() { return numberOfColours; } public int getNumberOfSColoursForCM() { if (colourMode == 0x10) return numberOfSolidColours; else return numberOfColours; } public int getCharsPerPixel() { return charsPerPixel; } public int getHeight() { return height; } public int getWidth() { return width; } public int getColourMode() { return colourMode; } public void setColourMode(int colourMode) { this.colourMode = (char) colourMode; } public void setSimple(boolean simple) { this.simple = simple; } public void setHasBorder(boolean hasBorder) { this.hasBorder = hasBorder; } /** * Replace the last pixel with a pixel with the same colour components and the given * alpha. * * This is used when the alpha value is specified separately to the colour values in the * input file. * @param alpha The alpha value to be added to the pixel. This is a real alpha, not a transparency. */ public void addAlpha(int alpha) { int last = colours.size(); RgbWithTag rgb = colours.get(last - 1); rgb = new RgbWithTag(rgb, alpha); colours.set(last - 1, rgb); } /** * Analyse the colour pallet and normalise it. * * Try to work out what is required from the supplied colour pallet and set the colour mode * and rearrange transparent pixels if necessary to be in the proper place. * * At the end we build the index from colour tag to pixel index. * * @param simple If this is a line or polygon. * @return A string describing the validation failure. */ public String analyseColours(boolean simple) { setSimple(simple); if (simple) { // There can be up to four colours, no partial transparency, and a max of one transparent pixel // in each of the day/night sections. if (numberOfColours > 4) return ("Too many colours for a line or polygon"); if (numberOfColours == 0) return "Line or polygon cannot have zero colours"; // Putting the transparent pixel first is common, so reverse if found if (colours.get(0).isTransparent()) { if (numberOfColours < 2) return "Only colour cannot be transparent for line or polygon"; swapColour(0, 1); } if (numberOfColours > 2 && colours.get(2).isTransparent()) { if (numberOfColours < 4) return "Only colour cannot be transparent for line or polygon"; swapColour(2, 3); } // There can only be one transparent pixel per colour pair if (numberOfColours > 1 && colours.get(0).isTransparent()) return "Both day foreground and background are transparent"; if (numberOfColours > 3 && colours.get(2).isTransparent()) return "Both night foreground and background are transparent"; } else { int transIndex = 0; // index of last transparent pixel, only used when there is only one int nTrans = 0; // completely transparent int nAlpha = 0; // partially transparent int count = 0; // total number of colours for (RgbWithTag rgb : colours) { if (rgb.isTransparent()) { nTrans++; transIndex = count; } if (rgb.getA() != 0xff && rgb.getA() != 0) nAlpha++; count++; } if (nAlpha > 0 || (count > 0 && count == nTrans)) { // If there is any partial transparency we need colour mode 0x20 // Also if there is only one pixel and it is transparent, since otherwise there would be zero // solid colours and that is a special case used to indicate a true colour pixmap. colourMode = 0x20; } else if (nTrans == 1) { colourMode = 0x10; // Ensure the transparent pixel is at the end RgbWithTag rgb = colours.remove(transIndex); colours.add(rgb); } } int count = 0; for (RgbWithTag rgb : colours) { indexMap.put(rgb.getTag(), count++); if (!rgb.isTransparent()) numberOfSolidColours++; } return null; } private void swapColour(int c1, int c2) { RgbWithTag tmp = colours.get(c1); colours.set(c1, colours.get(c2)); colours.set(c2, tmp); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/ImgFile.java0000644000076400007640000000440011400726372021770 0ustar stevesteve/* * 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: 03-Dec-2006 */ package uk.me.parabola.imgfmt.app; import java.io.Closeable; import java.io.IOException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.log.Logger; /** * Base class for all the img files. There is a common header that * all the sub-files share. They also have means of reading and writing * themselves. * * @author Steve Ratcliffe */ public abstract class ImgFile implements Closeable { private static final Logger log = Logger.getLogger(ImgFile.class); private CommonHeader header; private ImgFileWriter writer; private ImgFileReader reader; private boolean readable; private boolean writable; public void close() { try { sync(); } catch (IOException e) { log.debug("could not sync file"); } Utils.closeFile(writer); Utils.closeFile(reader); } public int position() { if (readable) return (int) reader.position(); else return writer.position(); } protected CommonHeader getHeader() { return header; } public long getSize() { if (writable) return writer.getSize(); throw new UnsupportedOperationException("getSize not implemented for read"); } protected void position(long pos) { writer.position(pos); } protected final void sync() throws IOException { if (!writable) return; getWriter().sync(); } protected ImgFileWriter getWriter() { return writer; } protected void setWriter(ImgFileWriter writer) { writable = true; this.writer = writer; } protected ImgFileReader getReader() { return reader; } protected void setReader(ImgFileReader reader) { readable = true; this.reader = reader; } protected final void setHeader(CommonHeader header) { this.header = header; } protected boolean isWritable() { return writable; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Exit.java0000644000076400007640000000311511400726372021367 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.lbl.ExitFacility; import uk.me.parabola.imgfmt.app.lbl.Highway; /** * Represent a motorway exit * * @author Mark Burton */ public class Exit { public final static String TAG_ROAD_REF = "exit:road_ref"; public final static String TAG_TO = "exit:to"; public final static String TAG_FACILITY = "exit:facility"; private final Highway highway; private Label description; private final List facilities = new ArrayList(); public Exit(Highway highway) { this.highway = highway; } public void setDescription(Label description) { this.description = description; } public void addFacility(ExitFacility facility) { facilities.add(facility); } public boolean getOvernightParking() { return false; // FIXME } public Highway getHighway() { return highway; } public List getFacilities() { return facilities; } public Label getDescription() { return description; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Section.java0000644000076400007640000000711711657205245022075 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 15, 2007 */ package uk.me.parabola.imgfmt.app; import java.io.IOException; /** * Represents an item size the position where those items start and the * total size of the section. */ public class Section { private char itemSize; private int size; private int position; private Section link; private int extraValue; public Section() { } public Section(char itemSize) { this.itemSize = itemSize; } public Section(Section link, char itemSize) { this.itemSize = itemSize; this.link = link; } public Section(Section link) { this.link = link; } public void inc() { size += itemSize; } public char getItemSize() { return itemSize; } public void setItemSize(char itemSize) { this.itemSize = itemSize; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } /** * Get the start position of this section. If this is linked to another * section, then we return the end address of that section. * @return The first offset for this section. */ public int getPosition() { if (link != null) return link.getEndPos(); return position; } public void setPosition(int position) { this.position = position; // Setting a position breaks the link this.link = null; } /** * Get the position of the end of the section. * @return The offset of the end of the section relative to the beginning * of the application file. */ public int getEndPos() { return getPosition() + size; } public String toString() { return "pos=" + getPosition() + ", size=" + size + ", itemSize=" + itemSize; } /** * Get the number of items in the section. This should only be called * if the itemSize is set. * @return The number of items in the section, or zero if this is not * a fixed size item kind of section. */ public int getNumItems() { if (itemSize == 0) return 0; return size/ (int) itemSize; } protected int getExtraValue() { return extraValue; } public void setExtraValue(int extraValue) { this.extraValue = extraValue; } public void readSectionInfo(ImgFileReader reader, boolean withItemSize) { setPosition(reader.getInt()); setSize(reader.getInt()); if (withItemSize) setItemSize(reader.getChar()); } public SectionWriter makeSectionWriter(ImgFileWriter writer) { setPosition(writer.position()); return new SectionWriter(writer, this); } public void writeSectionInfo(ImgFileWriter writer) { writeSectionInfo(writer, false); } public void writeSectionInfo(ImgFileWriter writer, boolean withItemSize) { writeSectionInfo(writer, withItemSize, false); } public void writeSectionInfo(ImgFileWriter writer, boolean withItemSize, boolean withExtraValue) { writer.putInt(getPosition()); writer.putInt(getSize()); if (withItemSize || getItemSize() > 0) writer.putChar(getItemSize()); if (withExtraValue) writer.putInt(getExtraValue()); } public static void close(ImgFileWriter writer) { assert writer instanceof SectionWriter; try { writer.close(); } catch (IOException e) { // ignore as this is only for section writers. } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/BufferedImgFileWriter.java0000644000076400007640000001215112333071201024620 0ustar stevesteve/* * 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; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * A straight forward implementation that just keeps all the data in a buffer * until the file needs to be written to disk. * * @author Steve Ratcliffe */ public class BufferedImgFileWriter implements ImgFileWriter { private static final Logger log = Logger.getLogger(BufferedImgFileWriter.class); private static final int KBYTE = 1024; private static final int INIT_SIZE = 16 * KBYTE; private static final int GROW_SIZE = 128 * KBYTE; private static final int GUARD_SIZE = KBYTE; private final ImgChannel chan; private ByteBuffer buf = ByteBuffer.allocate(INIT_SIZE); private int bufferSize = INIT_SIZE; // The size of the file. Note that for this to be set properly, the // position must be set to a low value after the full file is written. This // always happens because we go back and write the header after all is // written. private int maxSize; // The maximum allowed file size. private long maxAllowedSize = 0xffffff; public BufferedImgFileWriter(ImgChannel chan) { this.chan = chan; buf.order(ByteOrder.LITTLE_ENDIAN); } /** * Called to write out any saved buffers. The strategy may write * directly to the file in which case this would have nothing or * little to do. */ public void sync() throws IOException { buf.limit(maxSize); buf.position(0); log.debug("syncing to pos", chan.position(), ", size", buf.limit()); chan.write(buf); } /** * Get the position. Needed because may not be reflected in the underlying * file if being buffered. * * @return The logical position within the file. */ public int position() { return buf.position(); } /** * Set the position of the file. * * @param pos The new position in the file. */ public void position(long pos) { int cur = position(); if (cur > maxSize) maxSize = cur; buf.position((int) pos); } /** * Called when the stream is closed. Any resources can be freed. */ public void close() throws IOException { chan.close(); } /** * Write out a single byte. * * @param b The byte to write. */ public void put(byte b) { ensureSize(1); buf.put(b); } /** * Write out two bytes. Done in the correct byte order. * * @param c The value to write. */ public void putChar(char c) { ensureSize(2); buf.putChar(c); } /** * Write out a 3 byte value in the correct byte order etc. * * @param val The value to write. */ public void put3(int val) { ensureSize(3); buf.put((byte) (val & 0xff)); buf.putChar((char) (val >> 8)); } /** * Write out 4 byte value. * * @param val The value to write. */ public void putInt(int val) { ensureSize(4); buf.putInt(val); } /** * Write out an arbitrary length sequence of bytes. * * @param val The values to write. */ public void put(byte[] val) { ensureSize(val.length); buf.put(val); } /** * Write out part of a byte array. * * @param src The array to take bytes from. * @param start The start position. * @param length The number of bytes to write. */ public void put(byte[] src, int start, int length) { ensureSize(length); buf.put(src, start, length); } public void put(ByteBuffer src) { ensureSize(src.limit()); buf.put(src); } /** * Get the size of the file as written. * * NOTE: that calling this is only valid at certain times. * * @return The size of the file, if it is available. */ public long getSize() { return maxSize; } public ByteBuffer getBuffer() { return buf; } /** * Make sure there is enough room for the data we are about to write. * * @param length The amount of data. */ private void ensureSize(int length) { int needed = buf.position() + length; if (needed > (bufferSize - GUARD_SIZE)) { while(needed > (bufferSize - GUARD_SIZE)) bufferSize += GROW_SIZE; if (bufferSize > maxAllowedSize) { // Previous message was confusing people, although it is difficult to come // up with something that is strictly true in all situations. throw new MapFailedException( "There is not enough room in a single garmin map for all the input data." + " The .osm file should be split into smaller pieces first."); } ByteBuffer newb = ByteBuffer.allocate(bufferSize); newb.order(ByteOrder.LITTLE_ENDIAN); buf.flip(); newb.put(buf); buf = newb; } } public void setMaxSize(long maxSize) { this.maxAllowedSize = maxSize; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/0000755000076400007640000000000012651103763020425 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/SRTFile.java0000644000076400007640000001134312346663602022546 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt.app.srt; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.SectionWriter; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * The SRT file. This contains a table showing the sort order of * the characters that is being used. * * @author Steve Ratcliffe */ public class SRTFile extends ImgFile { private final SRTHeader header; private Sort sort; private boolean isMulti; private String description; private final List srt8Starts = new ArrayList<>(); public SRTFile(ImgChannel chan) { header = new SRTHeader(); setHeader(header); BufferedImgFileWriter fileWriter = new BufferedImgFileWriter(chan); fileWriter.setMaxSize(Long.MAX_VALUE); setWriter(fileWriter); // Position at the start of the writable area. position(header.getHeaderLength()); } /** * Write out the file. * This file has an unusual layout. There are several header like structures within * the main body of the file, with the real header being very small. */ public void write() { ImgFileWriter writer = getWriter(); writeDescription(writer); SectionWriter subWriter = header.makeSectionWriter(writer); subWriter.position(sort.isMulti()? SRTHeader.HEADER3_MULTI_LEN: SRTHeader.HEADER3_LEN); writeSrt4Chars(subWriter); writeSrt5Expansions(subWriter); if (sort.isMulti()) { for (int i = 0; i <= sort.getMaxPage(); i++) srt8Starts.add(-1); writeSrt8(subWriter); writeSrt7(subWriter); } subWriter.close(); // Header 2 is just after the real header writer.position(header.getHeaderLength()); header.writeHeader2(writer); // Header 3 is after the description writer.position(header.getHeaderLength() + description.length() + 1 + SRTHeader.HEADER2_LEN); header.writeHeader3(writer); header.writeHeader(writer); } private void writeDescription(ImgFileWriter writer) { writer.position(header.getHeaderLength() + SRTHeader.HEADER2_LEN); writer.put(description.getBytes(Charset.forName("ascii"))); writer.put((byte) 0); header.endDescription(writer.position()); } /** * Write SRT4 the character table. * * @param writer The img file writer. */ private void writeSrt4Chars(ImgFileWriter writer) { for (int i = 1; i < 256; i++) { writer.put(sort.getFlags(i)); writeWeights(writer, i); } header.endCharTable(writer.position()); } private void writeWeights(ImgFileWriter writer, int i) { if (isMulti) { writer.putChar((char) sort.getPrimary(i)); writer.put((byte) sort.getSecondary(i)); writer.put((byte) sort.getTertiary(i)); } else { writer.put((byte) sort.getPrimary(i)); writer.put((byte) ((sort.getTertiary(i) << 4) | (sort.getSecondary(i) & 0xf))); } } /** * Write SRT5, the expansion table. * * Write out the expansion table. This is referenced from the character table, when * the top nibble of the type is set via the primary position value. */ private void writeSrt5Expansions(ImgFileWriter writer) { int size = sort.getExpansionSize(); for (int j = 1; j <= size; j++) { CodePosition b = sort.getExpansion(j); if (isMulti) { writer.putChar(b.getPrimary()); writer.put(b.getSecondary()); writer.put(b.getTertiary()); } else { writer.put((byte) b.getPrimary()); writer.put((byte) ((b.getTertiary() << 4) | (b.getSecondary() & 0xf))); } } header.endTab2(writer.position()); } private void writeSrt7(SectionWriter writer) { assert sort.isMulti(); for (int i = 1; i <= sort.getMaxPage(); i++) { writer.putInt(srt8Starts.get(i)); } header.endSrt7(writer.position()); } private void writeSrt8(SectionWriter writer) { assert sort.isMulti(); int offset = 0; for (int p = 1; p <= sort.getMaxPage(); p++) { if (sort.hasPage(p)) { srt8Starts.set(p, offset); for (int j = 0; j < 256; j++) { int ch = p * 256 + j; writer.put(sort.getFlags(ch)); writeWeights(writer, ch); offset += 5; } } } header.endSrt8(writer.position()); } public void setSort(Sort sort) { this.sort = sort; header.setSort(sort); description = sort.getDescription(); isMulti = sort.isMulti(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/Sort.java0000644000076400007640000005312712354242000022213 0ustar stevesteve/* * Copyright (C) 2010, 2011. * * 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.imgfmt.app.srt; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.text.CollationKey; import java.text.Collator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.ExitException; import uk.me.parabola.imgfmt.app.Label; /** * Represents the sorting positions for all the characters in a codepage. * * A map contains a file that determines how the characters are to be sorted. So we * have to have to be able to create such a file and sort with exactly the same rules * as is contained in it. * * What about the java {@link java.text.RuleBasedCollator}? It turns out that it is possible to * make it work in the way we need it to, although it doesn't help with creating the srt file. * Also it is significantly slower than this implementation, so this one is staying. I also * found that sorting with the sort keys and the collator gave different results in some * cases. This implementation does not. * * Be careful when benchmarking. With small lists (< 10000 entries) repeated runs cause some * pretty aggressive optimisation to kick in. This tends to favour this implementation which has * much tighter loops that the java7 or ICU implementations, but this may not be realised with * real workloads. * * @author Steve Ratcliffe */ public class Sort { private static final byte[] ZERO_KEY = new byte[4]; private static final Integer NO_ORDER = 0; private int codepage; private int id1; // Unknown - identifies the sort private int id2; // Unknown - identifies the sort private String description; private Charset charset; private final Page[] pages = new Page[256]; private final List expansions = new ArrayList<>(); private int maxExpSize = 1; private CharsetEncoder encoder; private boolean multi; private int maxPage; public Sort() { pages[0] = new Page(); } public void add(int ch, int primary, int secondary, int tertiary, int flags) { ensurePage(ch >>> 8); if (getPrimary(ch) != 0) throw new ExitException(String.format("Repeated primary index 0x%x", ch & 0xff)); setPrimary (ch, primary); setSecondary(ch, secondary); setTertiary( ch, tertiary); setFlags(ch, flags); } /** * Run after all sorting order points have been added. * * Make sure that all tertiary values of secondary ignorable are greater * than any normal tertiary value. * * And the same for secondaries on primary ignorable. */ public void finish() { int maxSecondary = 0; int maxTertiary = 0; for (Page p : pages) { if (p == null) continue; for (int i = 0; i < 256; i++) { if (((p.flags[i] >>> 4) & 0x3) == 0) { if (p.getPrimary(i) != 0) { byte second = p.getSecondary(i); maxSecondary = Math.max(maxSecondary, second); if (second != 0) { maxTertiary = Math.max(maxTertiary, p.getTertiary(i)); } } } } } for (Page p : pages) { if (p == null) continue; for (int i = 0; i < 256; i++) { if (((p.flags[i] >>> 4) & 0x3) != 0) continue; if (p.getPrimary(i) == 0) { if (p.getSecondary(i) == 0) { if (p.getTertiary(i) != 0) { p.setTertiary(i, p.getTertiary(i) + maxTertiary); } } else { p.setSecondary(i, p.getSecondary(i) + maxSecondary); } } } } } /** * Return a table indexed by a character value in the target codepage, that gives the complete sort * position of the character. * * This is only used for testing. * * @return A table of sort positions. */ public char[] getSortPositions() { char[] tab = new char[256]; for (int i = 1; i < 256; i++) { tab[i] = (char) (((getPrimary(i) << 8) & 0xff00) | ((getSecondary(i) << 4) & 0xf0) | (getTertiary(i) & 0xf)); } return tab; } /** * Create a sort key for a given unicode string. The sort key can be compared instead of the original strings * and will compare based on the sorting represented by this Sort class. * * Using a sort key is more efficient if many comparisons are being done (for example if you are sorting a * list of strings). * * @param object This is saved in the sort key for later retrieval and plays no part in the sorting. * @param s The string for which the sort key is to be created. * @param second Secondary sort key. * @param cache A cache for the created keys. This is for saving memory so it is essential that this * is managed by the caller. * @return A sort key. */ public SortKey createSortKey(T object, String s, int second, Map cache) { // If there is a cache then look up and return the key. // This is primarily for memory management, not for speed. byte[] key; if (cache != null) { key = cache.get(s); if (key != null) return new SrtSortKey<>(object, key, second); } try { char[] chars; if (isMulti()) { chars = s.toCharArray(); } else { ByteBuffer out = encoder.encode(CharBuffer.wrap(s)); byte[] bval = out.array(); chars = new char[bval.length]; for (int i = 0; i < bval.length; i++) chars[i] = (char) (bval[i] & 0xff); } // In theory you could have a string where every character expands into maxExpSize separate characters // in the key. However if we allocate enough space to deal with the worst case, then we waste a // vast amount of memory. So allocate a minimal amount of space, try it and if it fails reallocate the // maximum amount. // // We need +1 for the null bytes, we also +2 for a couple of expanded characters. For a complete // german map this was always enough in tests. key = new byte[(chars.length + 1 + 2) * 4]; try { fillCompleteKey(chars, key); } catch (ArrayIndexOutOfBoundsException e) { // Ok try again with the max possible key size allocated. key = new byte[(chars.length+1) * 4 * maxExpSize]; fillCompleteKey(chars, key); } if (cache != null) cache.put(s, key); return new SrtSortKey<>(object, key, second); } catch (CharacterCodingException e) { return new SrtSortKey<>(object, ZERO_KEY); } } /** * Create a sort key based on a Label. * * The label will contain the actual characters (after transliteration for example) * @param object This is saved in the sort key for later retrieval and plays no part in the sorting. * @param label The label, the actual written bytes/chars will be used as input to the sort. * @param second Secondary sort key. * @param cache A cache for the created keys. This is for saving memory so it is essential that this * is managed by the caller. * @return A sort key. */ public SortKey createSortKey(T object, Label label, int second, Map cache) { byte[] key; if (cache != null) { key = cache.get(label); if (key != null) return new SrtSortKey<>(object, key, second); } char[] encText = label.getEncText(); // In theory you could have a string where every character expands into maxExpSize separate characters // in the key. However if we allocate enough space to deal with the worst case, then we waste a // vast amount of memory. So allocate a minimal amount of space, try it and if it fails reallocate the // maximum amount. // // We need +1 for the null bytes, we also +2 for a couple of expanded characters. For a complete // german map this was always enough in tests. key = new byte[(encText.length + 1 + 2) * 4]; try { fillCompleteKey(encText, key); } catch (ArrayIndexOutOfBoundsException e) { // Ok try again with the max possible key size allocated. key = new byte[encText.length * 4 * maxExpSize + 4]; fillCompleteKey(encText, key); } if (cache != null) cache.put(label, key); return new SrtSortKey<>(object, key, second); } /** * Convenient version of create sort key method. * @see #createSortKey(Object, String, int, Map) */ public SortKey createSortKey(T object, String s, int second) { return createSortKey(object, s, second, null); } /** * Convenient version of create sort key method. * * @see #createSortKey(Object, String, int, Map) */ public SortKey createSortKey(T object, String s) { return createSortKey(object, s, 0, null); } public SortKey createSortKey(T object, Label label) { return createSortKey(object, label, 0, null); } public SortKey createSortKey(T object, Label label, int second) { return createSortKey(object, label, second, null); } /** * Fill in the key from the given byte string. * * @param bVal The string for which we are creating the sort key. * @param key The sort key. This will be filled in. */ private void fillCompleteKey(char[] bVal, byte[] key) { int start = fillKey(Collator.PRIMARY, bVal, key, 0); start = fillKey(Collator.SECONDARY, bVal, key, start); fillKey(Collator.TERTIARY, bVal, key, start); } /** * Fill in the output key for a given strength. * * @param input The input string in a particular 8 bit codepage. * @param outKey The output sort key. * @param start The index into the output key to start at. * @return The next position in the output key. */ private int fillKey(int type, char[] input, byte[] outKey, int start) { int index = start; for (char c : input) { if (!hasPage(c >>> 8)) continue; int exp = (getFlags(c) >> 4) & 0x3; if (exp == 0) { index = writePos(type, c, outKey, index); } else { // now have to redirect to a list of input chars, get the list via the primary value always. int idx = getPrimary(c); for (int i = idx - 1; i < idx + exp; i++) { int pos = expansions.get(i).getPosition(type); if (pos != 0) { if (type == Collator.PRIMARY) outKey[index++] = (byte) ((pos >>> 8) & 0xff); outKey[index++] = (byte) pos; } } } } if (type == Collator.PRIMARY) outKey[index++] = '\0'; outKey[index++] = '\0'; return index; } public int getPrimary(int ch) { return this.pages[ch >>> 8].getPrimary(ch); } public int getSecondary(int ch) { return this.pages[ch >>> 8].getSecondary(ch); } public int getTertiary(int ch) { return this.pages[ch >>> 8].getTertiary(ch); } public byte getFlags(int ch) { assert ch >= 0; return this.pages[ch >>> 8].flags[ch & 0xff]; } public int getCodepage() { return codepage; } public Charset getCharset() { return charset; } public int getId1() { return id1; } public void setId1(int id1) { this.id1 = id1; } public int getId2() { return id2; } public void setId2(int id2) { this.id2 = id2 & 0x7fff; } /** * Get the sort order as a single integer. * A combination of id1 and id2. I think that they are arbitrary so may as well treat them as one. * * @return id1 and id2 as if they were a little endian 2 byte integer. */ public int getSortOrderId() { return (this.id2 << 16) + (this.id1 & 0xffff); } /** * Set the sort order as a single integer. * @param id The sort order id. */ public void setSortOrderId(int id) { id1 = id & 0xffff; id2 = (id >>> 16) & 0x7fff; } public void setCodepage(int codepage) { this.codepage = codepage; charset = charsetFromCodepage(codepage); encoder = charset.newEncoder(); encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } /** * Add an expansion to the sort. * An expansion is a letter that sorts as if it were two separate letters. * * The case were two letters sort as if the were just one (and more complex cases) are * not supported or are unknown to us. * * @param ch The code point of this letter in the code page. * @param inFlags The initial flags, eg if it is a letter or not. * @param expansionList The letters that this letter sorts as, as code points in the codepage. */ public void addExpansion(int ch, int inFlags, List expansionList) { ensurePage(ch >>> 8); setFlags(ch, (byte) ((inFlags & 0xf) | (((expansionList.size()-1) << 4) & 0xf0))); // Check for repeated definitions if (getPrimary(ch) != 0) throw new ExitException(String.format("repeated code point %x", ch)); setPrimary(ch, (expansions.size() + 1)); setSecondary(ch, 0); setTertiary(ch, 0); maxExpSize = Math.max(maxExpSize, expansionList.size()); for (Integer b : expansionList) { CodePosition cp = new CodePosition(); cp.setPrimary((char) getPrimary(b & 0xff)); // Currently sort without secondary or tertiary differences to the base letters. cp.setSecondary((byte) getSecondary(b & 0xff)); cp.setTertiary((byte) getTertiary(b & 0xff)); expansions.add(cp); } } /** * Get the expansion with the given index, one based. * @param val The one-based index number of the extension. */ public CodePosition getExpansion(int val) { return expansions.get(val - 1); } public Collator getCollator() { return new SrtCollator(codepage); } public int getExpansionSize() { return expansions.size(); } public String toString() { return String.format("sort cp=%d order=%08x", codepage, getSortOrderId()); } private void setPrimary(int ch, int val) { this.pages[ch >>> 8].setPrimary(ch, val); } private void setSecondary(int ch, int val) { this.pages[ch >>> 8].setSecondary(ch, val); } private void setTertiary(int ch, int val) { this.pages[ch >>> 8].setTertiary(ch, val); } private void setFlags(int ch, int val) { this.pages[ch >>> 8].flags[ch & 0xff] = (byte) val; } public static Charset charsetFromCodepage(int codepage) { Charset charset; switch (codepage) { case 0: charset = Charset.forName("ascii"); break; case 65001: charset = Charset.forName("UTF-8"); break; case 932: // Java uses "ms932" for code page 932 // (Windows-31J, Shift-JIS + MS extensions) charset = Charset.forName("ms932"); break; default: charset = Charset.forName("cp" + codepage); break; } return charset; } public void setMulti(boolean multi) { this.multi = multi; } public boolean isMulti() { return multi; } public int getPos(int type, int ch) { return pages[ch >>> 8].getPos(type, ch); } public int writePos(int type, int ch, byte[] outkey, int start) { return pages[ch >>> 8].writePos(type, ch, outkey, start); } /** * Ensure that the given page exists in the page array. * * @param n The page index. */ private void ensurePage(int n) { assert n == 0 || isMulti(); if (this.pages[n] == null) { this.pages[n] = new Page(); if (n > maxPage) maxPage = n; } } /** * The max page, top 8+ bits of the character that we have information on. */ public int getMaxPage() { return maxPage; } /** * @return True if there is at least one character with the given page/block number. */ public boolean hasPage(int p) { return pages[p] != null; } /** * Holds the sort positions of a 256 character block. */ private static class Page { private final char[] primary = new char[256]; private final byte[] secondary = new byte[256]; private final byte[] tertiary = new byte[256]; private final byte[] flags = new byte[256]; char getPrimary(int ch) { return primary[ch & 0xff]; } void setPrimary(int ch, int val) { primary[ch & 0xff] = (char) val; } byte getSecondary(int ch) { return secondary[ch & 0xff]; } void setSecondary(int ch, int val) { secondary[ch & 0xff] = (byte) val; } byte getTertiary(int ch) { return tertiary[ch & 0xff]; } void setTertiary(int ch, int val) { tertiary[ch & 0xff] = (byte) val; } /** * Get the sort position data for a given strength for a character. * @param type The collation strength PRIMARY, SECONDARY etc. * @param ch The character. * @return The sorting weight for the given character. */ public int getPos(int type, int ch) { switch (type) { case Collator.PRIMARY: return getPrimary(ch) & 0xffff; case Collator.SECONDARY: return getSecondary(ch) & 0xff; case Collator.TERTIARY: return getTertiary(ch) & 0xff; default: assert false : "bad collation type passed"; return 0; } } /** * Write a sort position for a given character to a sort key. * @param strength The sort strength type. * @param ch The character. * @param outKey The output key. * @param start The offset into outKey, the new position is written here. * @return The new start offset, after the key information has been written. */ public int writePos(int strength, int ch, byte[] outKey, int start) { int pos = getPos(strength, ch); if (pos != 0) { if (strength == Collator.PRIMARY) outKey[start++] = (byte) ((pos >> 8) & 0xff); // for 2 byte charsets outKey[start++] = (byte) (pos & 0xff); } return start; } } /** * A collator that works with this sort. This should be used if you just need to compare two * strings against each other once. * * The sort key is better when the comparison must be done several times as in a sort operation. * * This implementation has the same effect when used for sorting as the sort keys. */ private class SrtCollator extends Collator { private final int codepage; private SrtCollator(int codepage) { this.codepage = codepage; } public int compare(String source, String target) { char[] chars1; char[] chars2; if (isMulti()) { chars1 = source.toCharArray(); chars2 = target.toCharArray(); } else { CharBuffer in1 = CharBuffer.wrap(source); CharBuffer in2 = CharBuffer.wrap(target); try { byte[] bytes1 = encoder.encode(in1).array(); byte[] bytes2 = encoder.encode(in2).array(); chars1 = new char[bytes1.length]; for (int i = 0; i < bytes1.length; i++) chars1[i] = (char) (bytes1[i] & 0xff); chars2 = new char[bytes2.length]; for (int i = 0; i < bytes2.length; i++) chars2[i] = (char) (bytes2[i] & 0xff); } catch (CharacterCodingException e) { throw new ExitException("character encoding failed unexpectedly", e); } } int strength = getStrength(); int res = compareOneStrength(chars1, chars2, Collator.PRIMARY); if (res == 0 && strength != PRIMARY) { res = compareOneStrength(chars1, chars2, Collator.SECONDARY); if (res == 0 && strength != SECONDARY) { res = compareOneStrength(chars1, chars2, Collator.TERTIARY); } } return res; } /** * Compare the bytes against primary, secondary or tertiary arrays. * @param char1 Bytes for the first string in the codepage encoding. * @param char2 Bytes for the second string in the codepage encoding. * @return Comparison result -1, 0 or 1. */ private int compareOneStrength(char[] char1, char[] char2, int type) { int res = 0; PositionIterator it1 = new PositionIterator(char1, type); PositionIterator it2 = new PositionIterator(char2, type); while (it1.hasNext() || it2.hasNext()) { int p1 = it1.next(); int p2 = it2.next(); if (p1 < p2) { res = -1; break; } else if (p1 > p2) { res = 1; break; } } return res; } public CollationKey getCollationKey(String source) { throw new UnsupportedOperationException("use Sort.createSortKey() instead"); } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SrtCollator that = (SrtCollator) o; if (codepage != that.codepage) return false; return true; } public int hashCode() { return codepage; } class PositionIterator implements Iterator { private final char[] chars; private final int len; private final int type; private int pos; private int expStart; private int expEnd; private int expPos; PositionIterator(char[] chars, int type) { this.chars = chars; this.len = chars.length; this.type = type; } public boolean hasNext() { return pos < len || expPos != 0; } /** * Get the next sort order value for the input string. Does not ever return values * that are ignorable. Returns NO_ORDER at (and beyond) the end of the string, this * value sorts less than any other and so makes shorter strings sort first. * @return The next non-ignored sort position. At the end of the string it returns * NO_ORDER. */ public Integer next() { int next; if (expPos == 0) { do { if (pos >= len) { next = NO_ORDER; break; } // Get the first non-ignorable at this level int c = chars[(pos++ & 0xff)]; if (!hasPage(c >>> 8)) { next = 0; continue; } int nExpand = (getFlags(c) >> 4) & 0x3; // Check if this is an expansion. if (nExpand > 0) { expStart = getPrimary(c) - 1; expEnd = expStart + nExpand; expPos = expStart; next = expansions.get(expPos).getPosition(type); if (++expPos > expEnd) expPos = 0; } else { next = getPos(type, c); } } while (next == 0); } else { next = expansions.get(expPos).getPosition(type); if (++expPos > expEnd) expPos = 0; } return next; } public void remove() { throw new UnsupportedOperationException("remove not supported"); } } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/MultiSortKey.java0000644000076400007640000000243411607644250023707 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.srt; /** * Combines a number of sort keys into one. The first is the primary sort and contains the * actual object being sorted. * * @author Steve Ratcliffe */ public class MultiSortKey implements SortKey { private final SortKey key1; private final SortKey key2; private final SortKey key3; public MultiSortKey(SortKey key1, SortKey key2, SortKey key3) { this.key1 = key1; this.key2 = key2; this.key3 = key3; } public T getObject() { return key1.getObject(); } public int compareTo(SortKey o) { MultiSortKey other = (MultiSortKey) o; int res = key1.compareTo(other.key1); if (res == 0) { res = key2.compareTo(other.key2); if (res == 0 && key3 != null) { res = key3.compareTo(other.key3); } } return res; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/SrtSortKey.java0000644000076400007640000000330412305453510023353 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt.app.srt; import java.util.Arrays; /** * Sort key created from a Srt {@link Sort} object that allows strings to be compared according to that sorting * scheme. * * @author Steve Ratcliffe */ class SrtSortKey implements SortKey { private final T orig; private final byte[] key; private int second; public SrtSortKey(T orig, byte[] key) { this.orig = orig; this.key = key; } public SrtSortKey(T orig, byte[] key, int second) { this.orig = orig; this.key = key; this.second = second; } public int compareTo(SortKey o) { SrtSortKey other = (SrtSortKey) o; int length = Math.min(this.key.length, other.key.length); for (int i = 0; i < length; i++) { int k1 = this.key[i] & 0xff; int k2 = other.key[i] & 0xff; if (k1 < k2) { return -1; } else if (k1 > k2) { return 1; } } //if (this.key.length < other.key.length) // return -1; //else if (this.key.length > other.key.length) // return 1; if (second == other.second) return 0; else if (second < other.second) return -1; else return 1; } public T getObject() { return orig; } public String toString() { return String.format("%s,%d", Arrays.toString(key), second); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/CodePosition.java0000644000076400007640000000300512346663602023671 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.srt; import java.text.Collator; /** * Represents the collation positions of a given code point. * * @author Steve Ratcliffe */ class CodePosition { private char primary; private byte secondary; private byte tertiary; public char getPrimary() { return primary; } public byte getSecondary() { return secondary; } public byte getTertiary() { return tertiary; } /** * Get the position with the given strength. * * @param type The strength, Collator.PRIMARY, SECONDARY etc. * @return The collation position at the given strength. */ public int getPosition(int type) { switch (type) { case Collator.PRIMARY: return primary; case Collator.SECONDARY: return secondary & 0xff; case Collator.TERTIARY: return tertiary & 0xff; default: return 0; } } public void setPrimary(char primary) { this.primary = primary; } public void setSecondary(byte secondary) { this.secondary = secondary; } public void setTertiary(byte tertiary) { this.tertiary = tertiary; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/SortKey.java0000644000076400007640000000205311532723256022672 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt.app.srt; /** * A sort key that allows efficient comparison of a string with a particular sorting order multiple times. * * The general idea is that you create a key for each object to be sorted and then sort the keys. Once the * keys are sorted you retrieve the original object via the {@link #getObject} method. * * @author Steve Ratcliffe */ public interface SortKey extends Comparable> { /** * Get the object associated with this sort key. * This will usually be the real object being sorted. */ public T getObject(); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/SRTHeader.java0000644000076400007640000001225712346663602023064 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt.app.srt; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; import uk.me.parabola.imgfmt.app.SectionWriter; /** * The header of the SRT file. * * This file determines the sort order of the label characters. * * @author Steve Ratcliffe */ public class SRTHeader extends CommonHeader { // The header length we are using for the SRT file private static final int HEADER_LEN = 29; protected static final int HEADER2_LEN = 16; protected static final int HEADER3_LEN = 52; protected static final int HEADER3_MULTI_LEN = 92; // The section structure of this file is somewhat different to other // files, but I am still going to model it using Section. private final Section header = new Section(); private final Section desc = new Section(header); private final Section subheader = new Section(desc); private final Section chartab = new Section((char) 3); private final Section expansions = new Section(chartab, (char) 2); private final Section srt8 = new Section(expansions, (char) 5); private final Section srt7 = new Section(srt8, (char) 4); private Sort sort; public SRTHeader() { super(HEADER_LEN, "GARMIN SRT"); header.setPosition(HEADER_LEN); header.setSize(16); chartab.setPosition(HEADER3_LEN); } protected void readFileHeader(ImgFileReader reader) throws ReadFailedException { throw new UnsupportedOperationException("not implemented yet"); } /** * Write out the application header. This is unusual as it just points * to an area which is itself just a header. */ protected void writeFileHeader(ImgFileWriter writer) { writer.putChar((char) 1); writer.putInt(header.getPosition()); writer.putChar((char) header.getSize()); } /** * Section writer to write the character and expansions sections. These two sections are embedded within another * section and their offsets are relative to that section. * @param writer The real underlying writer. * @return A new writer where offsets are relative to the start of the sub-header section. */ SectionWriter makeSectionWriter(ImgFileWriter writer) { return new SectionWriter(writer, subheader); } /** * This is a header with pointers to the description and the main section. * The offsets contained in this section are relative to the beginning of the file. * @param writer Header is written here. */ protected void writeHeader2(ImgFileWriter writer) { desc.writeSectionInfo(writer); subheader.writeSectionInfo(writer); } /** * Header contained within the main section. Offsets within this section are relative to the beginning * of the section. * @param writer Header is written here. */ protected void writeHeader3(ImgFileWriter writer) { if (sort.isMulti()) { writer.putChar((char) HEADER3_MULTI_LEN); } else { writer.putChar((char) HEADER3_LEN); } writer.putChar((char) sort.getId1()); writer.putChar((char) sort.getId2()); writer.putChar((char) sort.getCodepage()); if (sort.isMulti()) writer.putInt(0x6f02); else writer.putInt(0x2002); chartab.writeSectionInfo(writer, true, true); writer.putChar((char) 0); expansions.writeSectionInfo(writer, true, true); writer.putChar((char) 0); // SRT6 A repeat pointer to the single byte character table writer.putInt(chartab.getPosition()); writer.putInt(0); if (sort.isMulti()) { writer.putInt(1); writer.putInt(sort.getMaxPage()); // max block in srt7 srt7.writeSectionInfo(writer, true); writer.putChar((char) 0); writer.putInt(0); srt8.writeSectionInfo(writer, true); writer.putChar((char) 0); writer.putInt(0); } } public void setSort(Sort sort) { this.sort = sort; if (sort.isMulti()) { chartab.setPosition(HEADER3_MULTI_LEN); chartab.setItemSize((char) 5); expansions.setItemSize((char) 4); } } /** Called after the description has been written to record the position. */ public void endDescription(int position) { desc.setSize(position - desc.getPosition()); subheader.setPosition(position); } /** Called after the character table has been written to record the position. */ public void endCharTable(int position) { chartab.setSize(position - chartab.getPosition()); } /** Called after the expansions has been written to record the position. */ public void endTab2(int postition) { subheader.setSize(postition - subheader.getPosition()); expansions.setSize(postition - expansions.getPosition()); } public void endSrt8(int position) { srt8.setSize(position - srt8.getPosition()); } public void endSrt7(int position) { srt7.setSize(position - srt7.getPosition()); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/package.html0000644000076400007640000000275611330560764022721 0ustar stevesteve

SRT File

This file is used to specify the character sorting order and which characters are the 'same' for the purposes of searching. All accented versions of a character will have the same code.

The first section has three byte records that have the following meaning

Byte number Description
1 The low nibble is 1 for letters, 2 for digits and zero for everything else.
2 This is the sort order for the unaccented form of the character. So a, , , and would all have the same code here. The next byte allows you to put them in order.
3 Upper nibble is 2 for uppercase letters, 1 for lowercase letters. It can also have the values 3 or 4 for unknown reasons.

The lower nibble is a number that can be added to byte 2 to get the full sorting order. This sorts the accented versions of the characters

The next section has two byte records with an unknown function.

Unknowns

At the time of writing Jan 2010 certain things are not known.
  • If it can deal with character pairs that sort as one.
  • What the second section is for.
  • Sorting for scandinavian languages where the accented characters come at the end of the alphabet. Perhaps you just treat them as separate letters and not as a base + accent, or perhaps there is a way of specifying it.
mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/IntegerSortKey.java0000644000076400007640000000246211532723256024214 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.srt; /** * Sort key when you are soring on a simple integer. * * @author Steve Ratcliffe */ public class IntegerSortKey implements SortKey { private final T object; private final int val; private final int second; public IntegerSortKey(T object, int val, int second) { this.object = object; this.val = val; this.second = second; } /** * Get the object associated with this sort key. This will usually be the real object being sorted. */ public T getObject() { return object; } public int compareTo(SortKey o) { IntegerSortKey other = (IntegerSortKey) o; if (val == other.val) { if (second == other.second) return 0; else if (second < other.second) return -1; else return 1; } else if (val < other.val) return -1; else return 1; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/srt/CombinedSortKey.java0000644000076400007640000000312511613353647024337 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.srt; /** * Allows you to combine another sort key with further integer comparisons. * Avoids having to cram two integers into one when there is the possibility that * they may not be enough bits to represent all values. * * @author Steve Ratcliffe */ public class CombinedSortKey implements SortKey { private final SortKey key; private final int first; private final int second; //public CombinedSortKey(SortKey key, int first) { // this(key, first, 0); //} public CombinedSortKey(SortKey obj, int first, int second) { this.key = obj; this.first = first; this.second = second; } public T getObject() { return key.getObject(); } public int compareTo(SortKey o) { CombinedSortKey other = (CombinedSortKey) o; int res = key.compareTo(other.key); if (res == 0) { res = compareInts(first, other.first); if (res == 0) { res = compareInts(second, other.second); } } return res; } private int compareInts(int i1, int i2) { int res; if (i1 == i2) res = 0; else if (i1 < i2) res = -1; else res = 1; return res; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/0000755000076400007640000000000012651103763020377 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr17.java0000644000076400007640000000604411701624112022127 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Name indexes consisting of a prefix of the string and the record * number at which that prefix first occurs. So it is like 8 and 12 * except that they are all combined together in this one section. * * @author Steve Ratcliffe */ public class Mdr17 extends MdrSection { private PrefixIndex streets; private PrefixIndex streetsByCountry; private PrefixIndex cities; private PrefixIndex pois; public Mdr17(MdrConfig config) { setConfig(config); streets = new PrefixIndex(getConfig(), 4); streetsByCountry = new PrefixIndex(getConfig(), 4); cities = new PrefixIndex(getConfig(), 2); pois = new PrefixIndex(getConfig(), 4); } public void writeSectData(ImgFileWriter writer) { writeSubSect(writer, streets); writeSubSect(writer, cities); writeSubSect(writer, streetsByCountry); writeSubSect(writer, pois); } /** * Write one of the subsections that makes up the section. They are all similar and * have a header with the length and the record size and prefix length of the * records in the subsection. */ private void writeSubSect(ImgFileWriter writer, PrefixIndex index) { index.preWrite(); int len = index.getItemSize() * index.getNumberOfItems() + 2; if (len == 2) return; // nothing to do // The length is a variable length integer with the length indicated by a suffix. len = (len << 1) + 1; int mask = ~0xff; int count = 1; while ((len & mask) != 0) { mask <<= 8; len <<= 1; count++; } putN(writer, count, len); // Calculate the header. This code is unlikely to survive the finding of another example! // Have no idea what the real thinking behind this is. int prefixLength = index.getPrefixLength(); int header = (prefixLength - 1) << 8; header += (prefixLength + 1) * (prefixLength + 1); header += (index.getItemSize() - prefixLength - 1) * 0xa; writer.putChar((char) header); index.writeSectData(writer); } protected void releaseMemory() { streets = null; cities = null; streetsByCountry = null; pois = null; } public void addStreets(List streetList) { streets.createFromList(streetList); } public void addCities(List cityList) { cities.createFromList(cityList); } public void addStreetsByCountry(List streets) { streetsByCountry.createFromList(streets, true); } public void addPois(List poiList) { pois.createFromList(poiList); } public int getItemSize() { return 0; } protected int numberOfItems() { return 0; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr6Record.java0000644000076400007640000000226411713757443023225 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import uk.me.parabola.imgfmt.app.lbl.Zip; /** * Holds information about a zip that will make its way into mdr 6. * * @author WanMil */ public class Mdr6Record extends RecordBase implements NamedRecord { /** The zip index within its own map */ private final int zipIndex; private final String name; private int stringOffset; public Mdr6Record(Zip zip) { zipIndex = zip.getIndex(); name = zip.getLabel().getText(); } public int getZipIndex() { return zipIndex; } public String getName() { return name; } public int getStringOffset() { return stringOffset; } public void setStringOffset(int stringOffset) { this.stringOffset = stringOffset; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr8Record.java0000644000076400007640000000173711532723256023225 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * This section is a simple index into the streets section (mdr7). * * @author Steve Ratcliffe */ public class Mdr8Record extends ConfigBase { private String prefix; private int recordNumber; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public int getRecordNumber() { return recordNumber; } public void setRecordNumber(int recordNumber) { this.recordNumber = recordNumber; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr29.java0000644000076400007640000000776012325011461022140 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * An index of countries sorted by name with pointers to the other country related sections. * There is only one per name, not per name and map. * * @author Steve Ratcliffe */ public class Mdr29 extends MdrSection implements HasHeaderFlags { private final List index = new ArrayList<>(); private int max17; public Mdr29(MdrConfig config) { setConfig(config); } public void buildFromCountries(List countries) { Sort sort = getConfig().getSort(); List> keys = MdrUtils.sortList(sort, countries); // Sorted by name, for every new name we allocate a new 29 record and set the same one in every // country with the same name. String lastName = null; Mdr29Record mdr29 = null; for (SortKey key : keys) { Mdr14Record country = key.getObject(); String name = country.getName(); if (!name.equals(lastName)) { mdr29 = new Mdr29Record(); mdr29.setName(name); mdr29.setStrOffset(country.getStrOff()); index.add(mdr29); lastName = name; } assert mdr29 != null; country.setMdr29(mdr29); } } protected void preWriteImpl() { if (!index.isEmpty()) { Mdr29Record r = index.get(index.size() - 1); this.max17 = r.getMdr17(); } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int magic = getExtraValue(); boolean hasString = (magic & 1) != 0; boolean has26 = (magic & 0x8) != 0; boolean has17 = (magic & 0x30) != 0; PointerSizes sizes = getSizes(); int size24 = sizes.getSize(24); int size22 = sizes.getSize(22); int size25 = sizes.getSize(5); // NB appears to be size of 5 (cities), not 25 (cities with country). int size26 = has26? sizes.getSize(26): 0; int size17 = numberToPointerSize(max17); for (Mdr29Record record : index) { putN(writer, size24, record.getMdr24()); if (hasString) putStringOffset(writer, record.getStrOffset()); putN(writer, size22, record.getMdr22()); putN(writer, size25, record.getMdr25()); if (has26) putN(writer, size26, record.getMdr26()); if (has17) putN(writer, size17, record.getMdr17()); } } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { PointerSizes sizes = getSizes(); int size = sizes.getSize(24) + sizes.getSize(22) + sizes.getSize(5) // NB: not 25 ; if (isForDevice()) { size += numberToPointerSize(max17); } else { size += sizes.getStrOffSize(); size += sizes.getSize(26); } return size; } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return index.size(); } /** * Unknown flags. * * As there are 4 bits set and 4 extra fields, that might be it. Compare * to mdr28 where there are 3 extra fields and 3 bits set. Just a guess... */ public int getExtraValue() { if (isForDevice()) { int magic = 0x6; // 22 and 25 magic |= numberToPointerSize(max17) << 4; return magic; // +17, -26, -strings } else return 0xf; // strings, 22, 25 and 26 } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr29Record.java0000644000076400007640000000326511701624112023273 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * Holds the name and pointers to all the other country related sections for * the given name. * * @author Steve Ratcliffe */ public class Mdr29Record { private String name; private int mdr24; private int mdr22; private int mdr25; private int mdr26; private int strOffset; private int mdr17; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getMdr24() { return mdr24; } public void setMdr24(int mdr24) { if (this.mdr24 == 0) this.mdr24 = mdr24; } public int getMdr22() { return mdr22; } public void setMdr22(int mdr22) { if (this.mdr22 == 0) this.mdr22 = mdr22; } public int getMdr25() { return mdr25; } public void setMdr25(int mdr25) { if (this.mdr25 == 0) this.mdr25 = mdr25; } public int getMdr26() { return mdr26; } public void setMdr26(int mdr26) { if (this.mdr26 == 0) this.mdr26 = mdr26; } public int getStrOffset() { return strOffset; } public void setStrOffset(int strOffset) { this.strOffset = strOffset; } public int getMdr17() { return mdr17; } public void setMdr17(int mdr17) { if (this.mdr17 == 0) this.mdr17 = mdr17; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MDRHeader.java0000644000076400007640000000732311770652007023003 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; import uk.me.parabola.imgfmt.app.srt.Sort; /** * The header of the MDR file. * * Note that there are many possible sections in this file and that * only a certain number of them are needed. There are also many * different lengths for the record sizes of the sections. Finally * there are different sections and record sizes for the version * that gets loaded into the gmapsupp. * * @author Steve Ratcliffe */ public class MDRHeader extends CommonHeader { private static final int MAX_SECTIONS = 40; private final Section[] sections = new Section[MAX_SECTIONS+1]; private Sort sort; public MDRHeader(int headerLen) { super(headerLen, "GARMIN MDR"); // Do a quick initialisation. Link every section to the // previous one so that all the positions are correct. for (int i = 1; i < sections.length; i++) { Section prev = (i == 0) ? null : sections[i - 1]; sections[i] = new Section(prev); } sections[1].setPosition(getHeaderLength()); } protected void readFileHeader(ImgFileReader reader) throws ReadFailedException { throw new UnsupportedOperationException("not implemented yet"); } /** * Write out the application header. */ protected void writeFileHeader(ImgFileWriter writer) { writer.putChar((char) sort.getCodepage()); writer.putChar((char) sort.getId1()); writer.putChar((char) sort.getId2()); writer.putChar((char) 14); sections[1].writeSectionInfo(writer, true, true); sections[2].writeSectionInfo(writer, true, true); sections[3].writeSectionInfo(writer, true, true); sections[4].writeSectionInfo(writer, true, true); sections[5].writeSectionInfo(writer, true, true); sections[6].writeSectionInfo(writer, true, true); sections[7].writeSectionInfo(writer, true, true); sections[8].writeSectionInfo(writer, true, true); sections[9].writeSectionInfo(writer, true, true); sections[10].writeSectionInfo(writer, false, true); sections[11].writeSectionInfo(writer, true, true); sections[12].writeSectionInfo(writer, true, true); sections[13].writeSectionInfo(writer, true, true); sections[14].writeSectionInfo(writer, true, true); sections[15].writeSectionInfo(writer); writer.put((byte) 0); sections[16].writeSectionInfo(writer, true, true); sections[17].writeSectionInfo(writer, false, true); for (int n = 18; n <= 30; n++) sections[n].writeSectionInfo(writer, true, true); } public void setItemSize(int sectionNumber, int itemSize) { Section section = sections[sectionNumber]; section.setItemSize((char) itemSize); } public void setExtraValue(int sectionNumber, int extraValue) { Section section = sections[sectionNumber]; section.setExtraValue(extraValue); } public void setPosition(int sectionNumber, int position) { sections[sectionNumber].setPosition(position); } public void setEnd(int sectionNumber, int position) { Section s = sections[sectionNumber]; s.setSize(position - s.getPosition()); } public Sort getSort() { return sort; } public void setSort(Sort sort) { this.sort = sort; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr13Record.java0000644000076400007640000000377511532723256023305 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * Information about a region. * @author Steve Ratcliffe */ public class Mdr13Record extends RecordBase implements Comparable, NamedRecord { private int regionIndex; private int countryIndex; private int lblOffset; private int strOffset; private String name; private Mdr14Record mdr14; private Mdr28Record mdr28; /** * We sort first by map id and then by region id. */ public int compareTo(Mdr13Record o) { int v1 = (getMapIndex()<<16) + regionIndex; int v2 = (o.getMapIndex()<<16) + o.regionIndex; if (v1 < v2) return -1; else if (v1 > v2) return 1; else return 0; } public int getRegionIndex() { return regionIndex; } public void setRegionIndex(int regionIndex) { this.regionIndex = regionIndex; } public int getStrOffset() { return strOffset; } public void setStrOffset(int strOffset) { this.strOffset = strOffset; } public void setCountryIndex(int countryIndex) { this.countryIndex = countryIndex; } public int getCountryIndex() { return countryIndex; } public int getLblOffset() { return lblOffset; } public void setLblOffset(int lblOffset) { this.lblOffset = lblOffset; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Mdr14Record getMdr14() { return mdr14; } public void setMdr14(Mdr14Record mdr14) { this.mdr14 = mdr14; } public Mdr28Record getMdr28() { return mdr28; } public void setMdr28(Mdr28Record mdr28) { this.mdr28 = mdr28; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/NamedRecord.java0000644000076400007640000000121411532723256023425 0ustar stevesteve/* * Copyright (C) 2010. * * 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.imgfmt.app.mdr; /** * Marks a record that has a name. */ public interface NamedRecord { public int getMapIndex(); public String getName(); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr15.java0000644000076400007640000000732311701624112022126 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import uk.me.parabola.imgfmt.ExitException; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * The string table. This is not used by the device. * * There is a compressed and non-compressed version of this section. * We are starting with the regular string version. * * @author Steve Ratcliffe */ public class Mdr15 extends MdrSection { private final OutputStream stringFile; private int nextOffset; private Map strings = new HashMap(); private final Charset charset; private final File tempFile; public Mdr15(MdrConfig config) { setConfig(config); charset = config.getSort().getCharset(); try { tempFile = File.createTempFile("strings", null, config.getOutputDir()); tempFile.deleteOnExit(); stringFile = new BufferedOutputStream(new FileOutputStream(tempFile), 64 * 1024); // reserve the string at offset 0 to be the empty string. stringFile.write(0); nextOffset = 1; } catch (IOException e) { throw new ExitException("Could not create temporary file"); } } public int createString(String str) { Integer offset = strings.get(str); if (offset != null) return offset; int off; try { off = nextOffset; byte[] bytes = str.getBytes(charset); stringFile.write(bytes); stringFile.write(0); // Increase offset for the length of the string and the null byte nextOffset += bytes.length + 1; } catch (IOException e) { // Can't convert, return empty string instead. off = 0; } strings.put(str, off); return off; } /** * Tidy up after reading files. * Close the temporary file, and release the string table which is no longer * needed. */ public void releaseMemory() { strings = null; try { stringFile.close(); } catch (IOException e) { throw new MapFailedException("Could not close string temporary file"); } } public void writeSectData(ImgFileWriter writer) { FileInputStream stream = null; try { stream = new FileInputStream(tempFile); FileChannel channel = stream.getChannel(); ByteBuffer buf = ByteBuffer.allocate(32 * 1024); while (channel.read(buf) > 0) { buf.flip(); writer.put(buf); buf.compact(); } } catch (IOException e) { throw new ExitException("Could not write string section of index"); } finally { Utils.closeFile(stream); } } public int getItemSize() { // variable sized records. return 0; } /** * The meaning of number of items for this section is the largest string * offset possible. We are taking the total size of the string section * for this. */ public int getSizeForRecord() { return numberToPointerSize(nextOffset); } /** * There is no use for this as the records are not fixed length. * * @return Always zero, could return the number of strings. */ protected int numberOfItems() { return 0; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr21.java0000644000076400007640000000465712325011461022132 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * This section contains the streets sorted by region. * There is no pointer from region, unlike in the case with cities. * * @author Steve Ratcliffe */ public class Mdr21 extends Mdr2x { public Mdr21(MdrConfig config) { setConfig(config); } /** * We need to sort the streets by the name of their region. Within a region * group the streets are ordered by their own index. * * @param inStreets The list of streets from mdr7. */ public void buildFromStreets(List inStreets) { Sort sort = getConfig().getSort(); List> keys = new ArrayList<>(); Map cache = new HashMap<>(); for (Mdr7Record s : inStreets) { Mdr5Record city = s.getCity(); if (city == null) continue; Mdr13Record region = city.getMdrRegion(); if (region == null) continue; String name = region.getName(); if (name == null) continue; keys.add(sort.createSortKey(s, name, s.getIndex(), cache)); } Collections.sort(keys); String lastName = null; int lastMapid = 0; int record = 0; for (SortKey key : keys) { Mdr7Record street = key.getObject(); String name = street.getName(); int mapid = street.getMapIndex(); if (mapid != lastMapid || !name.equals(lastName)) { record++; streets.add(street); Mdr13Record mdrRegion = street.getCity().getMdrRegion(); if (mdrRegion != null) { Mdr28Record mdr28 = mdrRegion.getMdr28(); mdr28.setMdr21(record); } lastMapid = mapid; lastName = name; } } } protected boolean sameGroup(Mdr7Record street1, Mdr7Record street2) { return true; } /** * Not known what these flags signify. */ public int getExtraValue() { return 0x11800; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/HasHeaderFlags.java0000644000076400007640000000166511532723256024055 0ustar stevesteve/* * Copyright (C) 2008 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 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.imgfmt.app.mdr; /** * @author Steve Ratcliffe */ public interface HasHeaderFlags { /** * Return the value that is put in the header after the section start, len * and record size fields. * At least in some cases this field controls what fields and/or size * exist in the section. * @return The correct value based on the contents of the section. Zero * if nothing needs to be done. */ int getExtraValue(); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr27.java0000644000076400007640000000516511642347335022150 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Cities sorted by region name. * * @author Steve Ratcliffe */ public class Mdr27 extends MdrSection { private final List cities = new ArrayList(); public Mdr27(MdrConfig config) { setConfig(config); } /** * Cities are sorted by region and then by the mdr5 city record number. * @param list The complete list of cities from mdr5. */ public void sortCities(List list) { Sort sort = getConfig().getSort(); List> keys = new ArrayList>(); for (Mdr5Record c : list) { Mdr13Record mdrRegion = c.getMdrRegion(); if (mdrRegion != null) { SortKey key = sort.createSortKey(c, mdrRegion.getName(), c.getGlobalCityIndex()); keys.add(key); } } Collections.sort(keys); String lastName = null; int record = 0; for (SortKey key : keys) { record++; Mdr5Record city = key.getObject(); Mdr13Record mdrRegion = city.getMdrRegion(); Mdr28Record mdr28 = mdrRegion.getMdr28(); String name = mdr28.getName(); if (!name.equals(lastName)) { mdr28.setMdr27(record); lastName = name; } cities.add(city); } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int size = getItemSize(); for (Mdr5Record city : cities) { putN(writer, size, city.getGlobalCityIndex()); } } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { return getSizes().getCitySize(); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return cities.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr7.java0000644000076400007640000002220112551416073022050 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.MultiSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * The MDR 7 section is a list of all streets. Only street names are saved * and so I believe that the NET section is required to make this work. * * @author Steve Ratcliffe */ public class Mdr7 extends MdrMapSection { public static final int MDR7_HAS_STRING = 0x01; public static final int MDR7_HAS_NAME_OFFSET = 0x20; public static final int MDR7_PARTIAL_SHIFT = 6; public static final int MDR7_U1 = 0x2; public static final int MDR7_U2 = 0x4; private static final int MAX_NAME_OFFSET = 127; private final int codepage; private final boolean isMulti; private final boolean splitName; private List allStreets = new ArrayList<>(); private List streets = new ArrayList<>(); private final int u2size = 1; public Mdr7(MdrConfig config) { setConfig(config); Sort sort = config.getSort(); splitName = config.isSplitName(); codepage = sort.getCodepage(); isMulti = sort.isMulti(); } public void addStreet(int mapId, String name, int lblOffset, int strOff, Mdr5Record mdrCity) { if (name.isEmpty()) return; // Find a name prefix, which is either a shield or a word ending 0x1e. We are treating // a shield as a prefix of length one. int prefix = 0; if (name.charAt(0) < 7) prefix = 1; int sep = name.indexOf(0x1e); if (sep > 0) prefix = sep + 1; // Find a name suffix which begins with 0x1f sep = name.indexOf(0x1f); int suffix = 0; if (sep > 0) suffix = sep; // Large values can't actually work. if (prefix >= MAX_NAME_OFFSET || suffix >= MAX_NAME_OFFSET) return; Mdr7Record st = new Mdr7Record(); st.setMapIndex(mapId); st.setLabelOffset(lblOffset); st.setStringOffset(strOff); st.setName(name); st.setCity(mdrCity); st.setPrefixOffset((byte) prefix); st.setSuffixOffset((byte) suffix); allStreets.add(st); if (!splitName) return; boolean start = false; boolean inWord = false; int c; int outOffset = 0; int end = Math.min((suffix > 0) ? suffix : name.length() - prefix - 1, MAX_NAME_OFFSET); for (int nameOffset = 0; nameOffset < end; nameOffset += Character.charCount(c)) { c = name.codePointAt(prefix + nameOffset); // Don't use any word after a bracket if (c == '(') break; if (!Character.isLetterOrDigit(c)) { start = true; inWord = false; } else if (start && Character.isLetterOrDigit(c)) { inWord = true; } if (start && inWord && outOffset > 0) { st = new Mdr7Record(); st.setMapIndex(mapId); st.setLabelOffset(lblOffset); st.setStringOffset(strOff); st.setName(name); st.setCity(mdrCity); st.setNameOffset((byte) nameOffset); st.setOutNameOffset((byte) outOffset); st.setPrefixOffset((byte) prefix); st.setSuffixOffset((byte) suffix); //System.out.println(st.getName() + ": add partial " + st.getPartialName()); allStreets.add(st); start = false; } outOffset += outSize(c); if (outOffset > MAX_NAME_OFFSET) break; } } /** * Return the number of bytes that the given character will consume in the output encoded * format. */ private int outSize(int c) { if (codepage == 65001) { // For unicode a simple lookup gives the number of bytes. if (c < 0x80) { return 1; } else if (c <= 0x7FF) { return 2; } else if (c <= 0xFFFF) { return 3; } else if (c <= 0x10FFFF) { return 4; } else { throw new MapFailedException(String.format("Invalid code point: 0x%x", c)); } } else if (!isMulti) { // The traditional single byte code-pages, always one byte. return 1; } else { // Other multi-byte code-pages (eg ms932); can't currently create index for these anyway. return 0; } } /** * Since we change the number of records by removing some after sorting, * we sort and de-duplicate here. * This is a performance critical part of the index creation process * as it requires a lot of heap to store the sort keys. */ protected void preWriteImpl() { Sort sort = getConfig().getSort(); List> sortedStreets = new ArrayList<>(allStreets.size()); for (Mdr7Record m : allStreets) { sortedStreets.add(new MultiSortKey<>( sort.createSortKey(m, m.getPartialName()), sort.createSortKey(m, m.getInitialPart(), m.getMapIndex()), null)); } Collections.sort(sortedStreets); // De-duplicate the street names so that there is only one entry // per map for the same name. int recordNumber = 0; Mdr7Record last = new Mdr7Record(); for (int i = 0; i < sortedStreets.size(); i++){ SortKey sk = sortedStreets.get(i); Mdr7Record r = sk.getObject(); if (r.getMapIndex() == last.getMapIndex() && r.getName().equals(last.getName()) // currently think equals is correct, not collator.compare() && r.getPartialName().equals(last.getPartialName())) { // This has the same name (and map number) as the previous one. Save the pointer to that one // which is going into the file. r.setIndex(recordNumber); } else { recordNumber++; last = r; r.setIndex(recordNumber); streets.add(r); } // release memory sortedStreets.set(i, null); } } public void writeSectData(ImgFileWriter writer) { String lastName = null; String lastPartial = null; boolean hasStrings = hasFlag(MDR7_HAS_STRING); boolean hasNameOffset = hasFlag(MDR7_HAS_NAME_OFFSET); for (Mdr7Record s : streets) { addIndexPointer(s.getMapIndex(), s.getIndex()); putMapIndex(writer, s.getMapIndex()); int lab = s.getLabelOffset(); String name = s.getName(); if (!name.equals(lastName)) { lab |= 0x800000; lastName = name; } String partialName = s.getPartialName(); int trailingFlags = 0; if (!partialName.equals(lastPartial)) { trailingFlags |= 1; lab |= 0x800000; // If it is not a partial repeat, then it is not a complete repeat either } lastPartial = partialName; writer.put3(lab); if (hasStrings) putStringOffset(writer, s.getStringOffset()); if (hasNameOffset) writer.put(s.getOutNameOffset()); putN(writer, u2size, trailingFlags); } } /** * For the map number, label, string (opt), and trailing flags (opt). * The trailing flags are variable size. We are just using 1 now. */ public int getItemSize() { PointerSizes sizes = getSizes(); int size = sizes.getMapSize() + 3 + u2size; if (!isForDevice()) size += sizes.getStrOffSize(); if ((getExtraValue() & MDR7_HAS_NAME_OFFSET) != 0) size += 1; return size; } protected int numberOfItems() { return streets.size(); } /** * Value of 3 possibly the existence of the lbl field. */ public int getExtraValue() { int magic = MDR7_U1 | MDR7_HAS_NAME_OFFSET | (u2size << MDR7_PARTIAL_SHIFT); if (isForDevice()) { magic |= MDR7_U2; } else { magic |= MDR7_HAS_STRING; } return magic; } protected void releaseMemory() { allStreets = null; streets = null; } /** * Must be called after the section data is written so that the streets * array is already sorted. * @return List of index records. */ public List getIndex() { List list = new ArrayList<>(); for (int number = 1; number <= streets.size(); number += 10240) { String prefix = getPrefixForRecord(number); // need to step back to find the first... int rec = number; while (rec > 1) { String p = getPrefixForRecord(rec); if (!p.equals(prefix)) { rec++; break; } rec--; } Mdr8Record indexRecord = new Mdr8Record(); indexRecord.setPrefix(prefix); indexRecord.setRecordNumber(rec); list.add(indexRecord); } return list; } /** * Get the prefix of the name at the given record. * @param number The record number. * @return The first 4 (or whatever value is set) characters of the street * name. */ private String getPrefixForRecord(int number) { Mdr7Record record = streets.get(number-1); int endIndex = MdrUtils.STREET_INDEX_PREFIX_LEN; String name = record.getName(); if (endIndex > name.length()) { StringBuilder sb = new StringBuilder(name); while (sb.length() < endIndex) sb.append('\0'); name = sb.toString(); } return name.substring(0, endIndex); } public List getStreets() { return Collections.unmodifiableList(allStreets); } public List getSortedStreets() { return Collections.unmodifiableList(streets); } public void relabelMaps(Mdr1 maps) { relabel(maps, allStreets); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr23.java0000644000076400007640000000534611642347335022145 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Regions sorted by name. Same number of records as mdr13. * * @author Steve Ratcliffe */ public class Mdr23 extends MdrSection { private final List regions = new ArrayList(); public Mdr23(MdrConfig config) { setConfig(config); } /** * Takes the region list and sorts it according to the name. * @param list Original region list. */ public void sortRegions(List list) { Sort sort = getConfig().getSort(); List> keys = MdrUtils.sortList(sort, list); Collections.sort(keys); String lastName = null; int lastMapIndex = 0; int record = 0; for (SortKey key : keys) { Mdr13Record reg = key.getObject(); // Only add if different name or map String name = reg.getName(); if (reg.getMapIndex() != lastMapIndex || !name.equals(lastName)) { record++; reg.getMdr28().setMdr23(record); regions.add(reg); lastName = name; lastMapIndex = reg.getMapIndex(); } } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { String lastName = null; for (Mdr13Record reg : regions) { putMapIndex(writer, reg.getMapIndex()); int flag = 0; String name = reg.getName(); if (!name.equals(lastName)) { flag = 0x800000; lastName = name; } writer.putChar((char) reg.getRegionIndex()); writer.putChar((char) reg.getCountryIndex()); writer.put3(reg.getLblOffset() | flag); } } /** * There is a map index followed by file region and country indexes. * Then there is a label offset for the name, which strangely appears * to be three bytes always, although that many would rarely (or never?) be * required. */ public int getItemSize() { PointerSizes sizes = getSizes(); return sizes.getMapSize() + 2 + 2 + 3; } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return regions.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr20.java0000644000076400007640000001104312467220204022120 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.srt.MultiSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * This is a list of streets that belong to each city. * * It is sorted with each group of streets that belong to a city in the same * order as the cities, within each group the sort is by street id in mdr7. * * Streets that do not have an associated city are not included. * * There is a subsection in the mdr1 reverse index for this section, however * the map index is not saved as part of this record. * * @author Steve Ratcliffe */ public class Mdr20 extends Mdr2x { public Mdr20(MdrConfig config) { setConfig(config); } /** * We need to sort the streets by the name of the city. Within a city * group the streets are ordered by their own index. * * Also have to set the record number of the first record in this section * on the city. * * @param inStreets The list of streets from mdr7, must have Mdr7.index set. */ public void buildFromStreets(List inStreets) { Sort sort = getConfig().getSort(); // Use a key cache because there are a large number of street names but a much smaller number // of city, region and country names. Therefore we can reuse the memory needed for the keys // most of the time, particularly for the country and region names. Map cache = new HashMap<>(); List> keys = new ArrayList<>(); for (Mdr7Record s : inStreets) { Mdr5Record city = s.getCity(); if (city == null) continue; String name = city.getName(); if (name == null || name.isEmpty()) assert false; // We are sorting the streets, but we are sorting primarily on the // city name associated with the street, then on the street name. SortKey cityKey = sort.createSortKey(s, city.getName(), 0, cache); SortKey regionKey = sort.createSortKey(null, city.getRegionName(), 0, cache); // The streets are already sorted, with the getIndex() method revealing the sort order SortKey countryStreetKey = sort.createSortKey(null, city.getCountryName(), s.getIndex(), cache); // Combine all together so we can sort on it. SortKey key = new MultiSortKey<>(cityKey, regionKey, countryStreetKey); keys.add(key); } Collections.sort(keys); Collator collator = getConfig().getSort().getCollator(); String lastName = null; String lastPartialName = null; Mdr5Record lastCity = null; int record = 0; int cityRecord = 1; int lastMapNumber = 0; for (SortKey key : keys) { Mdr7Record street = key.getObject(); String name = street.getName(); String partialName = street.getPartialName(); Mdr5Record city = street.getCity(); boolean citySameByName = city.isSameByName(collator, lastCity); int mapNumber = city.getMapIndex(); // Only save a single copy of each street name. if (!citySameByName || mapNumber != lastMapNumber || lastName == null || lastPartialName == null || !name.equals(lastName) || !partialName.equals(lastPartialName)) { record++; streets.add(street); lastName = name; lastPartialName = partialName; } // The mdr20 value changes for each new city name if (citySameByName) { assert cityRecord!=0; city.setMdr20(cityRecord); } else { // New city name, this marks the start of a new section in mdr20 assert cityRecord != 0; cityRecord = record; city.setMdr20(cityRecord); lastCity = city; } lastMapNumber = mapNumber; } } /** * Two streets are in the same group if they have the same mdr20 id. */ protected boolean sameGroup(Mdr7Record street1, Mdr7Record street2) { if (street2 != null && street1.getCity().getMdr20() == street2.getCity().getMdr20()) return true; return false; } /** * Unknown. */ public int getExtraValue() { return isForDevice() ? 0xe : 0x8800; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr4Record.java0000644000076400007640000000315611642347335023220 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * The records in MDR 4 are a list of poi types with an unknown byte. */ public class Mdr4Record implements Comparable { private int type; private int subtype; private int unknown; public int compareTo(Mdr4Record o) { int t1 = ((type<<8) + subtype) & 0xffff; int t2 = ((o.type<<8) + o.subtype) & 0xffff; if (t1 == t2) return 0; else if (t1 < t2) return -1; else return 1; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Mdr4Record that = (Mdr4Record) o; if (subtype != that.subtype) return false; if (type != that.type) return false; return true; } public int hashCode() { int result = type; result = 31 * result + subtype; return result; } public int getType() { return type; } public void setType(int type) { this.type = type; } public int getSubtype() { return subtype; } public void setSubtype(int subtype) { this.subtype = subtype; } public int getUnknown() { return unknown; } public void setUnknown(int unknown) { this.unknown = unknown; } }mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/PrefixIndex.java0000644000076400007640000001044212325011461023457 0ustar stevesteve/* * Copyright (C) 2012. * * 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.imgfmt.app.mdr; import java.nio.charset.Charset; import java.text.Collator; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; /** * Holds an index of name prefixes to record numbers. * * Extends MdrSection, although is sometimes a subsection, not an actual section. * * @author Steve Ratcliffe */ public class PrefixIndex extends MdrSection { private final int prefixLength; private int maxIndex; // We use mdr8record for all similar indexes. private final List index = new ArrayList<>(); /** * Sets the config and the prefix length for this index. * * Prefix length may differ depending on the amount of data, so will have * to deal with that when it happens. * * @param config Configuration for sorting methods. * @param prefixLength The prefix length for this index. */ public PrefixIndex(MdrConfig config, int prefixLength) { setConfig(config); this.prefixLength = prefixLength; } /** * We can create an index for any type that has a name. * @param list A list of items that have a name. */ public void createFromList(List list, boolean grouped) { maxIndex = list.size(); // Prefixes are equal based on the primary unaccented character, so // we need to use the collator to test for equality and not equals(). Sort sort = getConfig().getSort(); Collator collator = sort.getCollator(); collator.setStrength(Collator.PRIMARY); String lastCountryName = null; String lastPrefix = ""; int inRecord = 0; // record number of the input list int outRecord = 0; // record number of the index for (NamedRecord r : list) { inRecord++; String prefix = getPrefix(r.getName()); if (collator.compare(prefix, lastPrefix) != 0) { outRecord++; Mdr8Record ind = new Mdr8Record(); ind.setPrefix(prefix); ind.setRecordNumber(inRecord); index.add(ind); lastPrefix = prefix; if (grouped) { // Peek into the real type to support the mdr17 feature of indexes sorted on country. Mdr5Record city = ((Mdr7Record) r).getCity(); if (city != null) { String countryName = city.getCountryName(); if (!countryName.equals(lastCountryName)) { city.getMdrCountry().getMdr29().setMdr17(outRecord); lastCountryName = countryName; } } } } } } public void createFromList(List list) { createFromList(list, false); } /** * Write the section or subsection. */ public void writeSectData(ImgFileWriter writer) { int size = numberToPointerSize(maxIndex); Charset charset = getConfig().getSort().getCharset(); for (Mdr8Record s : index) { writer.put(s.getPrefix().getBytes(charset), 0, prefixLength); putN(writer, size, s.getRecordNumber()); } } public int getItemSize() { return prefixLength + numberToPointerSize(maxIndex); } protected int numberOfItems() { return index.size(); } /** * Get the prefix of the name at the given record. * If the name is shorter than the prefix length, then it padded with nul characters. * So it can be longer than the input string. * * @param in The name to truncate. * @return A string prefixLength characters long, consisting of the initial * prefix of name and padded with nulls if necessary to make up the length. */ private String getPrefix(String in) { StringBuilder sb = new StringBuilder(); char[] chars = in.toCharArray(); int ci = 0; for (int i = 0; i < prefixLength; i++) { char c = 0; while (ci < chars.length) { // TODO: simplify when initial spaces are removed c = chars[ci++]; if (ci == 1 && c== 0x20) continue; if (c >= 0x20) break; } sb.append(c); } return sb.toString(); } public int getPrefixLength() { return prefixLength; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr18.java0000644000076400007640000000274711701624112022136 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * This is an index into 19 showing the start of each new type. * * Section 19 creates the data for this, we just write it out here. * * @author Steve Ratcliffe */ public class Mdr18 extends MdrSection implements HasHeaderFlags { private List poiTypes = new ArrayList(); public Mdr18(MdrConfig config) { setConfig(config); } public void writeSectData(ImgFileWriter writer) { int poiSize = getSizes().getSize(19); for (Mdr18Record pt : poiTypes) { writer.putChar((char) (pt.getType() | 0x4000)); putN(writer, poiSize, pt.getRecord()); } } public int getItemSize() { return 2 + getSizes().getSize(19); } protected int numberOfItems() { return poiTypes.size(); } public void setPoiTypes(List poiTypes) { this.poiTypes = poiTypes; } public int getExtraValue() { return 4 + getSizes().getSize(19)-1; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr11.java0000644000076400007640000001070112325011461022114 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.SortKey; import uk.me.parabola.imgfmt.app.trergn.Point; /** * Holds all the POIs, including cities. Arranged alphabetically by * the name. * * @author Steve Ratcliffe */ public class Mdr11 extends MdrMapSection { private List pois = new ArrayList<>(); private Mdr10 mdr10; public Mdr11(MdrConfig config) { setConfig(config); } public Mdr11Record addPoi(int mapIndex, Point point, String name, int strOff) { Mdr11Record poi = new Mdr11Record(); poi.setMapIndex(mapIndex); poi.setPointIndex(point.getNumber()); poi.setSubdiv(point.getSubdiv().getNumber()); poi.setLblOffset(point.getLabel().getOffset()); poi.setName(name); poi.setStrOffset(strOff); pois.add(poi); return poi; } /** * Sort and fill in the mdr10 information. * * The POI index contains individual references to POI by subdiv and index, so they are not * de-duplicated in the index in the same way that streets and cities are. */ protected void preWriteImpl() { List> keys = MdrUtils.sortList(getConfig().getSort(), pois); pois.clear(); for (SortKey sk : keys) { Mdr11Record poi = sk.getObject(); mdr10.addPoiType(poi); pois.add(poi); } } public void writeSectData(ImgFileWriter writer) { int count = 1; boolean hasStrings = hasFlag(2); for (Mdr11Record poi : pois) { addIndexPointer(poi.getMapIndex(), count); poi.setRecordNumber(count++); putMapIndex(writer, poi.getMapIndex()); writer.put((byte) poi.getPointIndex()); writer.putChar((char) poi.getSubdiv()); writer.put3(poi.getLblOffset()); if (poi.isCity()) putRegionIndex(writer, poi.getRegionIndex()); else putCityIndex(writer, poi.getCityIndex(), true); if (hasStrings) putStringOffset(writer, poi.getStrOffset()); } } public int getItemSize() { PointerSizes sizes = getSizes(); int size = sizes.getMapSize() + 6 + sizes.getCitySizeFlagged(); if (hasFlag(0x2)) size += sizes.getStrOffSize(); return size; } protected int numberOfItems() { return pois.size(); } public int getNumberOfPois() { return getNumberOfItems(); } public int getExtraValue() { int mdr11flags = 0x11; PointerSizes sizes = getSizes(); // two bit field for city bytes. minimum size of 2 int citySize = sizes.getCitySizeFlagged(); if (citySize > 2) mdr11flags |= (citySize-2) << 2; if (isForDevice()) mdr11flags |= 0x80; else mdr11flags |= 0x2; return mdr11flags; } public List getIndex() { List list = new ArrayList<>(); for (int number = 1; number <= pois.size(); number += 10240) { String prefix = getPrefixForRecord(number); // need to step back to find the first... int rec = number; while (rec > 1) { String p = getPrefixForRecord(rec); if (!p.equals(prefix)) { rec++; break; } rec--; } Mdr12Record indexRecord = new Mdr12Record(); indexRecord.setPrefix(prefix); indexRecord.setRecordNumber(rec); list.add(indexRecord); } return list; } /** * Get the prefix of the name at the given record. * @param number The record number. * @return The first 4 (or whatever value is set) characters of the street * name. */ private String getPrefixForRecord(int number) { Mdr11Record record = pois.get(number-1); int endIndex = MdrUtils.POI_INDEX_PREFIX_LEN; String name = record.getName(); if (endIndex > name.length()) { StringBuilder sb = new StringBuilder(name); while (sb.length() < endIndex) sb.append('\0'); name = sb.toString(); } return name.substring(0, endIndex); } public void setMdr10(Mdr10 mdr10) { this.mdr10 = mdr10; } public void releaseMemory() { pois = null; mdr10 = null; } public List getPois() { return new ArrayList<>(pois); } public void relabelMaps(Mdr1 maps) { relabel(maps, pois); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MdrUtils.java0000644000076400007640000000665611701624112023011 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * A bunch of static routines for use in creating the MDR file. */ public class MdrUtils { public static final int STREET_INDEX_PREFIX_LEN = 4; public static final int POI_INDEX_PREFIX_LEN = 4; /** * Get the group number for the poi. This is the first byte of the records * in mdr9. * * Not entirely sure about how this works yet. * @param fullType The primary type of the object. * @return The group number. This is a number between 1 and 9 (and later * perhaps higher numbers such as 0x40, so do not assume there are no * gaps). */ public static int getGroupForPoi(int fullType) { // We group pois based on their type. This may not be the final thoughts on this. int type = MdrUtils.getTypeFromFullType(fullType); int group = 0; if (fullType < 0xf) group = 1; else if (type >= 0x2a && type <= 0x30) { group = type - 0x28; } else if (type == 0x28) { group = 9; } return group; } public static boolean canBeIndexed(int fullType) { return getGroupForPoi(fullType) != 0; } private static int getTypeFromFullType(int fullType) { if ((fullType & 0xfff00) > 0) return (fullType>>8) & 0xfff; else return fullType & 0xff; } /** * Gets the subtype if there is one, else the type. * @param fullType The type in the so-called 'full' format. * @return If there is a subtype, then it is returned. Otherwise the type is returned. */ public static int getSubtypeOrTypeFromFullType(int fullType) { return fullType & 0xff; } /** * Sort records that are sorted by a name. They appropriate sort order will be used. * @param sort The sort to be applied. * @param list The list to be sorted. * @param One of the Mdr?Record types that need to be sorted on a text field, eg street name. * @return A list of sort keys in the sorted order. The original object is retrieved from the key * by calling getObject(). */ public static List> sortList(Sort sort, List list) { List> toSort = new ArrayList>(list.size()); for (T m : list) { SortKey sortKey = sort.createSortKey(m, m.getName(), m.getMapIndex()); toSort.add(sortKey); } Collections.sort(toSort); return toSort; } /** * The 'natural' type is always a combination of the type and subtype with the type * shifted 5 bits and the sub type in the low 5 bits. * * For various reasons, we use 'fullType' in which the type is shifted up a full byte * or is in the lower byte. * * @param ftype The so-called full type of the object. * @return The natural type as defined above. */ public static int fullTypeToNaturalType(int ftype) { int type = getTypeFromFullType(ftype); int sub = 0; if ((ftype & ~0xff) != 0) sub = ftype & 0x1f; return type << 5 | sub; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MdrSection.java0000644000076400007640000001366412325011461023312 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Super class of all sections. * @author Steve Ratcliffe */ public abstract class MdrSection extends ConfigBase { private PointerSizes sizes; private boolean released; protected int nItems; private boolean sizeValid; /** * Write out the contents of this section. * @param writer Where to write it. */ public abstract void writeSectData(ImgFileWriter writer); /** * The size of a record in the section. This is not a constant and * might vary on various factors, such as the file version, if we are * preparing for a device, the number of maps etc. * * @return The size of a record in this section. */ public abstract int getItemSize(); protected PointerSizes getSizes() { return sizes; } public void setSizes(PointerSizes sizes) { this.sizes = sizes; } protected void putMapIndex(ImgFileWriter writer, int mapIndex) { putN(writer, sizes.getMapSize(), mapIndex); } protected void putStringOffset(ImgFileWriter writer, int strOff) { putN(writer, sizes.getStrOffSize(), strOff); } protected void putN(ImgFileWriter writer, int n, int value) { switch (n) { case 1: writer.put((byte) value); break; case 2: writer.putChar((char) value); break; case 3: writer.put3(value); break; case 4: writer.putInt(value); break; default: // Don't write anything. assert false; break; } } protected static int numberToPointerSize(int n) { if (n > 0xffffff) return 4; else if (n > 0xffff) return 3; else if (n > 0xff) return 2; else return 1; } /** * The number of records in this section. * @return The number of items in the section. */ public final int getNumberOfItems() { assert sizeValid; if (released) return nItems; else return numberOfItems(); } /** * Method to be implemented by subclasses to return the number of items in the section. * This will only be valid after the section is completely finished etc. * @return The number of items in the section. */ protected abstract int numberOfItems(); /** * Get the size of an integer that is sufficient to store a record number * from this section. If the pointer has a flag(s) then this must be * taken into account too. * @return A number between 1 and 4 giving the number of bytes required * to store the largest record number in this section. */ public int getSizeForRecord() { return numberToPointerSize(getNumberOfItems()); } /** * Called before the section is written and before the actual size of the section * is required. * * Calling it more than once is ok. * * The actual work is implemented in the subclass via the {@link #preWriteImpl()} method. */ public final void preWrite() { if (!sizeValid) preWriteImpl(); sizeValid = true; } /** * Prepare the final list of items to be written. * Used to de-duplicate or remove invalid entries from the raw data that was * saved. * * In particular after this call the number of items must not change. */ protected void preWriteImpl() { } public final void release() { nItems = numberOfItems(); releaseMemory(); released = true; } protected void releaseMemory() { throw new UnsupportedOperationException(); } /** * Provides the pointer sizes required to hold record of offset values * in the various sections. */ static class PointerSizes { private final MdrSection[] sections; public PointerSizes(MdrSection[] sections) { this.sections = sections; } public int getMapSize() { return sections[1].getSizeForRecord(); } public int getCitySize() { return sections[5].getSizeForRecord(); } /** * Get the number of bytes required to represent a city when there is * one bit reserved for a flag. * There is a minimum size of 2. * @return Number of bytes to represent a city record number and a * one bit flag. */ public int getCitySizeFlagged() { return Math.max(2, numberToPointerSize(sections[5].getNumberOfItems() << 1)); } public int getCityFlag() { return flagForSize(getCitySizeFlagged()); } public int getStreetSize() { return sections[7].getSizeForRecord(); } public int getStreetSizeFlagged() { return numberToPointerSize(sections[7].getNumberOfItems() << 1); } public int getPoiSize() { return sections[11].getSizeForRecord(); } public int getZipSize() { //return Math.max(2, sections[6].getSizeForRecord()); return sections[6].getSizeForRecord(); } /** * The number of bytes required to represent a POI (mdr11) record number * and a flag bit. */ public int getPoiSizeFlagged() { return numberToPointerSize(sections[11].getNumberOfItems() << 1); } public int getPoiFlag() { return flagForSize(getPoiSizeFlagged()); } /** * Size of the pointer required to index a byte offset into mdr15 (strings). * There is a minimum of 3 for this value. * @return Pointer size required for the string offset value. */ public int getStrOffSize() { return Math.max(3, sections[15].getSizeForRecord()); } public int getMdr20Size() { return sections[20].getSizeForRecord(); } private int flagForSize(int size) { int flag; if (size == 1) flag = 0x80; else if (size == 2) flag = 0x8000; else if (size == 3) flag = 0x800000; else if (size == 4) flag = 0x80000000; else flag = 0; return flag; } public int getSize(int sect) { return sections[sect].getSizeForRecord(); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr28Record.java0000644000076400007640000000334511532723256023304 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * There is one of these for each region name (ie no repeats) there are indexes * to the first entry with the same name in various other sections. * * @author Steve Ratcliffe */ public class Mdr28Record extends ConfigBase { private int index; private String name; private int mdr21; private int mdr23; private int mdr27; private int strOffset; private Mdr14Record mdr14; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getMdr21() { return mdr21; } public void setMdr21(int mdr21) { if (this.mdr21 == 0) this.mdr21 = mdr21; } public int getMdr23() { return mdr23; } public void setMdr23(int mdr23) { if (this.mdr23 == 0) this.mdr23 = mdr23; } public int getMdr27() { return mdr27; } public void setMdr27(int mdr27) { if (this.mdr27 == 0) this.mdr27 = mdr27; } public int getStrOffset() { return strOffset; } public void setStrOffset(int strOffset) { this.strOffset = strOffset; } public Mdr14Record getMdr14() { return mdr14; } public void setMdr14(Mdr14Record mdr14) { this.mdr14 = mdr14; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MdrConfig.java0000644000076400007640000000447312467220204023115 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.io.File; import uk.me.parabola.imgfmt.app.srt.Sort; /** * Configuration for the MDR file. * Mostly used when creating a file as there are a number of different options * in the way that it is done. * * @author Steve Ratcliffe */ public class MdrConfig { //private static final int DEFAULT_HEADER_LEN = 286; private static final int DEFAULT_HEADER_LEN = 568; private boolean writable; private boolean forDevice; private int headerLen = DEFAULT_HEADER_LEN; private Sort sort; private File outputDir; private boolean splitName; /** * True if we are creating the file, rather than reading it. */ public boolean isWritable() { return writable; } public void setWritable(boolean writable) { this.writable = writable; } /** * The format that is used by the GPS devices is different to that used * by Map Source. This parameter says which to do. * @return True if we are creating the the more compact format required * for a device. */ public boolean isForDevice() { return forDevice; } public void setForDevice(boolean forDevice) { this.forDevice = forDevice; } /** * There are a number of different header lengths in existence. This * controls what sections can exist (and perhaps what must exist). * @return The header length. */ public int getHeaderLen() { return headerLen; } public void setHeaderLen(int headerLen) { this.headerLen = headerLen; } public Sort getSort() { return sort; } public void setSort(Sort sort) { this.sort = sort; } public File getOutputDir() { return outputDir; } public void setOutputDir(String outputDir) { if (outputDir != null) this.outputDir = new File(outputDir); } public void setSplitName(boolean splitName) { this.splitName = splitName; } public boolean isSplitName() { return splitName; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr12Record.java0000644000076400007640000000125112571251757023275 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * This is an index into mdr11 (pois). * @author Steve Ratcliffe */ public class Mdr12Record extends Mdr8Record { // This is exactly the same as mdr8 } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr10.java0000644000076400007640000000733012325011461022117 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * POI subtype with a reference to MDR11. * These are sorted into groups based on the type, and contain the * subtype. * * The mdr9 section contains an index to this section based on the * the type groups. * * @author Steve Ratcliffe */ public class Mdr10 extends MdrMapSection { // The maximum group number. Note that this is 1 based, not 0 based. private static final int MAX_GROUP_NUMBER = 9; @SuppressWarnings({"unchecked"}) private List[] poiTypes = new ArrayList[MAX_GROUP_NUMBER+1]; private int numberOfPois; public Mdr10(MdrConfig config) { setConfig(config); for (int i = 1; i <= MAX_GROUP_NUMBER; i++) { poiTypes[i] = new ArrayList<>(); } } public void addPoiType(Mdr11Record poi) { Mdr10Record t = new Mdr10Record(); int type = poi.getType(); t.setSubtype(MdrUtils.getSubtypeOrTypeFromFullType(type)); t.setMdr11ref(poi); int group = MdrUtils.getGroupForPoi(type); if (group == 0) return; poiTypes[group].add(t); } public void writeSectData(ImgFileWriter writer) { int count = 0; for (List poiGroup : poiTypes) { if (poiGroup == null) continue; Collections.sort(poiGroup); String lastName = null; int lastSub = -1; for (Mdr10Record t : poiGroup) { count++; Mdr11Record mdr11ref = t.getMdr11ref(); addIndexPointer(mdr11ref.getMapIndex(), count); writer.put((byte) t.getSubtype()); int offset = mdr11ref.getRecordNumber(); // Top bit actually represents a non-repeated name. ie if // the bit is not set, then the name is the same as the previous // record. String name = mdr11ref.getName(); boolean isNew = !(name.equals(lastName) && (t.getSubtype() == lastSub)); putPoiIndex(writer, offset, isNew); lastName = name; lastSub = t.getSubtype(); } } } /** * Get a list of the group sizes along with the group index number. * @return A map that is guaranteed to iterate in the correct order for * writing mdr9. The key is the group number and the value is the * number of entries in that group. */ public Map getGroupSizes() { Map m = new LinkedHashMap<>(); for (int i = 1; i < MAX_GROUP_NUMBER; i++) { List poiGroup = poiTypes[i]; if (!poiGroup.isEmpty()) m.put(i, poiGroup.size()); } return m; } /** * This does not have a record size. * @return Always zero to indicate that there is not a record size. */ public int getItemSize() { return 0; } protected int numberOfItems() { return numberOfPois; } public void setNumberOfPois(int numberOfPois) { this.numberOfPois = numberOfPois; } protected void releaseMemory() { poiTypes = null; } public int getExtraValue() { // Nothing to do here return 0; } /** * Nothing to do for this section. * * Although this section has a subsection by map index in mdr1, its record does not contain the * map index and so nothing needs to be re-written here. The map index is contained in its mdr11ref. */ public void relabelMaps(Mdr1 maps) { } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr18Record.java0000644000076400007640000000155111701624112023265 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * An index entry into mdr 19. * * @author Steve Ratcliffe */ public class Mdr18Record { private int type; private int record; public int getType() { return type; } public void setType(int type) { this.type = type; } public int getRecord() { return record; } public void setRecord(int record) { this.record = record; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/RecordBase.java0000644000076400007640000000156712325011461023253 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * Records that belong to a particular map should extend this class. * Otherwise they can extend ConfigBase. * * @author Steve Ratcliffe */ public abstract class RecordBase { private short mapIndex; public int getMapIndex() { return mapIndex; } public void setMapIndex(int mapIndex) { assert mapIndex > 0; this.mapIndex = (short) mapIndex; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr14.java0000644000076400007640000000324411642347335022140 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * The countries that occur in each map. * * @author Steve Ratcliffe */ public class Mdr14 extends MdrSection implements HasHeaderFlags { private final List countries = new ArrayList(); public Mdr14(MdrConfig config) { setConfig(config); } public void writeSectData(ImgFileWriter writer) { Collections.sort(countries); for (Mdr14Record country : countries) { putMapIndex(writer, country.getMapIndex()); writer.putChar((char) country.getCountryIndex()); putStringOffset(writer, country.getStrOff()); } } public void addCountry(Mdr14Record country) { countries.add(country); } public int getItemSize() { PointerSizes sizes = getSizes(); return sizes.getMapSize() + 2 + sizes.getStrOffSize(); } /** * The number of records in this section. * @return The number of items in the section. */ protected int numberOfItems() { return countries.size(); } public int getExtraValue() { return 0x00; } public List getCountries() { return countries; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr1Record.java0000644000076400007640000000216211701624112023174 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * @author Steve Ratcliffe */ public class Mdr1Record extends RecordBase { private final int mapNumber; private Mdr1MapIndex mdrMapIndex; private int indexOffset; public Mdr1Record(int mapNumber, MdrConfig config) { this.mapNumber = mapNumber; } public int getMapNumber() { return mapNumber; } public Mdr1MapIndex getMdrMapIndex() { return mdrMapIndex; } public void setMdrMapIndex(Mdr1MapIndex mdrMapIndex) { this.mdrMapIndex = mdrMapIndex; } public int getIndexOffset() { return indexOffset; } public void setIndexOffset(int indexOffset) { this.indexOffset = indexOffset; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr8.java0000644000076400007640000000447611642347335022073 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * This section is a simple index into the streets section (mdr7). * * @author Steve Ratcliffe */ public class Mdr8 extends MdrSection implements HasHeaderFlags { private static final int STRING_WIDTH = 4; private List index = new ArrayList(); public Mdr8(MdrConfig config) { setConfig(config); } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int size = associatedSize(); Charset charset = getConfig().getSort().getCharset(); for (Mdr8Record s : index) { writer.put(s.getPrefix().getBytes(charset), 0, STRING_WIDTH); putN(writer, size, s.getRecordNumber()); } } protected int associatedSize() { return getSizes().getStreetSize(); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return index.size(); } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { return STRING_WIDTH + associatedSize(); } public void setIndex(List index) { this.index = index; } /** * The header flags for the section. * Possible values are not known. * * @return The correct value based on the contents of the section. Zero if * nothing needs to be done. */ public int getExtraValue() { // this value is likely to depend on the size of the max record number. return (STRING_WIDTH << 8) + 0x0a; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/ConfigBase.java0000644000076400007640000000153311332007627023241 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * A base class that provides access to the MDR configuration data. */ public abstract class ConfigBase { private MdrConfig config; protected boolean isForDevice() { return config.isForDevice(); } public void setConfig(MdrConfig config) { this.config = config; } protected MdrConfig getConfig() { return config; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr5Record.java0000644000076400007640000001057212306565326023221 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.text.Collator; /** * Holds information about a city that will make its way into mdr 5. * This class is used in several places as the information has to be gathered * from the cities section of LBL and the points in RGN. * * @author Steve Ratcliffe */ public class Mdr5Record extends RecordBase implements NamedRecord { /** The city index within its own map */ private int cityIndex; /** The index across all maps */ private int globalCityIndex; private int regionIndex; private int lblOffset; private int stringOffset; private String name; private Mdr13Record region; private Mdr14Record country; private int[] mdr20; private int mdr20Index; public int getCityIndex() { return cityIndex; } public void setCityIndex(int cityIndex) { this.cityIndex = cityIndex; } public int getGlobalCityIndex() { return globalCityIndex; } public void setGlobalCityIndex(int globalCityIndex) { this.globalCityIndex = globalCityIndex; } public int getRegionIndex() { return regionIndex; } public void setRegionIndex(int regionIndex) { this.regionIndex = regionIndex; } public int getLblOffset() { return lblOffset; } public void setLblOffset(int lblOffset) { this.lblOffset = lblOffset; } public int getStringOffset() { return stringOffset; } public void setStringOffset(int stringOffset) { this.stringOffset = stringOffset; } public void setName(String name) { this.name = name; } public String getName() { return name; } public Mdr13Record getMdrRegion() { return region; } public void setMdrRegion(Mdr13Record region) { this.region = region; } public Mdr14Record getMdrCountry() { return country; } public void setMdrCountry(Mdr14Record country) { this.country = country; } /** * Every mdr5 record contains the same array of values. It is only * allowed to access the one at the index globalCityIndex. Since * the array is shared, every record with the same global city index * knows the correct mdr20 value, regardless of where it was set. * * @param mdr20 An array large enough to hold all the cities (one based index). * This must be the same array for all mdr5records (in the same map set). */ public void setMdr20set(int[] mdr20) { this.mdr20 = mdr20; } /** * Set the index into the mdr20 array that we use to get/set the value. * @see #setMdr20set(int[]) */ public void setMdr20Index(int mdr20Index) { this.mdr20Index = mdr20Index; } public int getMdr20() { return mdr20[mdr20Index]; } public void setMdr20(int n) { int prev = mdr20[mdr20Index]; assert prev == 0 || prev == n : "mdr20 value changed f=" + prev + " t=" + n + " count=" + mdr20Index; mdr20[mdr20Index] = n; } /** * Is this the same city, by the rules segregating the cities in mdr5 and 20. * @return True if in the same tile and has the same name for city/region/country. */ public boolean isSameByMapAndName(Collator collator, Mdr5Record other) { if (other == null) return false; return getMapIndex() == other.getMapIndex() && isSameByName(collator, other); } /** * Same city by the name of the city/region/country combination. * * @param collator Used to sort names. * @param other The other city to compare with. * @return True if is the same city, maybe in a different tile. */ public boolean isSameByName(Collator collator, Mdr5Record other) { if (other == null) return false; return collator.compare(getName(), other.getName()) == 0 && collator.compare(getRegionName(), other.getRegionName()) == 0 && collator.compare(getCountryName(), other.getCountryName()) == 0; } public String toString() { return String.format("%d: %s r=%s c=%s", globalCityIndex, name, getRegionName(), country.getName()); } public String getRegionName() { if (region == null) return ""; else return region.getName(); } public String getCountryName() { return country.getName(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr12.java0000644000076400007640000000150011532723256022125 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; /** * This is the index for the POI section (Mdr11). * It works exactly the same way as Mdr8 as far as we know. * * @author Steve Ratcliffe */ public class Mdr12 extends Mdr8 { public Mdr12(MdrConfig config) { super(config); } protected int associatedSize() { return getSizes().getPoiSize(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MdrMapSection.java0000644000076400007640000000610112325011461023734 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Super class of all sections that contain items that belong to a particular * map. * * @author Steve Ratcliffe */ public abstract class MdrMapSection extends MdrSection implements HasHeaderFlags { private Mdr1 index; public void setMapIndex(Mdr1 index) { this.index = index; } /** * This is called before the sections are written out, but after all the * data is read into them. * @param sectionNumber The one-based section number. */ public final void initIndex(int sectionNumber) { // Set the size required to store the record numbers for this section. // There are no flags or minimums required here, unlike in setPointerSize() // which does a similar thing. int n = getNumberOfItems(); index.setPointerSize(sectionNumber, numberToPointerSize(n)); } /** * Add a pointer to the reverse index for this section. * @param recordNumber A record number in this section, belonging to the * given map. */ public void addIndexPointer(int mapNumber, int recordNumber) { if (!isForDevice()) index.addPointer(mapNumber, recordNumber); } protected void putCityIndex(ImgFileWriter writer, int cityIndex, boolean isNew) { int flag = (isNew && cityIndex > 0)? getSizes().getCityFlag(): 0; putN(writer, getSizes().getCitySizeFlagged(), cityIndex | flag); } protected void putRegionIndex(ImgFileWriter writer, int region) { // This is only called when putCityIndex might also be called and so has to be // the same size (probably ;) putN(writer, getSizes().getCitySizeFlagged(), region); } protected void putPoiIndex(ImgFileWriter writer, int poiIndex, boolean isNew) { int flag = isNew? getSizes().getPoiFlag(): 0; putN(writer, getSizes().getPoiSizeFlagged(), poiIndex | flag); } protected boolean hasFlag(int val) { return (getExtraValue() & val) != 0; } public abstract void relabelMaps(Mdr1 maps); /** * Relabel every map-index in the given set of records. * * The maps must be in sorted order, but because of the incremental way that we build the * index, this isn't known until the end. So we get rewrite the mapIndex from the * initial to the final ordering. * * @param maps The final ordering of the maps. * @param records The set of records to be relabeled. * @param The type of the record. Must be a subclass of RecordBase. */ protected void relabel(Mdr1 maps, List records) { for (T r : records) { int n = r.getMapIndex(); int newIndex = maps.sortedMapIndex(n); r.setMapIndex(newIndex); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr26.java0000644000076400007640000000520711701624112022127 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * An index into mdr28 (region names), sorted by country name. * * @author Steve Ratcliffe */ public class Mdr26 extends MdrSection { private final List index = new ArrayList(); public Mdr26(MdrConfig config) { setConfig(config); } public void sortMdr28(List in) { Sort sort = getConfig().getSort(); List> sortList = new ArrayList>(); int record = 0; for (Mdr28Record mdr28 : in) { SortKey key = sort.createSortKey(mdr28, mdr28.getMdr14().getName(), ++record); sortList.add(key); } Collections.sort(sortList); addToIndex(sortList); } private void addToIndex(List> sortList) { String lastName = null; int record26 = 0; for (SortKey key : sortList) { record26++; Mdr28Record mdr28 = key.getObject(); Mdr14Record mdr14 = mdr28.getMdr14(); assert mdr14 != null; // For each new name, set up the mdr29 record for it. String name = mdr14.getName(); if (!name.equals(lastName)) { Mdr29Record mdr29 = mdr14.getMdr29(); mdr29.setMdr26(record26); lastName = name; } index.add(mdr28); } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int size = getSizes().getSize(28); for (Mdr28Record record : index) { putN(writer, size, record.getIndex()); } } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { return getSizes().getSize(28); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return index.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr22.java0000644000076400007640000000546012467220204022130 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Index of streets by country. * * There is no pointer from the country section into this like there is with * cities. * * @author Steve Ratcliffe */ public class Mdr22 extends Mdr2x { public Mdr22(MdrConfig config) { setConfig(config); } /** * We need to sort the streets by the name of the country. Within a city * group the streets are ordered by their own index. * * Also have to set the record number of the first record in this section * on the city. * * @param inStreets The list of streets from mdr7. */ public void buildFromStreets(List inStreets) { Sort sort = getConfig().getSort(); List> keys = new ArrayList<>(); Map cache = new HashMap<>(); for (Mdr7Record s : inStreets) { Mdr5Record city = s.getCity(); if (city == null) continue; String name = city.getMdrCountry().getName(); assert name != null; // We are sorting the streets, but we are sorting primarily on the // country name associated with the street. // For memory use, we re-use country name part of the key. keys.add(sort.createSortKey(s, name, s.getIndex(), cache)); } Collections.sort(keys); int record = 0; String lastName = null; int lastMapid = 0; for (SortKey key : keys) { Mdr7Record street = key.getObject(); String name = street.getName(); int mapid = street.getMapIndex(); if (mapid != lastMapid || !name.equals(lastName)) { record++; streets.add(street); // Include in the mdr29 index if we have one for this record. Mdr14Record mdrCountry = street.getCity().getMdrCountry(); if (mdrCountry != null) { Mdr29Record mdr29 = mdrCountry.getMdr29(); mdr29.setMdr22(record); } lastMapid = mapid; lastName = name; } } } protected boolean sameGroup(Mdr7Record street1, Mdr7Record street2) { return true; } public List getStreets() { return Collections.unmodifiableList(streets); } /** * Unknown flag */ public int getExtraValue() { if (isForDevice()) return 0x600e; else return 0x11000; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr5.java0000644000076400007640000001322412325011461022042 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.MultiSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Section containing cities. * * We need: map number, city index in map, offset in LBL, flags * and pointer into MDR 15 for the string name. * * @author Steve Ratcliffe */ public class Mdr5 extends MdrMapSection { private List allCities = new ArrayList<>(); private List cities = new ArrayList<>(); private int maxCityIndex; private int localCitySize; public Mdr5(MdrConfig config) { setConfig(config); } public void addCity(Mdr5Record record) { assert record.getMapIndex() != 0; allCities.add(record); if (record.getCityIndex() > maxCityIndex) maxCityIndex = record.getCityIndex(); } /** * Called after all cities to sort and number them. */ public void preWriteImpl() { localCitySize = numberToPointerSize(maxCityIndex + 1); List> sortKeys = new ArrayList<>(allCities.size()); Sort sort = getConfig().getSort(); for (Mdr5Record m : allCities) { if (m.getName() == null) continue; // Sort by city name, region name, country name and map index. SortKey sortKey = sort.createSortKey(m, m.getName()); SortKey regionKey = sort.createSortKey(null, m.getRegionName()); SortKey countryKey = sort.createSortKey(null, m.getCountryName(), m.getMapIndex()); sortKey = new MultiSortKey<>(sortKey, regionKey, countryKey); sortKeys.add(sortKey); } Collections.sort(sortKeys); Collator collator = getConfig().getSort().getCollator(); int count = 0; Mdr5Record lastCity = null; // We need a common area to save the mdr20 values, since there can be multiple // city records with the same global city index int[] mdr20s = new int[sortKeys.size()+1]; int mdr20count = 0; for (SortKey key : sortKeys) { Mdr5Record c = key.getObject(); c.setMdr20set(mdr20s); if (!c.isSameByName(collator, lastCity)) mdr20count++; c.setMdr20Index(mdr20count); if (c.isSameByMapAndName(collator, lastCity)) { c.setGlobalCityIndex(count); } else { count++; c.setGlobalCityIndex(count); cities.add(c); lastCity = c; } } } public void writeSectData(ImgFileWriter writer) { int size20 = getSizes().getMdr20Size(); Mdr5Record lastCity = null; boolean hasString = hasFlag(0x8); boolean hasRegion = hasFlag(0x4); Collator collator = getConfig().getSort().getCollator(); for (Mdr5Record city : cities) { int gci = city.getGlobalCityIndex(); addIndexPointer(city.getMapIndex(), gci); // Work out if the name is the same as the previous one and set // the flag if so. int flag = 0; int mapIndex = city.getMapIndex(); int region = city.getRegionIndex(); // Set the no-repeat flag if the name/region is different if (!city.isSameByName(collator, lastCity)) { flag = 0x800000; lastCity = city; } // Write out the record putMapIndex(writer, mapIndex); putLocalCityIndex(writer, city.getCityIndex()); writer.put3(flag | city.getLblOffset()); if (hasRegion) writer.putChar((char) region); if (hasString) putStringOffset(writer, city.getStringOffset()); putN(writer, size20, city.getMdr20()); } } /** * Put the map city index. This is the index within the individual map * and not the global city index used in mdr11. */ private void putLocalCityIndex(ImgFileWriter writer, int cityIndex) { if (localCitySize == 2) // 3 probably not possible in actual maps. writer.putChar((char) cityIndex); else writer.put((byte) cityIndex); } /** * Base size of 8, plus enough bytes to represent the map number * and the city number. * @return The size of a record in this section. */ public int getItemSize() { PointerSizes sizes = getSizes(); int size = sizes.getMapSize() + localCitySize + 3 + sizes.getMdr20Size(); if (hasFlag(0x4)) size += 2; if (hasFlag(0x8)) size += sizes.getStrOffSize(); return size; } protected int numberOfItems() { return cities.size(); } /** * Known structure: * bits 0-1: size of local city index - 1 (all values appear to work) * bit 3: has region * bit 4: has string * @return The value to be placed in the header. */ public int getExtraValue() { int val = (localCitySize - 1); // String offset is only included for a mapsource index. if (isForDevice()) { val |= 0x40; // not known, probably refers to mdr17. } else { val |= 0x04; // region val |= 0x08; // string } val |= 0x10; val |= 0x100; // mdr20 present return val; } protected void releaseMemory() { allCities = null; cities = null; } /** * Get a list of all the cities, including duplicate named ones. * @return All cities. */ public List getCities() { return Collections.unmodifiableList(allCities); } public List getSortedCities() { return Collections.unmodifiableList(cities); } public void relabelMaps(Mdr1 maps) { relabel(maps, cities); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr14Record.java0000644000076400007640000000341511532723256023275 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * A country record. * @author Steve Ratcliffe */ public class Mdr14Record extends RecordBase implements Comparable, NamedRecord { private int countryIndex; private int lblOffset; private int strOff; private String name; private Mdr29Record mdr29; /** * Sort by map id and then country id like for regions. We don't have * any evidence that this is necessary, but it would be surprising if * it wasn't. */ public int compareTo(Mdr14Record o) { int v1 = (getMapIndex()<<16) + countryIndex; int v2 = (o.getMapIndex()<<16) + o.countryIndex; if (v1 < v2) return -1; else if (v1 > v2) return 1; else return 0; } public int getCountryIndex() { return countryIndex; } public void setCountryIndex(int countryIndex) { this.countryIndex = countryIndex; } public int getLblOffset() { return lblOffset; } public void setLblOffset(int lblOffset) { this.lblOffset = lblOffset; } public int getStrOff() { return strOff; } public void setStrOff(int strOff) { this.strOff = strOff; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Mdr29Record getMdr29() { return mdr29; } public void setMdr29(Mdr29Record mdr29) { this.mdr29 = mdr29; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr11Record.java0000644000076400007640000000415611532723256023275 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * The details required to write a POI record to mdr 11. * @author Steve Ratcliffe */ public class Mdr11Record extends RecordBase implements NamedRecord { private int pointIndex; private int subdiv; private int lblOffset; private int strOffset; private int recordNumber; private String name; private Mdr5Record city; private boolean isCity; private int type; public boolean isCity() { return isCity; } public void setIsCity(boolean isCity) { this.isCity = isCity; } public int getPointIndex() { return pointIndex; } public void setPointIndex(int pointIndex) { this.pointIndex = pointIndex; } public int getSubdiv() { return subdiv; } public void setSubdiv(int subdiv) { this.subdiv = subdiv; } public int getLblOffset() { return lblOffset; } public void setLblOffset(int lblOffset) { this.lblOffset = lblOffset; } public int getCityIndex() { return city == null ? 0 : city.getGlobalCityIndex(); } public int getRegionIndex() { return city == null ? 0 : city.getRegionIndex(); } public int getStrOffset() { return strOffset; } public void setStrOffset(int strOffset) { this.strOffset = strOffset; } public int getRecordNumber() { return recordNumber; } public void setRecordNumber(int recordNumber) { this.recordNumber = recordNumber; } public String getName() { assert name!=null; return name; } public void setName(String name) { assert name!=null; this.name = name; } public void setCity(Mdr5Record city) { this.city = city; } public void setType(int type) { this.type = type; } public int getType() { return type; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr1SubHeader.java0000644000076400007640000000423411532723256023635 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; /** * A header for the map index, pointed to from MDR 1. * There is one of these for each map. * * @author Steve Ratcliffe */ public class Mdr1SubHeader { // The number of subsections, this starts at 1. Larger numbers are possible // for larger MDR header sizes. private static final int MAX_SECTION = 9; // Each sub header requires 8 bytes apart from sub2 which only needs 4. private static final int HEADER_SIZE = 70; private final Section[] sections = new Section[MAX_SECTION+1]; public Mdr1SubHeader() { // Initialise the sections. for (int n = 1; n <= MAX_SECTION; n++) { Section prev = sections[n - 1]; sections[n] = new Section(prev); } sections[1].setPosition(HEADER_SIZE); } protected void writeFileHeader(ImgFileWriter writer) { writer.putChar((char) HEADER_SIZE); for (int n = 1; n <= MAX_SECTION; n++) { Section section = sections[n]; // The second subsection does not have a length, because it always // has the same length as subsection 1. if (n == 2) writer.putInt(section.getPosition()); else { //section.writeSectionInfo(writer); writer.putInt(section.getPosition()); int size = section.getSize(); if (size == 0) writer.putInt(0); else writer.putInt(size / section.getItemSize()); } } } public void setEndSubsection(int sub, int pos) { sections[sub].setSize(pos - sections[sub].getPosition()); } public long getHeaderLen() { return HEADER_SIZE; } public void setItemSize(int sectionNumber, int itemSize) { sections[sectionNumber].setItemSize((char) itemSize); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr10Record.java0000644000076400007640000000267712325011461023267 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * An index into mdr11. * There is a primary ordering on the type and a secondary ordering on the * record number of the mdr11 record. The type is not actually stored * in this section, you use mdr9 to divide up this section into groups of * types. * * This section contains the subtype of each point. * * @author Steve Ratcliffe */ public class Mdr10Record implements Comparable { private int subtype; private Mdr11Record mdr11ref; public int compareTo(Mdr10Record o) { if (mdr11ref.getRecordNumber() == o.mdr11ref.getRecordNumber()) return 0; else if (mdr11ref.getRecordNumber() < o.mdr11ref.getRecordNumber()) return -1; else return 1; } public Mdr11Record getMdr11ref() { return mdr11ref; } public void setMdr11ref(Mdr11Record mdr11ref) { this.mdr11ref = mdr11ref; } public int getSubtype() { return subtype; } public void setSubtype(int subtype) { this.subtype = subtype; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr4.java0000644000076400007640000000350211642347335022054 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * POI types. A simple list of the types that are used? * If you have this section, then the ability to select POI categories * goes away. * * @author Steve Ratcliffe */ public class Mdr4 extends MdrSection implements HasHeaderFlags { private final Set poiTypes = new HashSet(); public Mdr4(MdrConfig config) { setConfig(config); } public void writeSectData(ImgFileWriter writer) { List list = new ArrayList(poiTypes); Collections.sort(list); for (Mdr4Record r : list) { writer.put((byte) r.getType()); writer.put((byte) r.getUnknown()); writer.put((byte) r.getSubtype()); } } public int getItemSize() { return 3; } public void addType(int type) { Mdr4Record r = new Mdr4Record(); if (type <= 0xff) r.setType(type); else { r.setType((type >> 8) & 0xff); r.setSubtype(type & 0xff); } r.setUnknown(0); poiTypes.add(r); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return poiTypes.size(); } public int getExtraValue() { return 0x00; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr19.java0000644000076400007640000000657111770652007022150 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * POIs ordered by type. Section 18 is the index into this. * * @author Steve Ratcliffe */ public class Mdr19 extends MdrSection implements HasHeaderFlags { private List pois; private final List poiTypes = new ArrayList(); public Mdr19(MdrConfig config) { setConfig(config); } /** * Sort the pois by type. */ public void preWriteImpl() { Collections.sort(pois, new Comparator() { public int compare(Mdr11Record o1, Mdr11Record o2) { // For mainly historical reasons, we keep the element type in a number of different // formats. Need to normalise it before sorting. int t1 = MdrUtils.fullTypeToNaturalType(o1.getType()); int t2 = MdrUtils.fullTypeToNaturalType(o2.getType()); if (t1 == t2) return 0; else if (t1 < t2) return -1; else return 1; } }); } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int n = getSizes().getPoiSizeFlagged(); int flag = getSizes().getPoiFlag(); String lastName = null; int lastType = -1; int record = 1; for (Mdr11Record p : pois) { int index = p.getRecordNumber(); String name = p.getName(); if (!name.equals(lastName)) { index |= flag; lastName = name; } putN(writer, n, index); int type = MdrUtils.fullTypeToNaturalType(p.getType()); if (type != lastType) { Mdr18Record mdr18 = new Mdr18Record(); mdr18.setType(type); mdr18.setRecord(record); poiTypes.add(mdr18); lastType = type; } record++; } Mdr18Record m18 = new Mdr18Record(); m18.setRecord(record); m18.setType(~0); poiTypes.add(m18); } /** * Release the copy of the pois. The other index is small and not worth * worrying about. */ protected void releaseMemory() { pois = null; } /** * Records in this section are record numbers into mdr 11 with a flag * so has to be large enough for a flagged index. * * @return The size of a record in this section. */ public int getItemSize() { return getSizes().getPoiSizeFlagged(); } /** * Method to be implemented by subclasses to return the number of items in the section. This will only be valid after * the section is completely finished etc. * * @return The number of items in the section. */ protected int numberOfItems() { return pois.size(); } /** * Not yet known. * * @return The correct value based on the contents of the section. Zero if nothing needs to be done. */ public int getExtraValue() { return getSizes().getSize(19) - 1; } public void setPois(List pois) { this.pois = pois; } public List getPoiTypes() { return poiTypes; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/package.html0000644000076400007640000000073111532723256022663 0ustar stevesteve

Overview

This package creates the MDR file.

There are many, many possible sections in the file. We will attempt to find the minimum number required to make something work.

Sections to be implemented first

Use a header of size 286. This gives us the possibility of the first 19 sections.

Initial sections to implement: 1, 4, 5, 6, 7, 11, 15. Of these 4 is not known and only some subsection of MDR 1 are also not known. mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr9.java0000644000076400007640000000356211642347335022067 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.LinkedHashMap; import java.util.Map; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * An index into mdr10. There is a single byte group number followed by * the first record in mdr10 that belongs to that group. * * @author Steve Ratcliffe */ public class Mdr9 extends MdrSection implements HasHeaderFlags { private final Map index = new LinkedHashMap(); public Mdr9(MdrConfig config) { setConfig(config); } public void writeSectData(ImgFileWriter writer) { int poiSize = getSizes().getPoiSize(); for (Map.Entry ent : index.entrySet()) { int group = ent.getKey(); writer.put((byte) group); putN(writer, poiSize, ent.getValue()); } } /** * The item size is one byte for the group and then enough bytes for the * index into mdr10. * @return Just return 4 for now. */ public int getItemSize() { return 1 + getSizes().getPoiSize(); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return index.size(); } public void setGroups(Map groupSizes) { int offset = 1; for (Map.Entry ent : groupSizes.entrySet()) { index.put(ent.getKey(), offset); offset += ent.getValue(); } } public int getExtraValue() { return 0x00; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr2x.java0000644000076400007640000000665312467220204022243 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Common code for 20, 21, 22 which are all lists of streets ordered in * different ways. * * @author Steve Ratcliffe */ public abstract class Mdr2x extends MdrMapSection implements HasHeaderFlags { protected List streets = new ArrayList<>(); /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { String lastName = null; Mdr7Record prev = null; int size = getSizes().getStreetSizeFlagged(); boolean hasLabel = hasFlag(0x2); String lastPartial = null; int recordNumber = 0; for (Mdr7Record street : streets) { assert street.getMapIndex() == street.getCity().getMapIndex() : street.getMapIndex() + "/" + street.getCity().getMapIndex(); addIndexPointer(street.getMapIndex(), ++recordNumber); int index = street.getIndex(); String name = street.getName(); int repeat = 1; if (name.equals(lastName) && sameGroup(street, prev)) repeat = 0; if (hasLabel) { putMapIndex(writer, street.getMapIndex()); int offset = street.getLabelOffset(); if (repeat != 0) offset |= 0x800000; int trailing = 0; String partialName = street.getPartialName(); if (!partialName.equals(lastPartial)) { trailing |= 1; offset |= 0x800000; } writer.put3(offset); writer.put(street.getOutNameOffset()); writer.put((byte) trailing); lastPartial = partialName; } else putN(writer, size, (index << 1) | repeat); lastName = name; prev = street; } } /** * The size of a record in the section. * * For these sections there is one field that is an index into the * streets with an extra bit for a flag. * * In the device configuration, then there is a label and a flag, just like * for mdr7. */ public int getItemSize() { int size; if (isForDevice()) { // map-index, label, name-offset, 1byte flag size = getSizes().getMapSize() + 3 + 1 + 1; } else { size = getSizes().getStreetSizeFlagged(); } return size; } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return streets.size(); } protected void releaseMemory() { streets = null; } /** * These sections are divided into groups based on city, region or country. This routine is * implemented to return true if the two streets are in the same group. * * It is not clear if this is needed for region or country. * @param street1 The first street. * @param street2 The street to compare against. * @return True if the streets are in the same group (city, region etc). */ protected abstract boolean sameGroup(Mdr7Record street1, Mdr7Record street2); public void relabelMaps(Mdr1 maps) { // Nothing to do, since all streets are re-labeled in their own section. } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr24.java0000644000076400007640000000531711642347335022144 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Countries sorted by name. Same number of entries as 14. * * @author Steve Ratcliffe */ public class Mdr24 extends MdrSection { private final List countries = new ArrayList(); public Mdr24(MdrConfig config) { setConfig(config); } /** * Sort the countries by name. Duplicates are kept. * @param list The full list of countries. */ public void sortCountries(List list) { Sort sort = getConfig().getSort(); List> keys = MdrUtils.sortList(sort, list); Collections.sort(keys); String lastName = null; int lastMapIndex = 0; int record = 0; for (SortKey key : keys) { Mdr14Record c = key.getObject(); // If this is a new name, then we prepare a mdr29 record for it. String name = c.getName(); if (lastMapIndex != c.getMapIndex() || !name.equals(lastName)) { record++; c.getMdr29().setMdr24(record); countries.add(c); lastName = name; lastMapIndex = c.getMapIndex(); } } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { String lastName = null; for (Mdr14Record c : countries) { putMapIndex(writer, c.getMapIndex()); int flag = 0; String name = c.getName(); if (!name.equals(lastName)) { flag = 0x800000; lastName = name; } writer.putChar((char) c.getCountryIndex()); writer.put3(c.getLblOffset() | flag); } } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { PointerSizes sizes = getSizes(); return sizes.getMapSize() + 2 + 3; } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return countries.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr1.java0000644000076400007640000001071112325011461022034 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * The section MDR 1 contains a list of maps and for each map * an offset to a reverse index for that map. * * The reverse index consists of a number of sections, that I call sub-sections * here. The sub-sections are all lists of record numbers in other sections * in the MDR that contain records belonging to more than one map. * * Using the index you could extract records that belong to an individual map * from other MDR sections without having to go through them all and check * which map they belong to. * * The subsections are as follows: * * sub1 points into MDR 11 (POIs) * sub2 points into MDR 10 (POI types) * sub3 points into MDR 7 (street names) * sub4 points into MDR 5 (cities) * sub5 points into MDR 6 (zips) * sub6 points into MDR 20 * sub7 points into MDR 21 * sub8 points into MDR 22 * * @author Steve Ratcliffe */ public class Mdr1 extends MdrSection implements HasHeaderFlags { private final List maps = new ArrayList<>(); private int[] mapping; public Mdr1(MdrConfig config) { setConfig(config); } /** * Add a map. Create an MDR1 record for it and also allocate its reverse * index if this is not for a device. * @param mapNumber The map index number. * @param index The map index. */ public void addMap(int mapNumber, int index) { assert index > 0; Mdr1Record rec = new Mdr1Record(mapNumber, getConfig()); rec.setMapIndex(index); maps.add(rec); if (!isForDevice()) { Mdr1MapIndex mapIndex = new Mdr1MapIndex(); rec.setMdrMapIndex(mapIndex); } } /** * The maps must be sorted in numerical order. */ public void finish() { Collections.sort(maps, new Comparator() { public int compare(Mdr1Record o1, Mdr1Record o2) { if (o1.getMapNumber() == o2.getMapNumber()) return 0; else if (o1.getMapNumber() < o2.getMapNumber()) return -1; else return 1; } }); // Used to renumber all the existing (pre-sorted) map index numbers. mapping = new int[maps.size()]; int count = 1; for (Mdr1Record r : maps) { mapping[r.getMapIndex()-1] = count; count++; } } public void writeSubSections(ImgFileWriter writer) { if (isForDevice()) return; for (Mdr1Record rec : maps) { rec.setIndexOffset(writer.position()); Mdr1MapIndex mapIndex = rec.getMdrMapIndex(); mapIndex.writeSubSection(writer); } } /** * This is written right at the end after we know all the offsets in * the MDR 1 record. * @param writer The mdr 1 records are written out to this writer. */ public void writeSectData(ImgFileWriter writer) { boolean revIndex = (getExtraValue() & 1) != 0; for (Mdr1Record rec : maps) { writer.putInt(rec.getMapNumber()); if (revIndex) writer.putInt(rec.getIndexOffset()); } } public int getItemSize() { return isForDevice()? 4: 8; } public void setStartPosition(int sectionNumber) { if (isForDevice()) return; for (Mdr1Record mi : maps) mi.getMdrMapIndex().startSection(sectionNumber); } public void setEndPosition(int sectionNumber) { if (isForDevice()) return; for (Mdr1Record mi : maps) { mi.getMdrMapIndex().endSection(sectionNumber); } } public void setPointerSize(int sectionSize, int recordSize) { for (Mdr1Record mi : maps) { Mdr1MapIndex mapIndex = mi.getMdrMapIndex(); mapIndex.setPointerSize(sectionSize, recordSize); } } public void addPointer(int mapNumber, int recordNumber) { Mdr1MapIndex mi = maps.get(mapNumber - 1).getMdrMapIndex(); mi.addPointer(recordNumber); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return maps.size(); } public int getExtraValue() { int magic = 0; if (!isForDevice()) magic |= 1; return magic; } public int sortedMapIndex(int n) { return mapping[n-1]; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr6.java0000644000076400007640000000467612325011461022056 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.lbl.Zip; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Section containing zip codes. * * We need: map number, zip index in map, pointer into MDR 15 for the string name. * * @author WanMil */ public class Mdr6 extends MdrMapSection { private final List zips = new ArrayList<>(); public Mdr6(MdrConfig config) { setConfig(config); } public void addZip(int mapIndex, Zip zip, int strOff) { Mdr6Record record = new Mdr6Record(zip); record.setMapIndex(mapIndex); record.setStringOffset(strOff); zips.add(record); } public void writeSectData(ImgFileWriter writer) { int zipSize = getSizes().getZipSize(); List> sortKeys = MdrUtils.sortList(getConfig().getSort(), zips); boolean hasString = hasFlag(0x4); int record = 1; for (SortKey key : sortKeys) { Mdr6Record z = key.getObject(); addIndexPointer(z.getMapIndex(), record++); putMapIndex(writer, z.getMapIndex()); putN(writer, zipSize, z.getZipIndex()); if (hasString) putStringOffset(writer, z.getStringOffset()); } } /** * Enough bytes to represent the map number * and the zip index and the string offset. * @return The size of a record in this section. */ public int getItemSize() { PointerSizes sizes = getSizes(); int size = sizes.getMapSize() + sizes.getZipSize(); if (hasFlag(0x4)) size += sizes.getStrOffSize(); return size; } protected int numberOfItems() { return zips.size(); } /** * Known structure: * bits 0-1: size of local zip index - 1 (all values appear to work) * bits 2: if MDR 15 available * @return The value to be placed in the header. */ public int getExtraValue() { return ((getSizes().getZipSize()-1)&0x03) | (isForDevice() ? 0 : 0x04); } public void relabelMaps(Mdr1 maps) { relabel(maps, zips); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr7Record.java0000644000076400007640000000556712476306743023237 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; /** * Holds details of a single street. * @author Steve Ratcliffe */ public class Mdr7Record extends RecordBase implements NamedRecord { private int labelOffset; private int stringOffset; private String name; private int index; private Mdr5Record city; // For searching on partial names private byte nameOffset; // offset into the name where matching should start private byte outNameOffset; // offset into the encoded output name private byte prefixOffset; // offset after 0x1e prefix private byte suffixOffset; // offset just before 0x1f suffix public int getLabelOffset() { return labelOffset; } public void setLabelOffset(int labelOffset) { this.labelOffset = labelOffset; } public int getStringOffset() { return stringOffset; } public void setStringOffset(int stringOffset) { this.stringOffset = stringOffset; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void setIndex(int index) { this.index = index; } public int getIndex() { return index; } public void setCity(Mdr5Record city) { this.city = city; } public Mdr5Record getCity() { return city; } public int getNameOffset() { return nameOffset & 0xff; } public void setNameOffset(byte nameOffset) { this.nameOffset = nameOffset; } public byte getOutNameOffset() { return outNameOffset; } public void setOutNameOffset(byte outNameOffset) { this.outNameOffset = outNameOffset; } public void setPrefixOffset(byte prefixOffset) { this.prefixOffset = prefixOffset; } public void setSuffixOffset(byte suffixOffset) { this.suffixOffset = suffixOffset; } /** * Get the name starting at the given nameOffset. * * To avoid creating unnecessary objects, a check is made for an offset of zero * and the original name string is returned. * * @return A substring of name, starting at the nameOffset value. */ public String getPartialName() { if (nameOffset == 0 && prefixOffset == 0 && suffixOffset == 0) return name; else if ((suffixOffset & 0xff) > 0) return name.substring((nameOffset & 0xff) + (prefixOffset & 0xff), (suffixOffset & 0xff)); else return name.substring((nameOffset & 0xff) + (prefixOffset & 0xff)); } public String toString() { return name + " in " + city.getName(); } public String getInitialPart() { return name.substring(0, (nameOffset & 0xff)); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr1MapIndex.java0000644000076400007640000000464511532723256023506 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.nio.ByteBuffer; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Each map has one of these and it is used to provide a list of records * in the various sections that actually belong to this map. * * @author Steve Ratcliffe */ public class Mdr1MapIndex { private final Mdr1SubHeader subHeader = new Mdr1SubHeader(); private final BufferedImgFileWriter subWriter = new BufferedImgFileWriter(null); private int pointerSize; public Mdr1MapIndex() { // skip over where the header will be this.subWriter.position(subHeader.getHeaderLen()); } public void startSection(int n) { } public void endSection(int n) { int sn = sectionToSubsection(n); if (sn != 0) subHeader.setEndSubsection(sn, subWriter.position()); } public void addPointer(int recordNumber) { switch (pointerSize) { case 4: subWriter.putInt(recordNumber); break; case 3: subWriter.put3(recordNumber); break; case 2: subWriter.putChar((char) recordNumber); break; case 1: subWriter.put((byte) recordNumber); break; default: assert false; } } private int sectionToSubsection(int n) { int sn; switch (n) { case 11: sn = 1; break; case 10: sn = 2; break; case 7: sn = 3; break; case 5: sn = 4; break; case 6: sn = 5; break; case 20: sn = 6; break; case 21: sn = 7; break; case 22: sn = 8; break; default: sn = 0; break; } return sn; } public void writeSubSection(ImgFileWriter writer) { subHeader.writeFileHeader(writer); ByteBuffer buffer = subWriter.getBuffer(); byte[] bytes = buffer.array(); int hl = (int) subHeader.getHeaderLen(); writer.put(bytes, hl, buffer.position() - hl); } public void setPointerSize(int sectionNumber, int pointerSize) { this.pointerSize = pointerSize; int sn = sectionToSubsection(sectionNumber); if (sn != 0) subHeader.setItemSize(sn, pointerSize); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr25.java0000644000076400007640000000524211701624112022125 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * Cities sorted by country. * * @author Steve Ratcliffe */ public class Mdr25 extends MdrSection { private final List cities = new ArrayList(); public Mdr25(MdrConfig config) { setConfig(config); } /** * Cities are sorted by country and then by the mdr5 city record number. * @param list The complete list of cities from mdr5. */ public void sortCities(List list) { Sort sort = getConfig().getSort(); List> keys = new ArrayList>(); for (Mdr5Record c : list) { SortKey key = sort.createSortKey(c, c.getMdrCountry().getName(), c.getGlobalCityIndex()); keys.add(key); } Collections.sort(keys); String lastName = null; Mdr5Record lastCity = null; int record = 0; for (SortKey key : keys) { Mdr5Record city = key.getObject(); if (lastCity == null || (!city.getName().equals(lastCity.getName()) || !(city.getRegionName().equals(lastCity.getRegionName())))) { record++; // Record in the 29 index if there is one for this record Mdr14Record mdrCountry = city.getMdrCountry(); Mdr29Record mdr29 = mdrCountry.getMdr29(); String name = mdr29.getName(); assert mdrCountry.getName().equals(name); if (!name.equals(lastName)) { mdr29.setMdr25(record); lastName = name; } cities.add(city); lastCity = city; } } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { int size = getItemSize(); for (Mdr5Record city : cities) { putN(writer, size, city.getGlobalCityIndex()); } } /** * One field pointing to a city. Not flagged. */ public int getItemSize() { return getSizes().getCitySize(); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return cities.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr13.java0000644000076400007640000000326211642347335022137 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Holds all the regions for each map. * * @author Steve Ratcliffe */ public class Mdr13 extends MdrSection implements HasHeaderFlags { private final List regions = new ArrayList(); public Mdr13(MdrConfig config) { setConfig(config); } public void addRegion(Mdr13Record rec) { regions.add(rec); } public void writeSectData(ImgFileWriter writer) { Collections.sort(regions); for (Mdr13Record region : regions) { putMapIndex(writer, region.getMapIndex()); writer.putChar((char) region.getRegionIndex()); writer.putChar((char) region.getCountryIndex()); putStringOffset(writer, region.getStrOffset()); } } public int getItemSize() { return getSizes().getMapSize() + 4 + getSizes().getStrOffSize(); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return regions.size(); } @Override public int getExtraValue() { return 0x00; } public List getRegions() { return regions; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/Mdr28.java0000644000076400007640000000605711642347335022152 0ustar stevesteve/* * Copyright (C) 2011. * * 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.imgfmt.app.mdr; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; /** * One of these per region name. There are pointers into the other sections * that are sorted by region to the first record that has the given name. * * @author Steve Ratcliffe */ public class Mdr28 extends MdrSection implements HasHeaderFlags { private final List index = new ArrayList(); public Mdr28(MdrConfig config) { setConfig(config); } public void buildFromRegions(List regions) { Sort sort = getConfig().getSort(); List> keys = MdrUtils.sortList(sort, regions); int record = 0; Mdr28Record mdr28 = null; String lastName = null; for (SortKey key : keys) { Mdr13Record region = key.getObject(); String name = region.getName(); if (!name.equals(lastName)) { record++; mdr28 = new Mdr28Record(); mdr28.setIndex(record); mdr28.setName(name); mdr28.setStrOffset(region.getStrOffset()); mdr28.setMdr14(region.getMdr14()); index.add(mdr28); lastName = name; } assert mdr28 != null; region.setMdr28(mdr28); } } /** * Write out the contents of this section. * * @param writer Where to write it. */ public void writeSectData(ImgFileWriter writer) { PointerSizes sizes = getSizes(); int size21 = sizes.getSize(21); int size23 = sizes.getSize(23); int size27 = sizes.getSize(27); int idx = 0; for (Mdr28Record mdr28 : index) { putN(writer, size23, mdr28.getMdr23()); putStringOffset(writer, mdr28.getStrOffset()); putN(writer, size21, mdr28.getMdr21()); putN(writer, size27, mdr28.getMdr27()); idx++; } } /** * The size of a record in the section. This is not a constant and might vary * on various factors, such as the file version, if we are preparing for a * device, the number of maps etc. * * @return The size of a record in this section. */ public int getItemSize() { PointerSizes sizes = getSizes(); return sizes.getSize(23) + sizes.getStrOffSize() + sizes.getSize(21) + sizes.getSize(27); } /** * The number of records in this section. * * @return The number of items in the section. */ protected int numberOfItems() { return index.size(); } /** * Flag purposes are not known. */ public int getExtraValue() { return 0x7; } public List getIndex() { return Collections.unmodifiableList(index); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/mdr/MDRFile.java0000644000076400007640000002723712325011461022466 0ustar stevesteve/* * 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 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.imgfmt.app.mdr; import java.util.Arrays; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.FileBackedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.Country; import uk.me.parabola.imgfmt.app.lbl.Region; import uk.me.parabola.imgfmt.app.lbl.Zip; import uk.me.parabola.imgfmt.app.mdr.MdrSection.PointerSizes; import uk.me.parabola.imgfmt.app.net.RoadDef; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.trergn.Point; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * The MDR file. This is embedded into a .img file, either its own * separate one, or as one file in the gmapsupp.img. * * @author Steve Ratcliffe */ public class MDRFile extends ImgFile { private final MDRHeader mdrHeader; // The sections private final Mdr1 mdr1; private final Mdr4 mdr4; private final Mdr5 mdr5; private final Mdr6 mdr6; private final Mdr7 mdr7; private final Mdr8 mdr8; private final Mdr9 mdr9; private final Mdr10 mdr10; private final Mdr11 mdr11; private final Mdr12 mdr12; private final Mdr13 mdr13; private final Mdr14 mdr14; private final Mdr15 mdr15; private final Mdr17 mdr17; private final Mdr18 mdr18; private final Mdr19 mdr19; private final Mdr20 mdr20; private final Mdr21 mdr21; private final Mdr22 mdr22; private final Mdr23 mdr23; private final Mdr24 mdr24; private final Mdr25 mdr25; private final Mdr26 mdr26; private final Mdr27 mdr27; private final Mdr28 mdr28; private final Mdr29 mdr29; private int currentMap; private final boolean forDevice; private final MdrSection[] sections; private PointerSizes sizes; public MDRFile(ImgChannel chan, MdrConfig config) { Sort sort = config.getSort(); forDevice = config.isForDevice(); mdrHeader = new MDRHeader(config.getHeaderLen()); mdrHeader.setSort(sort); setHeader(mdrHeader); if (config.isWritable()) { ImgFileWriter fileWriter = new FileBackedImgFileWriter(chan, config.getOutputDir()); setWriter(fileWriter); // Position at the start of the writable area. position(mdrHeader.getHeaderLength()); } else { setReader(new BufferedImgFileReader(chan)); mdrHeader.readHeader(getReader()); } // Initialise the sections mdr1 = new Mdr1(config); mdr4 = new Mdr4(config); mdr5 = new Mdr5(config); mdr6 = new Mdr6(config); mdr7 = new Mdr7(config); mdr8 = new Mdr8(config); mdr9 = new Mdr9(config); mdr10 = new Mdr10(config); mdr11 = new Mdr11(config); mdr12 = new Mdr12(config); mdr13 = new Mdr13(config); mdr14 = new Mdr14(config); mdr15 = new Mdr15(config); mdr17 = new Mdr17(config); mdr18 = new Mdr18(config); mdr19 = new Mdr19(config); mdr20 = new Mdr20(config); mdr21 = new Mdr21(config); mdr22 = new Mdr22(config); mdr23 = new Mdr23(config); mdr24 = new Mdr24(config); mdr25 = new Mdr25(config); mdr26 = new Mdr26(config); mdr27 = new Mdr27(config); mdr28 = new Mdr28(config); mdr29 = new Mdr29(config); this.sections = new MdrSection[]{ null, mdr1, null, null, mdr4, mdr5, mdr6, mdr7, mdr8, mdr9, mdr10, mdr11, mdr12, mdr13, mdr14, mdr15, null, mdr17, mdr18, mdr19, mdr20, mdr21, mdr22, mdr23, mdr24, mdr25, mdr26, mdr27, mdr28, mdr29, }; mdr11.setMdr10(mdr10); } /** * Add a map to the index. You must add the map, then all of the items * that belong to it, before adding the next map. * @param mapName The numeric name of the map. * @param codePage The code page of the map. */ public void addMap(int mapName, int codePage) { currentMap++; mdr1.addMap(mapName, currentMap); Sort sort = mdrHeader.getSort(); if (sort.getCodepage() != codePage) System.err.println("WARNING: input files have different code pages"); } public Mdr14Record addCountry(Country country) { Mdr14Record record = new Mdr14Record(); String name = country.getLabel().getText(); record.setMapIndex(currentMap); record.setCountryIndex(country.getIndex()); record.setLblOffset(country.getLabel().getOffset()); record.setName(name); record.setStrOff(createString(name)); mdr14.addCountry(record); return record; } public Mdr13Record addRegion(Region region, Mdr14Record country) { Mdr13Record record = new Mdr13Record(); String name = region.getLabel().getText(); record.setMapIndex(currentMap); record.setLblOffset(region.getLabel().getOffset()); record.setCountryIndex(region.getCountry().getIndex()); record.setRegionIndex(region.getIndex()); record.setName(name); record.setStrOffset(createString(name)); record.setMdr14(country); mdr13.addRegion(record); return record; } public void addCity(Mdr5Record city) { int labelOffset = city.getLblOffset(); if (labelOffset != 0) { String name = city.getName(); assert name != null : "off=" + labelOffset; city.setMapIndex(currentMap); city.setStringOffset(createString(name)); mdr5.addCity(city); } } public void addZip(Zip zip) { int strOff = createString(zip.getLabel().getText()); mdr6.addZip(currentMap, zip, strOff); } public void addPoint(Point point, Mdr5Record city, boolean isCity) { assert currentMap > 0; int fullType = point.getType(); if (!MdrUtils.canBeIndexed(fullType)) return; Label label = point.getLabel(); String name = label.getText(); int strOff = createString(name); Mdr11Record poi = mdr11.addPoi(currentMap, point, name, strOff); poi.setCity(city); poi.setIsCity(isCity); poi.setType(fullType); mdr4.addType(point.getType()); } public void addStreet(RoadDef street, Mdr5Record mdrCity) { // Add a separate record for each name for (Label lab : street.getLabels()) { if (lab == null) break; if (lab.getOffset() == 0) continue; String name = lab.getText(); String cleanName = cleanUpName(name); int strOff = createString(cleanName); // We sort on the dirty name (ie with the Garmin shield codes) although those codes do not // affect the sort order. The string for mdr15 does not include the shield codes. mdr7.addStreet(currentMap, name, lab.getOffset(), strOff, mdrCity); } } /** * Remove shields and other kinds of strange characters. Perform any * rearrangement of the name to make it searchable. * @param name The street name as read from the img file. * @return The name as it will go into the index. */ private String cleanUpName(String name) { return Label.stripGarminCodes(name); } public void write() { mdr15.release(); ImgFileWriter writer = getWriter(); writeSections(writer); // Now refresh the header position(0); getHeader().writeHeader(writer); } /** * Write all the sections out. * * The order of all the operations in this method is important. The order * of the sections in the actual output file doesn't matter at all, so * they can be re-ordered to suit. * * Most of the complexity here is arranging the order of things so that the smallest * amount of temporary memory is required. * * @param writer File is written here. */ private void writeSections(ImgFileWriter writer) { sizes = new MdrMapSection.PointerSizes(sections); mdr1.finish(); // Deal with the dependencies between the sections. The order of the following // statements is sometimes important. mdr28.buildFromRegions(mdr13.getRegions()); mdr23.sortRegions(mdr13.getRegions()); mdr29.buildFromCountries(mdr14.getCountries()); mdr24.sortCountries(mdr14.getCountries()); mdr26.sortMdr28(mdr28.getIndex()); writeSection(writer, 4, mdr4); mdr1.preWrite(); mdr5.preWrite(); mdr20.preWrite(); // We write the following sections that contain per-map data, in the // order of the subsections of the reverse index that they are associated // with. writeSection(writer, 11, mdr11); mdr10.setNumberOfPois(mdr11.getNumberOfPois()); mdr12.setIndex(mdr11.getIndex()); mdr19.setPois(mdr11.getPois()); mdr17.addPois(mdr11.getPois()); mdr11.release(); if (forDevice) { mdr19.preWrite(); writeSection(writer, 19, mdr19); mdr18.setPoiTypes(mdr19.getPoiTypes()); mdr19.release(); writeSection(writer, 18, mdr18); } writeSection(writer, 10, mdr10); mdr9.setGroups(mdr10.getGroupSizes()); mdr10.release(); // mdr7 depends on the size of mdr20, so mdr20 must be built first mdr7.preWrite(); mdr20.buildFromStreets(mdr7.getStreets()); writeSection(writer, 7, mdr7); writeSection(writer, 5, mdr5); mdr25.sortCities(mdr5.getCities()); mdr27.sortCities(mdr5.getCities()); mdr17.addCities(mdr5.getSortedCities()); mdr5.release(); writeSection(writer, 6, mdr6); writeSection(writer, 20, mdr20); mdr20.release(); mdr21.buildFromStreets(mdr7.getStreets()); writeSection(writer, 21, mdr21); mdr21.release(); mdr22.buildFromStreets(mdr7.getStreets()); mdr8.setIndex(mdr7.getIndex()); mdr17.addStreets(mdr7.getSortedStreets()); mdr7.release(); writeSection(writer, 22, mdr22); mdr17.addStreetsByCountry(mdr22.getStreets()); mdr22.release(); if (forDevice) { writeSection(writer, 17, mdr17); mdr17.release(); } // The following do not have mdr1 subsections //writeSection(writer, 8, mdr8); writeSection(writer, 9, mdr9); writeSection(writer, 12, mdr12); writeSection(writer, 13, mdr13); writeSection(writer, 14, mdr14); writeSection(writer, 15, mdr15); writeSection(writer, 23, mdr23); writeSection(writer, 24, mdr24); writeSection(writer, 25, mdr25); mdr28.preWrite(); // TODO reorder writes below so this is not needed. Changes the output file though writeSection(writer, 26, mdr26); writeSection(writer, 27, mdr27); writeSection(writer, 28, mdr28); writeSection(writer, 29, mdr29); // write the reverse index last. mdr1.writeSubSections(writer); mdrHeader.setPosition(1, writer.position()); mdr1.writeSectData(writer); mdrHeader.setItemSize(1, mdr1.getItemSize()); mdrHeader.setEnd(1, writer.position()); mdrHeader.setExtraValue(1, mdr1.getExtraValue()); } /** * Write out the given single section. */ private void writeSection(ImgFileWriter writer, int sectionNumber, MdrSection section) { // Some sections are just not written in the device config if (forDevice && Arrays.asList(13, 14, 15, 21, 23, 26, 27, 28).contains(sectionNumber)) return; section.setSizes(sizes); mdrHeader.setPosition(sectionNumber, writer.position()); mdr1.setStartPosition(sectionNumber); section.preWrite(); if (!forDevice && section instanceof MdrMapSection) { MdrMapSection mapSection = (MdrMapSection) section; mapSection.setMapIndex(mdr1); mapSection.initIndex(sectionNumber); mapSection.relabelMaps(mdr1); } if (section instanceof HasHeaderFlags) mdrHeader.setExtraValue(sectionNumber, ((HasHeaderFlags) section).getExtraValue()); section.writeSectData(writer); int itemSize = section.getItemSize(); if (itemSize > 0) mdrHeader.setItemSize(sectionNumber, itemSize); mdrHeader.setEnd(sectionNumber, writer.position()); mdr1.setEndPosition(sectionNumber); } /** * Creates a string in MDR 15 and returns an offset value that can be * used to refer to it in the other sections. * @param str The text of the string. * @return An offset value. */ private int createString(String str) { return mdr15.createString(str); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Label.java0000644000076400007640000000727612471634002021506 0ustar stevesteve/* * Copyright (C) 2006,2014 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: 10-Jan-2007 */ package uk.me.parabola.imgfmt.app; import java.util.regex.Pattern; import uk.me.parabola.imgfmt.app.labelenc.EncodedText; /** * Labels are used for names of roads, points of interest etc. * * There are different storage formats. * * 1. A 6 bit compact uppercase ascii format, that has escape codes for some * special characters. * * 2. An 8 bit format. This seems to be a fairly straightforward latin-1 like * encoding with no tricks to reduce the amount of space required. * * 3. A multi-byte format. For unicode, cp932 etc. * * @author Steve Ratcliffe */ public class Label { public static final Label NULL_LABEL = new Label(""); public static final Label NULL_OUT_LABEL = new Label(new char[0]); private final String text; private final char[] encText; // The offset in to the data section. private int offset; public Label(String text) { this.text = text; this.encText = null; } public Label(char[] encText) { this.encText = encText; this.text = null; } public int getLength() { if (text != null) return text.length(); if (encText != null) return encText.length; return 0; } public String getText() { assert text != null; return text; } public char[] getEncText() { return encText; } // highway shields and "thin" separators public final static Pattern SHIELDS = Pattern.compile("[\u0001-\u0006\u001b-\u001c]"); // "fat" separators private final static Pattern SEPARATORS = Pattern.compile("[\u001d-\u001f]"); // two or more whitespace characters private final static Pattern SQUASH_SPACES = Pattern.compile("\\s\\s+"); public static String stripGarminCodes(String s) { if(s == null) return null; s = SHIELDS.matcher(s).replaceAll(""); // remove s = SEPARATORS.matcher(s).replaceAll(" "); // replace with a space s = SQUASH_SPACES.matcher(s).replaceAll(" "); // replace with a space // a leading separator would have turned into a space so trim it return s.trim(); } public static String squashSpaces(String s) { if(s == null || s.isEmpty()) return null; return SQUASH_SPACES.matcher(s).replaceAll(" "); // replace with single space } /** * The offset of this label in the LBL file. The first byte of this file * is zero and an offset of zero means that the label has a zero length/is * empty. * * @return The offset within the LBL file of this string. */ public int getOffset() { return offset; } public void setOffset(int offset) { this.offset = offset; } /** * Write this label to the given img file. * * @param writer The LBL file to write to. * @param encText The encoded version of the text for this label. */ public void write(ImgFileWriter writer, EncodedText encText) { assert encText != null; if (encText.getLength() > 0) writer.put(encText.getCtext(), 0, encText.getLength()); } /** * String version of the label, for diagnostic purposes. */ public String toString() { return text != null ? text : "[" + offset + "]"; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; return offset == ((Label) o).offset; } public int hashCode() { return offset; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/0000755000076400007640000000000012651103763020366 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/Region.java0000644000076400007640000000255211164230243022450 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * A region is in a country and contains one or more cities. * * @author Steve Ratcliffe */ public class Region { private char index; private final Country country; private Label label; public Region(Country country) { this.country = country; } public void write(ImgFileWriter writer) { writer.putChar(country.getIndex()); writer.put3(label.getOffset()); } public char getIndex() { assert index > 0 : "Index not yet set"; return index; } public Country getCountry() { return country; } public void setIndex(int index) { this.index = (char)index; } public void setLabel(Label label) { this.label = label; } public Label getLabel() { return label; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/POIIndex.java0000644000076400007640000000235711532723256022661 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.trergn.Subdivision; /** * Represent a POI index entry * * @author Mark Burton */ public class POIIndex { private final String name; private final byte poiIndex; private final Subdivision group; private final byte subType; public POIIndex(String name, byte poiIndex, Subdivision group, byte subType) { this.name = name; this.poiIndex = poiIndex; this.group = group; this.subType = subType; } void write(ImgFileWriter writer) { writer.put(poiIndex); writer.putChar((char)group.getNumber()); writer.put(subType); } public String getName() { return name; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/ExitFacility.java0000644000076400007640000000367211400726372023635 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; /** * Represent a facility at a motorway exit * * @author Mark Burton */ public class ExitFacility { private final int index; private final Label description; private final int type; // truck stop - 24 hour diesel + food private final int direction; // undefined private final int facilities; // none private final boolean last; public ExitFacility(int type, char direction, int facilities, Label description, boolean last, int index) { this.type = type; this.direction = directionCode(direction); this.facilities = facilities; this.description = description; this.last = last; this.index = index; } void write(ImgFileWriter writer) { int word = 0; word |= description.getOffset(); // 0:21 = label offset // 22 = unknown if(last) word |= 1 << 23; // 23 = last facility for this exit word |= type << 24; // 24:27 = 4 bit type // 28 = unknown word |= direction << 29; // 29:31 = 3 bit direction writer.putChar((char)word); writer.putChar((char)(word >> 16)); writer.put((byte)facilities); } public int getIndex() { return index; } public boolean getOvernightParking() { return false; } private int directionCode(char direction) { int code = "NSEWIOB".indexOf(direction); if(code < 0) code = 7; // undefined return code; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/POIRecord.java0000644000076400007640000002270012433605560023020 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import java.util.List; import uk.me.parabola.imgfmt.app.Exit; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; /** * @author Steve Ratcliffe */ public class POIRecord { static final byte HAS_STREET_NUM = 0x01; static final byte HAS_STREET = 0x02; static final byte HAS_CITY = 0x04; static final byte HAS_ZIP = 0x08; static final byte HAS_PHONE = 0x10; static final byte HAS_EXIT = 0x20; static final byte HAS_TIDE_PREDICTION = 0x40; /* Not used yet private static final AddrAbbr ABBR_HASH = new AddrAbbr(' ', "#"); private static final AddrAbbr ABBR_APARTMENT = new AddrAbbr('1', "APT"); private static final AddrAbbr ABBR_BUILDING = new AddrAbbr('2', "BLDG"); private static final AddrAbbr ABBR_DEPT = new AddrAbbr('3', "DEPT"); private static final AddrAbbr ABBR_FLAT = new AddrAbbr('4', "FL"); private static final AddrAbbr ABBR_ROOM = new AddrAbbr('5', "RM"); private static final AddrAbbr ABBR_STE = new AddrAbbr('6', "STE"); // don't know what this is? private static final AddrAbbr ABBR_UNIT = new AddrAbbr('7', "UNIT"); */ private int offset = -1; private Label poiName; private final SimpleStreetPhoneNumber simpleStreetNumber = new SimpleStreetPhoneNumber(); private final SimpleStreetPhoneNumber simplePhoneNumber = new SimpleStreetPhoneNumber(); private Label streetName; private Label streetNumberName; // Used for numbers such as 221b private Label complexPhoneNumber; // Used for numbers such as 221b private City city; private Zip zip; private Exit exit; //private String phoneNumber; public void setLabel(Label label) { this.poiName = label; } public void setStreetName(Label label) { this.streetName = label; } public boolean setSimpleStreetNumber(String streetNumber) { return simpleStreetNumber.set(streetNumber); } public void setComplexStreetNumber(Label label) { streetNumberName = label; } public boolean setSimplePhoneNumber(String phone) { return simplePhoneNumber.set(phone); } public void setComplexPhoneNumber(Label label) { complexPhoneNumber = label; } public void setZip(Zip zip) { this.zip = zip; } public void setCity(City city) { this.city = city; } public void setExit(Exit exit) { this.exit = exit; } void write(ImgFileWriter writer, byte POIGlobalFlags, int realofs, long numCities, long numZips, long numHighways, long numExitFacilities) { assert offset == realofs : "offset = " + offset + " realofs = " + realofs; int ptr = poiName.getOffset(); if (POIGlobalFlags != getPOIFlags()) ptr |= 0x800000; writer.put3(ptr); if (POIGlobalFlags != getPOIFlags()) writer.put(getWrittenPOIFlags(POIGlobalFlags)); if (streetNumberName != null) { int labOff = streetNumberName.getOffset(); writer.put((byte)((labOff & 0x7F0000) >> 16)); writer.putChar((char)(labOff & 0xFFFF)); } else if (simpleStreetNumber.isUsed()) simpleStreetNumber.write(writer); if (streetName != null) writer.put3(streetName.getOffset()); if (city != null) { char cityIndex = (char) city.getIndex(); if(numCities > 255) writer.putChar(cityIndex); else writer.put((byte)cityIndex); } if (zip != null) { char zipIndex = (char) zip.getIndex(); if(numZips > 255) writer.putChar(zipIndex); else writer.put((byte) zipIndex); } if (complexPhoneNumber != null) { int labOff = complexPhoneNumber.getOffset(); writer.put((byte)((labOff & 0x7F0000) >> 16)); writer.putChar((char)(labOff & 0xFFFF)); } else if (simplePhoneNumber.isUsed()) simplePhoneNumber.write(writer); if(exit != null) { Label description = exit.getDescription(); int val = 0; if(description != null) { val = description.getOffset(); assert val < 0x400000 : "Exit description label offset too large"; } if(exit.getOvernightParking()) val |= 0x400000; List facilites = exit.getFacilities(); ExitFacility ef = null; if(!facilites.isEmpty()) ef = facilites.get(0); if(ef != null) val |= 0x800000; // exit facilities defined writer.put3(val); char highwayIndex = (char)exit.getHighway().getIndex(); if(numHighways > 255) writer.putChar(highwayIndex); else writer.put((byte)highwayIndex); if(ef != null) { char exitFacilityIndex = (char)ef.getIndex(); if(numExitFacilities > 255) writer.putChar(exitFacilityIndex); else writer.put((byte)exitFacilityIndex); } } } byte getPOIFlags() { byte b = 0; if (streetName != null) b |= HAS_STREET; if (simpleStreetNumber.isUsed() || streetNumberName != null) b |= HAS_STREET_NUM; if (city != null) b |= HAS_CITY; if (zip != null) b |= HAS_ZIP; if (simplePhoneNumber.isUsed() || complexPhoneNumber != null) b |= HAS_PHONE; if (exit != null) b |= HAS_EXIT; return b; } byte getWrittenPOIFlags(byte POIGlobalFlags) { int flag = 0; int j = 0; int usedFields = getPOIFlags(); /* the local POI flag is really tricky if a bit is not set in the global mask we have to skip this bit in the local mask. In other words the meaning of the local bits change influenced by the global bits */ for(byte i = 0; i < 6; i++) { int mask = 1 << i; if((mask & POIGlobalFlags) == mask) { if((mask & usedFields) == mask) flag |= (1 << j); j++; } } flag |= 0x80; // gpsmapedit asserts for this bit set return (byte) flag; } /** * Sets the start offset of this POIRecord * * \return Number of bytes needed by this entry */ int calcOffset(int ofs, byte POIGlobalFlags, long numCities, long numZips, long numHighways, long numExitFacilities) { offset = ofs; int size = 3; if (exit != null) { size += 3; size += (numHighways > 255)? 2 : 1; if(!exit.getFacilities().isEmpty()) size += (numExitFacilities > 255)? 2 : 1; } if (POIGlobalFlags != getPOIFlags()) size += 1; if (simpleStreetNumber.isUsed()) size += simpleStreetNumber.getSize(); if (streetNumberName != null) size += 3; if (simplePhoneNumber.isUsed()) size += simplePhoneNumber.getSize(); if (complexPhoneNumber != null) size += 3; if (streetName != null) size += 3; if (city != null) { /* depending on how many cities are in the LBL block we have to write one or two bytes */ if(numCities > 255) size += 2; else size += 1; } if (zip != null) { // depending on how many zips are in the LBL block we have to write one or two bytes if(numZips > 255) size += 2; else size += 1; } return size; } public int getOffset() { if (offset == -1) throw new IllegalStateException("Offset not known yet."); return offset; } public Label getNameLabel() { return poiName; } public City getCity() { return city; } /** * Street and Phone numbers can be stored in two different ways in the poi record * Simple Number that only contain digits are coded in base 11 coding. * This helper class tries to code the given number. If the number contains other * chars like in 4a the coding fails and the caller has to use a Label instead */ class SimpleStreetPhoneNumber { private byte[] encodedNumber; private int encodedSize; /** * Encode a string as base 11. * @param str The input string. * @return If the string is not all numeric (or A) then false is returned * and this object is invalid. */ public boolean set(String str) { // remove surrounding whitespace to increase chance for simple encoding String number = str.trim(); encodedNumber = new byte[(number.length()/2)+2]; int i = 0; int j = 0; while (i < number.length()) { int c1 = decodeChar(number.charAt(i++)); int c2; if (i < number.length()) { c2 = decodeChar(number.charAt(i++)); } else c2 = 10; // Only 0-9 and - allowed if (c1 < 0 || c1 > 10 || c2 < 0 || c2 > 10) return false; // Encode as base 11 int val = c1 * 11 + c2; // first byte needs special marking with 0x80 // If this is not set would be treated as label pointer if (j == 0) val |= 0x80; encodedNumber[j++] = (byte)val; } if (j == 0) return false; if (j == 1) encodedNumber[j++] = (byte) 0xf8; else encodedNumber[j-1] |= 0x80; encodedSize = j; return true; } public void write(ImgFileWriter writer) { for(int i = 0; i < encodedSize; i++) writer.put(encodedNumber[i]); } public boolean isUsed() { return (encodedSize > 0); } public int getSize() { return encodedSize; } /** * Convert the characters '0' to '9' and '-' to a number 0-10 (base 11). * @param ch The character to convert. * @return A number between 0 and 10 or -1 if the character is not valid. */ private int decodeChar(char ch) { if (ch == '-') return 10; else if (ch >= '0' && ch <= '9') return (ch - '0'); else return -1; } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/Zip.java0000644000076400007640000000226611720516721021777 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; /** * A zip or postal code record. * * @author Steve Ratcliffe */ public class Zip { // The index is not stored in the file, you just use the index of it in // the section. private int index; private Label label; public void write(ImgFileWriter writer) { writer.put3(label.getOffset()); } public Label getLabel() { return label; } public void setLabel(Label label) { this.label = label; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/City.java0000644000076400007640000000756112305453510022144 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.trergn.Subdivision; /** * A city is in a region. It also has (or can have anyway) a reference to * an indexed point within the map itself. * * @author Steve Ratcliffe */ public class City { private static final int POINT_REF = 0x8000; private static final int REGION_IS_COUNTRY = 0x4000; private int index = -1; private final Region region; private final Country country; // This determines if a label is being used or a subdivision and point // combo. private boolean pointRef; // The location of the city. These could both be zero if we are using a // label instead. private Subdivision subdivision; private byte pointIndex; // You can have either a label or a subdivision and point. This will be // null if the location is being specified. private Label label; public City(Region region) { this.region = region; this.country = null; } public City(Country country) { this.country = country; this.region = null; } void write(ImgFileWriter writer) { //writer.put3() if (pointRef) { // System.err.println("City point = " + (int)pointIndex + " div = " + subdivision.getNumber()); writer.put(pointIndex); writer.putChar((char)subdivision.getNumber()); } else { writer.put3(label.getOffset()); } char info; if(region != null) info = (char) (region.getIndex() & 0x3fff); else info = (char) (REGION_IS_COUNTRY | (country.getIndex() & 0x3fff)); if (pointRef) info |= POINT_REF; writer.putChar(info); } public int getIndex() { if (index == -1) throw new IllegalStateException("Offset not known yet."); return index; } public void setIndex(int index) { this.index = index; } public void setLabel(Label label) { pointRef = false; this.label = label; } public void setPointIndex(byte pointIndex) { pointRef = true; this.pointIndex = pointIndex; } public void setSubdivision(Subdivision subdivision) { pointRef = true; this.subdivision = subdivision; } public String getName() { if (label == null) return ""; return label.getText(); } public int getLblOffset() { if (label == null) return 0; return label.getOffset(); } public String toString() { String result = ""; if(label != null) result += label.getText(); if (subdivision != null) result += " " + subdivision.getNumber() + "/" + pointIndex; if(country != null) result += " in country " + (0 + country.getIndex()); if(region != null) result += " in region " + (0 + region.getIndex()); return result; } public int getSubdivNumber() { return subdivision.getNumber(); } public int getPointIndex() { return pointIndex; } /** * Get the region or country number. * @return The region number if there is one, else the country number * with a flag bit set to indicate that it is one. */ public int getRegionCountryNumber() { if (region == null) { if (country != null) return country.getIndex() | 0x4000; } else { return region.getIndex(); } return 0; } public int getRegionNumber() { return region == null? 0: region.getIndex(); } public int getCountryNumber() { return country != null ? country.getIndex() : 0; } public Label getLabel() { return label; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/LBLHeader.java0000644000076400007640000001113312227500422022742 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 14, 2007 */ package uk.me.parabola.imgfmt.app.lbl; import java.io.UnsupportedEncodingException; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions; import uk.me.parabola.imgfmt.app.srt.Sort; /** * The header for the LBL file. * * @author Steve Ratcliffe */ public class LBLHeader extends CommonHeader { public static final int HEADER_LEN = 196; // Other lengths are possible private static final char UNK3_REC_LEN = 0; private int labelStart; // Start of labels. private int labelSize; // Size of file. private int offsetMultiplier; // Code page and sorting info. private Sort sort; private int sortDescriptionLength; // The type of encoding employed. This is not a length. private int encodingType = CodeFunctions.ENCODING_FORMAT6; // The label section also contains all kinds of records related to place, // so these have all been put in their own class. private final PlacesHeader placeHeader; public LBLHeader() { super(HEADER_LEN, "GARMIN LBL"); placeHeader = new PlacesHeader(); } public int getSortDescriptionLength() { return sortDescriptionLength; } /** * Read the rest of the header. Specific to the given file. It is guaranteed * that the file position will be set to the correct place before this is * called. * * @param reader The header is read from here. */ protected void readFileHeader(ImgFileReader reader) { labelStart = reader.getInt(); labelSize = reader.getInt(); offsetMultiplier = 1 << reader.get(); encodingType = reader.get(); // Read the places part of the header. placeHeader.readFileHeader(reader); int codepage = reader.getChar(); int id1 = reader.getChar(); int id2 = reader.getChar(); int descOff = reader.getInt(); int descLen = reader.getInt(); reader.position(descOff); byte[] bytes = reader.get(descLen); String description; try { description = new String(bytes, "ascii"); } catch (UnsupportedEncodingException e) { description = "Unknown"; } sort = new Sort(); sort.setCodepage(codepage); sort.setId1(id1); sort.setId2(id2); sort.setDescription(description); // more to do but not needed yet... Just set position reader.position(labelStart); } /** * Write the rest of the header. It is guaranteed that the writer will be set * to the correct position before calling. * * @param writer The header is written here. */ protected void writeFileHeader(ImgFileWriter writer) { // LBL1 section, these are regular labels writer.putInt(HEADER_LEN + sortDescriptionLength); writer.putInt(getLabelSize()); writer.put((byte) offsetMultiplier); writer.put((byte) encodingType); placeHeader.writeFileHeader(writer); writer.putChar((char) getCodePage()); // Identifying the sort char id1 = (char) sort.getId1(); writer.putChar(id1); char id2 = (char) sort.getId2(); if (id1 != 0 && id2 != 0) id2 |= 0x8000; writer.putChar(id2); writer.putInt(HEADER_LEN); writer.putInt(sortDescriptionLength); writer.putInt(placeHeader.getLastPos()); writer.putInt(0); writer.putChar(UNK3_REC_LEN); writer.putChar((char) 0); } protected int getEncodingType() { return encodingType; } public void setEncodingType(int type) { this.encodingType = type; } protected int getLabelSize() { return labelSize; } public void setLabelSize(int labelSize) { this.labelSize = labelSize; placeHeader.setLabelEnd(HEADER_LEN + sortDescriptionLength + labelSize); } protected int getCodePage() { return sort.getCodepage(); } public void setSort(Sort sort) { sortDescriptionLength = sort.getDescription().length() + 1; this.sort = sort; } public int getSortOrderId() { return sort.getSortOrderId(); } public int getLabelStart() { return labelStart; } public int getOffsetMultiplier() { return offsetMultiplier; } public PlacesHeader getPlaceHeader() { return placeHeader; } public void setOffsetMultiplier(int offsetMultiplier) { this.offsetMultiplier = offsetMultiplier; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/PlacesHeader.java0000644000076400007640000001560011520506576023556 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; /** * This is not a separate header, but rather part of the LBL header. It is just * separated out for convenience. All the records that have some kind of * meaning associated with a place are put here. * * @author Steve Ratcliffe */ public class PlacesHeader { private static final char COUNTRY_REC_LEN = 3; private static final char REGION_REC_LEN = 5; private static final char CITY_REC_LEN = 5; private static final char POI_INDEX_REC_LEN = 4; private static final char POI_TYPE_INDEX_REC_LEN = 4; private static final char ZIP_REC_LEN = 3; private static final char HIGHWAY_REC_LEN = 6; private static final char EXIT_REC_LEN = 5; private static final char HIGHWAYDATA_REC_LEN = 3; private final Section country = new Section(COUNTRY_REC_LEN); private final Section region = new Section(country, REGION_REC_LEN); private final Section city = new Section(region, CITY_REC_LEN); private final Section poiIndex = new Section(city, POI_INDEX_REC_LEN); private final Section poiProperties = new Section(poiIndex); private final Section poiTypeIndex = new Section(poiProperties, POI_TYPE_INDEX_REC_LEN); private final Section zip = new Section(poiTypeIndex, ZIP_REC_LEN); private final Section highway = new Section(zip, HIGHWAY_REC_LEN); private final Section exitFacility = new Section(highway, EXIT_REC_LEN); private final Section highwayData = new Section(exitFacility, HIGHWAYDATA_REC_LEN); private byte POIGlobalFlags ; void setPOIGlobalFlags(byte flags) { this.POIGlobalFlags = flags; } byte getPOIGlobalFlags() { return POIGlobalFlags; } void writeFileHeader(ImgFileWriter writer) { writer.putInt(country.getPosition()); writer.putInt(country.getSize()); writer.putChar(country.getItemSize()); writer.putInt(0); writer.putInt(region.getPosition()); writer.putInt(region.getSize()); writer.putChar(region.getItemSize()); writer.putInt(0); writer.putInt(city.getPosition()); writer.putInt(city.getSize()); writer.putChar(city.getItemSize()); writer.putInt(0); writer.putInt(poiIndex.getPosition()); writer.putInt(poiIndex.getSize()); writer.putChar(poiIndex.getItemSize()); writer.putInt(0); writer.putInt(poiProperties.getPosition()); writer.putInt(poiProperties.getSize()); writer.put((byte) 0); // offset multiplier // mb 5/9/2009 - discovered that Garmin maps can contain more // than 8 bits of POI global flags - have seen the 9th bit set // to indicate the presence of some extra POI info (purpose // unknown but it starts with a byte that contains the number // of further bytes to read << 1) - therefore, this group // should probably be: 16 bits of POI global flags followed by // 16 zero bits rather than 8 bits of flags and 24 zero bits writer.put(POIGlobalFlags); // properties global mask writer.putChar((char) 0); writer.put((byte) 0); writer.putInt(poiTypeIndex.getPosition()); writer.putInt(poiTypeIndex.getSize()); writer.putChar(poiTypeIndex.getItemSize()); writer.putInt(0); writer.putInt(zip.getPosition()); writer.putInt(zip.getSize()); writer.putChar(zip.getItemSize()); writer.putInt(0); writer.putInt(highway.getPosition()); writer.putInt(highway.getSize()); writer.putChar(highway.getItemSize()); writer.putInt(0); writer.putInt(exitFacility.getPosition()); writer.putInt(exitFacility.getSize()); writer.putChar(exitFacility.getItemSize()); writer.putInt(0); writer.putInt(highwayData.getPosition()); writer.putInt(highwayData.getSize()); writer.putChar(highwayData.getItemSize()); writer.putInt(0); } void readFileHeader(ImgFileReader reader) { reader.position(0x1f); country.readSectionInfo(reader, true); reader.getInt(); region.readSectionInfo(reader, true); reader.getInt(); city.readSectionInfo(reader, true); reader.getInt(); poiIndex.readSectionInfo(reader, true); reader.getInt(); poiProperties.readSectionInfo(reader, false); reader.get(); // offset multiplier POIGlobalFlags = reader.get(); reader.getChar(); reader.get(); poiTypeIndex.readSectionInfo(reader, true); reader.getInt(); zip.readSectionInfo(reader, true); reader.getInt(); highway.readSectionInfo(reader, true); reader.getInt(); exitFacility.readSectionInfo(reader, true); reader.getInt(); highwayData.readSectionInfo(reader, true); reader.getInt(); } int getLastPos() { // Beware this is not really valid until all is written. return highwayData.getEndPos(); } void setLabelEnd(int pos) { country.setPosition(pos); } void endCountries(int pos) { country.setSize(pos - country.getPosition()); } void endRegions(int pos) { region.setSize(pos - region.getPosition()); } void endCity(int pos) { city.setSize(pos - city.getPosition()); } void endPOI(int pos) { poiProperties.setSize(pos - poiProperties.getPosition()); } void endPOIIndex(int pos) { poiIndex.setSize(pos - poiIndex.getPosition()); } void endPOITypeIndex(int pos) { poiTypeIndex.setSize(pos - poiTypeIndex.getPosition()); } void endZip(int pos) { zip.setSize(pos - zip.getPosition()); } void endHighway(int pos) { highway.setSize(pos - highway.getPosition()); } void endExitFacility(int pos) { exitFacility.setSize(pos - exitFacility.getPosition()); } void endHighwayData(int pos) { highwayData.setSize(pos - highwayData.getPosition()); } public int getNumCities() { return city.getNumItems(); } public int getNumZips() { return zip.getNumItems(); } public int getPoiPropertiesStart() { return poiProperties.getPosition(); } public int getPoiPropertiesEnd() { return poiProperties.getEndPos(); } public int getCitiesStart() { return city.getPosition(); } public int getCitiesEnd() { return city.getEndPos(); } public int getNumExits() { return exitFacility.getNumItems(); } public int getCountriesStart() { return country.getPosition(); } public int getCountriesEnd() { return country.getEndPos(); } public int getRegionsStart() { return region.getPosition(); } public int getRegionsEnd() { return region.getEndPos(); } public int getNumHighways() { return highway.getNumItems(); } public int getZipsStart() { return zip.getPosition(); } public int getZipsEnd() { return zip.getEndPos(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/PlacesFile.java0000644000076400007640000002505512571251757023257 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import uk.me.parabola.imgfmt.app.Exit; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.srt.CombinedSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; import uk.me.parabola.imgfmt.app.trergn.Subdivision; /** * This is really part of the LBLFile. We split out all the parts of the file * that are to do with location to here. */ @SuppressWarnings({"unchecked", "rawtypes"}) public class PlacesFile { private final Map countries = new LinkedHashMap<>(); private final List countryList = new ArrayList<>(); private final Map regions = new LinkedHashMap<>(); private final List regionList = new ArrayList<>(); private final Map cities = new LinkedHashMap<>(); private final List cityList = new ArrayList<>(); private final Map postalCodes = new LinkedHashMap<>(); private final List zipList = new ArrayList<>(); private final List highways = new ArrayList<>(); private final List exitFacilities = new ArrayList<>(); private final List pois = new ArrayList<>(); private final List[] poiIndex = new ArrayList[256]; private LBLFile lblFile; private PlacesHeader placeHeader; private boolean poisClosed; private Sort sort; private final Random random = new Random(); /** * We need to have links back to the main LBL file and need to be passed * the part of the header that we manage here. * * @param file The main LBL file, used so that we can create labels. * @param pheader The place header. */ void init(LBLFile file, PlacesHeader pheader) { lblFile = file; placeHeader = pheader; } void write(ImgFileWriter writer) { for (Country c : countryList) c.write(writer); placeHeader.endCountries(writer.position()); for (Region region : regionList) region.write(writer); placeHeader.endRegions(writer.position()); for (City sc : cityList) sc.write(writer); placeHeader.endCity(writer.position()); for (List pil : poiIndex) { if(pil != null) { // sort entries by POI name List> sorted = new ArrayList<>(); for (POIIndex index : pil) { SortKey sortKey = sort.createSortKey(index, index.getName()); sorted.add(sortKey); } Collections.sort(sorted); for (SortKey key : sorted) { key.getObject().write(writer); } } } placeHeader.endPOIIndex(writer.position()); int poistart = writer.position(); byte poiglobalflags = placeHeader.getPOIGlobalFlags(); for (POIRecord p : pois) p.write(writer, poiglobalflags, writer.position() - poistart, cityList.size(), postalCodes.size(), highways.size(), exitFacilities.size()); placeHeader.endPOI(writer.position()); int numPoiIndexEntries = 0; for (int i = 0; i < 256; ++i) { if(poiIndex[i] != null) { writer.put((byte)i); writer.put3(numPoiIndexEntries + 1); numPoiIndexEntries += poiIndex[i].size(); } } placeHeader.endPOITypeIndex(writer.position()); for (Zip z : zipList) z.write(writer); placeHeader.endZip(writer.position()); int extraHighwayDataOffset = 0; for (Highway h : highways) { h.setExtraDataOffset(extraHighwayDataOffset); extraHighwayDataOffset += h.getExtraDataSize(); h.write(writer, false); } placeHeader.endHighway(writer.position()); for (ExitFacility ef : exitFacilities) ef.write(writer); placeHeader.endExitFacility(writer.position()); for (Highway h : highways) h.write(writer, true); placeHeader.endHighwayData(writer.position()); } Country createCountry(String name, String abbr) { String s = abbr != null ? name + (char)0x1d + abbr : name; Country c = countries.get(s); if(c == null) { c = new Country(countries.size()+1); Label l = lblFile.newLabel(s); c.setLabel(l); countries.put(s, c); } return c; } Region createRegion(Country country, String name, String abbr) { String s = abbr != null ? name + (char)0x1d + abbr : name; String uniqueRegionName = s.toUpperCase() + "_C" + country.getLabel().getOffset(); Region r = regions.get(uniqueRegionName); if(r == null) { r = new Region(country); Label l = lblFile.newLabel(s); r.setLabel(l); regionList.add(r); regions.put(uniqueRegionName, r); } return r; } City createCity(Country country, String name, boolean unique) { String uniqueCityName = name.toUpperCase() + "_C" + country.getLabel().getOffset(); // if unique is true, make sure that the name really is unique if(unique && cities.get(uniqueCityName) != null) { do { // add random suffix uniqueCityName += "_" + new Random().nextInt(0x10000); } while(cities.get(uniqueCityName) != null); } City c = null; if (!unique) c = cities.get(uniqueCityName); if (c == null) { c = new City(country); Label l = lblFile.newLabel(name); c.setLabel(l); cityList.add(c); cities.put(uniqueCityName, c); assert cityList.size() == cities.size() : " cityList and cities are different lengths after inserting " + name + " and " + uniqueCityName; } return c; } City createCity(Region region, String name, boolean unique) { String uniqueCityName = name.toUpperCase() + "_R" + region.getLabel().getOffset(); // if unique is true, make sure that the name really is unique if (unique && cities.get(uniqueCityName) != null) { do { // add semi-random suffix. uniqueCityName += "_" + random.nextInt(0x10000); } while(cities.get(uniqueCityName) != null); } City c = null; if(!unique) c = cities.get(uniqueCityName); if(c == null) { c = new City(region); Label l = lblFile.newLabel(name); c.setLabel(l); cityList.add(c); cities.put(uniqueCityName, c); assert cityList.size() == cities.size() : " cityList and cities are different lengths after inserting " + name + " and " + uniqueCityName; } return c; } Zip createZip(String code) { Zip z = postalCodes.get(code); if(z == null) { z = new Zip(); Label l = lblFile.newLabel(code); z.setLabel(l); zipList.add(z); postalCodes.put(code, z); } return z; } Highway createHighway(Region region, String name) { Highway h = new Highway(region, highways.size()+1); Label l = lblFile.newLabel(name); h.setLabel(l); highways.add(h); return h; } public ExitFacility createExitFacility(int type, char direction, int facilities, String description, boolean last) { Label d = lblFile.newLabel(description); ExitFacility ef = new ExitFacility(type, direction, facilities, d, last, exitFacilities.size()+1); exitFacilities.add(ef); return ef; } POIRecord createPOI(String name) { assert !poisClosed; // TODO... POIRecord p = new POIRecord(); Label l = lblFile.newLabel(name); p.setLabel(l); pois.add(p); return p; } POIRecord createExitPOI(String name, Exit exit) { assert !poisClosed; // TODO... POIRecord p = new POIRecord(); Label l = lblFile.newLabel(name); p.setLabel(l); p.setExit(exit); pois.add(p); return p; } POIIndex createPOIIndex(String name, int index, Subdivision group, int type) { assert index < 0x100 : "Too many POIS in division"; POIIndex pi = new POIIndex(name, (byte)index, group, (byte)type); int t = type >> 8; if(poiIndex[t] == null) poiIndex[t] = new ArrayList(); poiIndex[t].add(pi); return pi; } void allPOIsDone() { sortCountries(); sortRegions(); sortCities(); sortZips(); poisClosed = true; byte poiFlags = 0; for (POIRecord p : pois) { poiFlags |= p.getPOIFlags(); } placeHeader.setPOIGlobalFlags(poiFlags); int ofs = 0; for (POIRecord p : pois) ofs += p.calcOffset(ofs, poiFlags, cityList.size(), postalCodes.size(), highways.size(), exitFacilities.size()); } /** * I don't know that you have to sort these (after all most tiles will * only be in one country or at least a very small number). * * But why not? */ private void sortCountries() { List> keys = new ArrayList<>(); for (Country c : countries.values()) { SortKey key = sort.createSortKey(c, c.getLabel()); keys.add(key); } Collections.sort(keys); countryList.clear(); int index = 1; for (SortKey key : keys) { Country c = key.getObject(); c.setIndex(index++); countryList.add(c); } } /** * Sort the regions by the defined sort. */ private void sortRegions() { List> keys = new ArrayList<>(); for (Region r : regionList) { SortKey key = sort.createSortKey(r, r.getLabel(), r.getCountry().getIndex()); keys.add(key); } Collections.sort(keys); regionList.clear(); int index = 1; for (SortKey key : keys) { Region r = key.getObject(); r.setIndex(index++); regionList.add(r); } } /** * Sort the cities by the defined sort. */ private void sortCities() { List> keys = new ArrayList<>(); for (City c : cityList) { SortKey sortKey = sort.createSortKey(c, c.getLabel()); sortKey = new CombinedSortKey<>(sortKey, c.getRegionNumber(), c.getCountryNumber()); keys.add(sortKey); } Collections.sort(keys); cityList.clear(); int index = 1; for (SortKey sc: keys) { City city = sc.getObject(); city.setIndex(index++); cityList.add(city); } } private void sortZips() { List> keys = new ArrayList<>(); for (Zip c : postalCodes.values()) { SortKey sortKey = sort.createSortKey(c, c.getLabel()); keys.add(sortKey); } Collections.sort(keys); zipList.clear(); int index = 1; for (SortKey sc: keys) { Zip zip = sc.getObject(); zip.setIndex(index++); zipList.add(zip); } } public int numCities() { return cityList.size(); } public int numZips() { return postalCodes.size(); } public void setSort(Sort sort) { this.sort = sort; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/LBLFileReader.java0000644000076400007640000003470212433605560023573 0ustar stevesteve/* * 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 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.imgfmt.app.lbl; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.labelenc.CharacterDecoder; import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions; import uk.me.parabola.imgfmt.app.labelenc.DecodedText; import uk.me.parabola.imgfmt.app.trergn.Subdivision; import uk.me.parabola.imgfmt.fs.ImgChannel; import static uk.me.parabola.imgfmt.app.Label.NULL_LABEL; /** * The file that holds all the labels for the map. * * There are also a number of sections that hold country, * region, city, etc. records. * * The main focus of mkgmap is creating files, there are plenty of applications * that read and display the data, reading is implemented only to the * extent required to support creating the various auxiliary files etc. * * @author Steve Ratcliffe */ public class LBLFileReader extends ImgFile { private CharacterDecoder textDecoder = CodeFunctions.getDefaultDecoder(); private final LBLHeader header = new LBLHeader(); private final Map labels = new HashMap<>(); private final Map pois = new HashMap<>(); private final List countries = new ArrayList<>(); private final List regions = new ArrayList<>(); private final Map zips = new HashMap<>(); private final List cities = new ArrayList<>(); public LBLFileReader(ImgChannel chan) { setHeader(header); setReader(new BufferedImgFileReader(chan)); header.readHeader(getReader()); int offsetMultiplier = header.getOffsetMultiplier(); CodeFunctions funcs = CodeFunctions.createEncoderForLBL( header.getEncodingType(), header.getCodePage()); textDecoder = funcs.getDecoder(); readLables(offsetMultiplier); readCountries(); readRegions(); readCities(); readZips(); readPoiInfo(); } /** * Get a label by its offset in the label area. * @param offset The offset in the label section. The offset 0 always * is an empty string. * @return The label including its text. */ public Label fetchLabel(int offset) { Label label = labels.get(offset); if (label == null) { assert offset == 0 : "Invalid label offset found " + offset; return NULL_LABEL; } return label; } /** * Get a list of cites. This is not cached here. * @return A list of City objects. */ public List getCities() { return cities; } public List getCountries() { return Collections.unmodifiableList(countries); } public List getRegions() { return Collections.unmodifiableList(regions); } public List getZips() { return new ArrayList<>(zips.values()); } /** * Return POI information. * @param offset The offset of the poi information in the header. * @return Returns a poi record at the given offset. Returns null if * there isn't one at that offset (probably a bug if that does happen though...). */ public POIRecord fetchPoi(int offset) { return pois.get(offset); } /** * Read a cache the countries. These are used when reading cities. */ private void readCountries() { ImgFileReader reader = getReader(); PlacesHeader placeHeader = header.getPlaceHeader(); countries.add(null); // 1 based indexes int start = placeHeader.getCountriesStart(); int end = placeHeader.getCountriesEnd(); reader.position(start); int index = 1; while (reader.position() < end) { int offset = reader.getu3(); Label label = fetchLabel(offset); if (label != null) { Country country = new Country(index); country.setLabel(label); countries.add(country); } index++; } } /** * Read an cache the regions. These are used when reading cities. */ private void readRegions() { ImgFileReader reader = getReader(); PlacesHeader placeHeader = header.getPlaceHeader(); int start = placeHeader.getRegionsStart(); int end = placeHeader.getRegionsEnd(); regions.add(null); reader.position(start); int index = 1; while (reader.position() < end) { int country = reader.getChar(); int offset = reader.getu3(); Label label = fetchLabel(offset); if (label != null) { Region region = new Region(countries.get(country)); region.setIndex(index); region.setLabel(label); regions.add(region); } index++; } } /** * Read in the city section and cache the results here. They are needed * to read in the POI properties section. */ private void readCities() { PlacesHeader placeHeader = header.getPlaceHeader(); int start = placeHeader.getCitiesStart(); int end = placeHeader.getCitiesEnd(); ImgFileReader reader = getReader(); // Since cities are indexed starting from 1, we add a null one at index 0 reader.position(start); int index = 1; while (reader.position() < end) { // First is either a label offset or a point/subdiv combo, we // don't know until we have read further int label = reader.getu3(); int info = reader.getChar(); City city; if ((info & 0x4000) == 0) { Region region = regions.get(info & 0x3fff); city = new City(region); } else { Country country = countries.get(info & 0x3fff); city = new City(country); } city.setIndex(index); if ((info & 0x8000) == 0) { city.setSubdivision(Subdivision.createEmptySubdivision(1)); Label label1 = labels.get(label & 0x3fffff); city.setLabel(label1); } else { // Has subdiv/point index int pointIndex = label & 0xff; int subdiv = (label >> 8) & 0xffff; city.setPointIndex((byte) pointIndex); city.setSubdivision(Subdivision.createEmptySubdivision(subdiv)); } cities.add(city); index++; } } /** * Read and cache all the labels. * * Note: It is pretty pointless saving the whole label rather than just * the text, except that other objects take a Label. Perhaps this can * be changed. */ private void readLables(int mult) { ImgFileReader reader = getReader(); labels.put(0, NULL_LABEL); int start = header.getLabelStart(); int size = header.getLabelSize(); reader.position(start + mult); int labelOffset = mult; for (int off = mult; off <= size; off++) { byte b = reader.get(); if (textDecoder.addByte(b)) { labelOffset = saveLabel(labelOffset, off, mult); // If there is an offset multiplier greater than one then padding will be used to // ensure that the labels are on suitable boundaries. We must skip over any such padding. while ((labelOffset & (mult - 1)) != 0) { textDecoder.reset(); if (labelOffset <= off) { // In the 6bit decoder, we may have already read the (first) padding byte and so // we increment the label offset without reading anything more. labelOffset++; } else { reader.get(); //noinspection AssignmentToForLoopParameter off++; labelOffset++; } } } } } /** * We have a label and we need to save it. * * @param labelOffset The offset of the label we are about to save. * @param currentOffset The current offset that last read from. * @param multiplier The label offset multiplier. * @return The offset of the next label. */ private int saveLabel(int labelOffset, int currentOffset, int multiplier) { DecodedText encText = textDecoder.getText(); String text = encText.getText(); Label label = new Label(text); assert (labelOffset & (multiplier - 1)) == 0; int adustedOffset = labelOffset / multiplier; label.setOffset(adustedOffset); labels.put(adustedOffset, label); // Calculate the offset of the next label. This is not always // the current offset + 1 because there may be bytes left // inside the decoder. return currentOffset + 1 + encText.getOffsetAdjustment(); } /** * Reads the zips. */ private void readZips() { ImgFileReader reader = getReader(); PlacesHeader placeHeader = header.getPlaceHeader(); int start = placeHeader.getZipsStart(); int end = placeHeader.getZipsEnd(); reader.position(start); int zipIndex = 1; while (reader.position() < end) { int lblOffset = reader.get3(); Zip zip = new Zip(); zip.setLabel(fetchLabel(lblOffset)); zip.setIndex(zipIndex); zips.put(zip.getIndex(), zip); zipIndex++; } } /** * Read all the POI information. * This will create a POIRecord, but we just get the name at the minute. * * TODO: not finished */ private void readPoiInfo() { ImgFileReader reader = getReader(); PlacesHeader placeHeader = header.getPlaceHeader(); int poiGlobalFlags = placeHeader.getPOIGlobalFlags(); int start = placeHeader.getPoiPropertiesStart(); int end = placeHeader.getPoiPropertiesEnd(); reader.position(start); PoiMasks localMask = makeLocalMask(placeHeader); while (reader.position() < end) { int poiOffset = position() - start; int val = reader.getu3(); int labelOffset = val & 0x3fffff; boolean override = (val & 0x800000) != 0; POIRecord poi = new POIRecord(); poi.setLabel(fetchLabel(labelOffset)); // We have what we want, but now have to find the start of the // next record as they are not fixed length. int flags; boolean hasStreet; boolean hasStreetNum; boolean hasCity; boolean hasZip; boolean hasPhone; boolean hasHighwayExit; boolean hasTides; if (override) { flags = reader.get(); hasStreetNum = (flags & localMask.streetNumMask) != 0; hasStreet = (flags & localMask.streetMask) != 0; hasCity = (flags & localMask.cityMask) != 0; hasZip = (flags & localMask.zipMask) != 0; hasPhone = (flags & localMask.phoneMask) != 0; hasHighwayExit = (flags & localMask.highwayExitMask) != 0; hasTides = (flags & localMask.tidesMask) != 0; } else { flags = poiGlobalFlags; hasStreetNum = (flags & POIRecord.HAS_STREET_NUM) != 0; hasStreet = (flags & POIRecord.HAS_STREET) != 0; hasCity = (flags & POIRecord.HAS_CITY) != 0; hasZip = (flags & POIRecord.HAS_ZIP) != 0; hasPhone = (flags & POIRecord.HAS_PHONE) != 0; hasHighwayExit = (flags & POIRecord.HAS_EXIT) != 0; hasTides = (flags & POIRecord.HAS_TIDE_PREDICTION) != 0; } if (hasStreetNum) { byte b = reader.get(); if ((b & 0x80) == 0) { int mpoffset = (b << 16) & 0xff0000; mpoffset |= reader.getChar() & 0xffff; poi.setComplexStreetNumber(fetchLabel(mpoffset)); } else { poi.setSimpleStreetNumber(reader.getBase11str(b, '-')); } } if (hasStreet) { int streetNameOffset = reader.getu3();// label for street Label label = fetchLabel(streetNameOffset); poi.setStreetName(label); } if (hasCity) { int cityIndex; if (placeHeader.getNumCities() > 0xFF) cityIndex = reader.getChar(); else cityIndex = reader.get() & 0xff; poi.setCity(cities.get(cityIndex-1)); } if (hasZip) { int zipIndex; if (placeHeader.getNumZips() > 0xff) zipIndex = reader.getChar(); else zipIndex = reader.get() & 0xff; poi.setZip(zips.get(zipIndex-1)); } if (hasPhone) { byte b = reader.get(); if ((b & 0x80) == 0) { // Yes this is a bit strange it is a byte followed by a char int mpoffset = (b << 16) & 0xff0000; mpoffset |= reader.getChar() & 0xffff; poi.setComplexPhoneNumber(fetchLabel(mpoffset)); } else { poi.setSimplePhoneNumber(reader.getBase11str(b, '-')); } } if (hasHighwayExit) { int lblinfo = reader.getu3(); int highwayLabelOffset = lblinfo & 0x3FFFF; boolean indexed = (lblinfo & 0x800000) != 0; boolean overnightParking = (lblinfo & 0x400000) != 0; int highwayIndex = (placeHeader.getNumHighways() > 255) ? reader.getChar() : reader.get(); if (indexed) { int eidx = (placeHeader.getNumExits() > 255) ? reader.getChar() : reader.get(); } } if (hasTides) { System.out.println("Map has tide prediction, please implement!"); } pois.put(poiOffset, poi); } } /** * The meaning of the bits in the local flags depends on which bits * are set in the global flags. Hence we have to calculate the * masks to use. These are held in an instance of PoiMasks * @param placeHeader The label header. * @return The masks as modified by the global flags. */ private PoiMasks makeLocalMask(PlacesHeader placeHeader) { int globalPoi = placeHeader.getPOIGlobalFlags(); char mask= 0x1; boolean hasStreetNum = (globalPoi & POIRecord.HAS_STREET_NUM) != 0; boolean hasStreet = (globalPoi & POIRecord.HAS_STREET) != 0; boolean hasCity = (globalPoi & POIRecord.HAS_CITY) != 0; boolean hasZip = (globalPoi & POIRecord.HAS_ZIP) != 0; boolean hasPhone = (globalPoi & POIRecord.HAS_PHONE) != 0; boolean hasHighwayExit = (globalPoi & POIRecord.HAS_EXIT) != 0; boolean hasTides = (globalPoi & POIRecord.HAS_TIDE_PREDICTION) != 0; PoiMasks localMask = new PoiMasks(); if (hasStreetNum) { localMask.streetNumMask = mask; mask <<= 1; } if (hasStreet) { localMask.streetMask = mask; mask <<= 1; } if (hasCity) { localMask.cityMask = mask; mask <<= 1; } if (hasZip) { localMask.zipMask = mask; mask <<= 1; } if (hasPhone) { localMask.phoneMask = mask; mask <<= 1; } if (hasHighwayExit) { localMask.highwayExitMask = mask; mask <<= 1; } if (hasTides) { localMask.tidesMask = mask; mask <<= 1; } return localMask; } public Map getLabels() { Map m = new HashMap<>(); for (Map.Entry ent : labels.entrySet()) { m.put(ent.getKey(), ent.getValue().getText()); } return m; } public int getCodePage() { return header.getCodePage(); } public int getSortOrderId() { return header.getSortOrderId(); } private class PoiMasks { private char streetNumMask; private char streetMask; private char cityMask; private char zipMask; private char phoneMask; private char highwayExitMask; private char tidesMask; } public int getEncodingType() { return header.getEncodingType(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/Country.java0000644000076400007640000000234511533707246022704 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * A country contains one or more regions. * * @author Steve Ratcliffe */ public class Country { // The country number. This is not recorded in the file private char index; private Label label; public Country(int index) { this.index = (char) index; } void write(ImgFileWriter writer) { writer.put3(label.getOffset()); } public char getIndex() { return index; } public void setLabel(Label label) { this.label = label; } public Label getLabel() { return label; } public void setIndex(int index) { this.index = (char) index; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/package.html0000644000076400007640000000053110735217321022643 0ustar stevesteve

The LBL file

This file holds the textual labels. There are a number of different formats that the text is stored in, the default format is a space saving format that saves characters in 6 bits but doesn't allow for non-ascii characters.

This file can also hold region, town/city and point of interest names

mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/LBLFile.java0000644000076400007640000001515712305453510022445 0ustar stevesteve/* * 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: 03-Dec-2006 */ package uk.me.parabola.imgfmt.app.lbl; import java.util.HashMap; import java.util.Map; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.Exit; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.labelenc.BaseEncoder; import uk.me.parabola.imgfmt.app.labelenc.CharacterEncoder; import uk.me.parabola.imgfmt.app.labelenc.CodeFunctions; import uk.me.parabola.imgfmt.app.labelenc.EncodedText; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.trergn.Subdivision; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The file that holds all the labels for the map. * * Would be quite simple, but there are a number of sections that hold country, * region, city, etc. records. * * To begin with I shall only support regular labels. * * @author Steve Ratcliffe */ public class LBLFile extends ImgFile { private static final Logger log = Logger.getLogger(LBLFile.class); private CharacterEncoder textEncoder = CodeFunctions.getDefaultEncoder(); private final Map labelCache = new HashMap(); private final LBLHeader lblHeader = new LBLHeader(); private final PlacesFile places = new PlacesFile(); private Sort sort; // Shift value for the label offset. private final int offsetMultiplier = 1; public LBLFile(ImgChannel chan, Sort sort) { this.sort = sort; lblHeader.setSort(sort); lblHeader.setOffsetMultiplier(offsetMultiplier); setHeader(lblHeader); setWriter(new BufferedImgFileWriter(chan)); position(LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength()); // The zero offset is for no label. getWriter().put((byte) 0); alignForNext(); places.init(this, lblHeader.getPlaceHeader()); places.setSort(sort); labelCache.put(BaseEncoder.NO_TEXT, Label.NULL_OUT_LABEL); } public void write() { writeBody(); } public void writePost() { // Now that the body is written all the required offsets will be set up // inside the header, so we can go back and write it. ImgFileWriter writer = getWriter(); getHeader().writeHeader(writer); // Text can be put between the header and the body of the file. writer.put(Utils.toBytes(sort.getDescription())); writer.put((byte) 0); assert writer.position() == LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength(); } private void writeBody() { // The label section has already been written, but we need to record // its size before doing anything else. lblHeader.setLabelSize(getWriter().position() - (LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength())); places.write(getWriter()); } public void setCharacterType(String cs, boolean forceUpper) { log.info("encoding type " + cs); CodeFunctions cfuncs = CodeFunctions.createEncoderForLBL(cs); lblHeader.setEncodingType(cfuncs.getEncodingType()); textEncoder = cfuncs.getEncoder(); if (forceUpper && textEncoder instanceof BaseEncoder) { BaseEncoder baseEncoder = (BaseEncoder) textEncoder; baseEncoder.setUpperCase(true); } } public void setEncoder(int encodingType, int codepage ) { CodeFunctions cfuncs = CodeFunctions.createEncoderForLBL(encodingType, codepage); lblHeader.setEncodingType(cfuncs.getEncodingType()); textEncoder = cfuncs.getEncoder(); } /** * Add a new label with the given text. Labels are shared, so that identical * text is always represented by the same label. * * @param text The text of the label, it will be in uppercase. * @return A reference to the created label. */ public Label newLabel(String text) { EncodedText encodedText = textEncoder.encodeText(text); Label l = labelCache.get(encodedText); if (l == null) { l = new Label(encodedText.getChars()); labelCache.put(encodedText, l); l.setOffset(getNextLabelOffset()); l.write(getWriter(), encodedText); alignForNext(); if (l.getOffset() > 0x3fffff) throw new MapFailedException("Overflow of LBL section"); } return l; } /** * Align for the next label. * * Only has any effect when offsetMultiplier is not zero. */ private void alignForNext() { // Align ready for next label while ((getCurrentLabelOffset() & ((1 << offsetMultiplier) - 1)) != 0) getWriter().put((byte) 0); } private int getNextLabelOffset() { return getCurrentLabelOffset() >> offsetMultiplier; } private int getCurrentLabelOffset() { return position() - (LBLHeader.HEADER_LEN + lblHeader.getSortDescriptionLength()); } public POIRecord createPOI(String name) { return places.createPOI(name); } public POIRecord createExitPOI(String name, Exit exit) { return places.createExitPOI(name, exit); } public POIIndex createPOIIndex(String name, int poiIndex, Subdivision group, int type) { return places.createPOIIndex(name, poiIndex, group, type); } public Country createCountry(String name, String abbr) { return places.createCountry(name, abbr); } public Region createRegion(Country country, String region, String abbr) { return places.createRegion(country, region, abbr); } public City createCity(Region region, String city, boolean unique) { return places.createCity(region, city, unique); } public City createCity(Country country, String city, boolean unique) { return places.createCity(country, city, unique); } public Zip createZip(String code) { return places.createZip(code); } public Highway createHighway(Region region, String name) { return places.createHighway(region, name); } public ExitFacility createExitFacility(int type, char direction, int facilities, String description, boolean last) { return places.createExitFacility(type, direction, facilities, description, last); } public void allPOIsDone() { places.allPOIsDone(); } public void setSort(Sort sort) { this.sort = sort; lblHeader.setSort(sort); places.setSort(sort); } public int numCities() { return places.numCities(); } public int numZips() { return places.numZips(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/lbl/Highway.java0000644000076400007640000000464011400726372022633 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 1, 2008 */ package uk.me.parabola.imgfmt.app.lbl; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.trergn.Subdivision; /** * A highway is in a region. * * @author Mark Burton */ public class Highway { class ExitPoint implements Comparable { final String name; final byte index; final Subdivision div; public ExitPoint(String name, byte index, Subdivision div) { this.name = name; this.index = index; this.div = div; } public int compareTo(ExitPoint o) { return name.compareTo(o.name); } } private final int index; private final Region region; private final List exits = new ArrayList(); private Label label; private int extraDataOffset; // in 3-byte records - 1 based public Highway(Region region, int index) { this.region = region; this.index = index; } void write(ImgFileWriter writer, boolean extraData) { if(extraData) { writer.put((byte)0); writer.putChar(region == null? 0 : region.getIndex()); Collections.sort(exits); for(ExitPoint ep : exits) { writer.put(ep.index); writer.putChar((char)ep.div.getNumber()); } } else { assert extraDataOffset != 0; writer.put3(label.getOffset()); writer.putChar((char)extraDataOffset); writer.put((byte)0); // unknown (setting any of 0x3f stops exits being found) } } public int getIndex() { return index; } public void setLabel(Label label) { this.label = label; } public void setExtraDataOffset(int extraDataOffset) { this.extraDataOffset = extraDataOffset / 3 + 1; } public int getExtraDataSize() { return (1 + exits.size()) * 3; } public void addExitPoint(String name, int index, Subdivision div) { exits.add(new ExitPoint(name, (byte)index, div)); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Area.java0000644000076400007640000001560312306023172021325 0ustar stevesteve/* * 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; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.log.Logger; /** * A map area in map units. There is a constructor available for creating * in lat/long form. * * @author Steve Ratcliffe */ public class Area { private static final Logger log = Logger.getLogger(Area.class); private final int minLat; private final int minLong; private final int maxLat; private final int maxLong; /** * Create an area from the given coordinates. We ensure that no dimension * is zero. * * @param minLat The western latitude. * @param minLong The southern longitude. * @param maxLat The eastern lat. * @param maxLong The northern long. */ public Area(int minLat, int minLong, int maxLat, int maxLong) { this.minLat = minLat; if (maxLat == minLat) this.maxLat = minLat+1; else this.maxLat = maxLat; this.minLong = minLong; if (minLong == maxLong) this.maxLong = maxLong+1; else this.maxLong = maxLong; } public Area(double minLat, double minLong, double maxLat, double maxLong) { this(Utils.toMapUnit(minLat), Utils.toMapUnit(minLong) , Utils.toMapUnit(maxLat), Utils.toMapUnit(maxLong)); } public int getMinLat() { return minLat; } public int getMinLong() { return minLong; } public int getMaxLat() { return maxLat; } public int getMaxLong() { return maxLong; } public int getWidth() { return maxLong - minLong; } public int getHeight() { return maxLat - minLat; } public Coord getCenter() { return new Coord((minLat + maxLat)/2, (minLong + maxLong)/2);// high prec not needed } public String toString() { return "(" + Utils.toDegrees(minLat) + ',' + Utils.toDegrees(minLong) + ") to (" + Utils.toDegrees(maxLat) + ',' + Utils.toDegrees(maxLong) + ')' ; } /** * Split this area up into a number of smaller areas. * * @param xsplit The number of pieces to split this area into in the x * direction. * @param ysplit The number of pieces to split this area into in the y * direction. * @return An area containing xsplit*ysplit areas. */ public Area[] split(int xsplit, int ysplit) { Area[] areas = new Area[xsplit * ysplit]; int xsize = getWidth() / xsplit; int ysize = getHeight() / ysplit; int xextra = getWidth() - xsize * xsplit; int yextra = getHeight() - ysize * ysplit; for (int x = 0; x < xsplit; x++) { int xstart = minLong + x * xsize; int xend = xstart + xsize; if (x == xsplit - 1) xend += xextra; for (int y = 0; y < ysplit; y++) { int ystart = minLat + y * ysize; int yend = ystart + ysize; if (y == ysplit - 1) yend += yextra; Area a = new Area(ystart, xstart, yend, xend); log.debug(x, y, a); areas[x * ysplit + y] = a; } } assert areas.length == xsplit * ysplit; return areas; } /** * Get the largest dimension. So either the width or height, depending * on which is larger. * * @return The largest dimension in map units. */ public int getMaxDimension() { return Math.max(getWidth(), getHeight()); } /** * * @param co a coord * @return true if co is inside the Area (it may touch the boundary) */ public final boolean contains(Coord co) { int lat30 = co.getHighPrecLat(); int lon30 = co.getHighPrecLon(); return lat30 >= (minLat << Coord.DELTA_SHIFT) && lat30 <= (maxLat << Coord.DELTA_SHIFT) && lon30 >= (minLong << Coord.DELTA_SHIFT) && lon30 <= (maxLong << Coord.DELTA_SHIFT); } /** * * @param other an area * @return true if the other area is inside the Area (it may touch the boundary) */ public final boolean contains(Area other) { return other.getMinLat() >= minLat && other.getMaxLat() <= maxLat && other.getMinLong() >= minLong && other.getMaxLong() <= maxLong; } /** * @param co a coord * @return true if co is inside the Area and doesn't touch the boundary */ public final boolean insideBoundary(Coord co) { int lat30 = co.getHighPrecLat(); int lon30 = co.getHighPrecLon(); return lat30 > (minLat << Coord.DELTA_SHIFT) && lat30 < (maxLat << Coord.DELTA_SHIFT) && lon30 > (minLong << Coord.DELTA_SHIFT) && lon30 < (maxLong << Coord.DELTA_SHIFT); } /** * * @param other an area * @return true if the other area is inside the Area and doesn't touch the boundary */ public final boolean insideBoundary(Area other) { return other.getMinLat() > minLat && other.getMaxLat() < maxLat && other.getMinLong() > minLong && other.getMaxLong() < maxLong; } /** * @param co * @return true if co is on the boundary */ public final boolean onBoundary(Coord co) { return contains(co) && !insideBoundary(co); } /** * Checks if this area intersects the given bounding box at least * in one point. * * @param bbox an area * @return true if this area intersects the bbox; * false else */ public final boolean intersects(Area bbox) { return minLat <= bbox.getMaxLat() && maxLat >= bbox.getMinLat() && minLong <= bbox.getMaxLong() && maxLong >= bbox.getMinLong(); } public boolean isEmpty() { return minLat >= maxLat || minLong >= maxLong; } /** * * @param coords a list of coord instances * @return false if any of the coords lies on or outside of this area */ public boolean allInsideBoundary(List coords) { for (Coord co : coords) { if (!insideBoundary(co)) return false; } return true; } public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Area area = (Area) o; if (maxLat != area.maxLat) return false; if (maxLong != area.maxLong) return false; if (minLat != area.minLat) return false; if (minLong != area.minLong) return false; return true; } public int hashCode() { int result = minLat; result = 31 * result + minLong; result = 31 * result + maxLat; result = 31 * result + maxLong; return result; } /** * @return list of coords that form the rectangle */ public List toCoords(){ List coords = new ArrayList(5); Coord start = new Coord(minLat, minLong); coords.add(start); Coord co = new Coord(minLat, maxLong); coords.add(co); co = new Coord(maxLat, maxLong); coords.add(co); co = new Coord(maxLat, minLong); coords.add(co); coords.add(start); return coords; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Coord.java0000644000076400007640000006076512651103761021542 0ustar stevesteve/* * 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: 11-Dec-2006 */ package uk.me.parabola.imgfmt.app; import java.util.ArrayList; import java.util.List; import java.util.Locale; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.mkgmap.filters.ShapeMergeFilter; /** * A point coordinate in unshifted map-units. * A map unit is 360/2^24 degrees. In some places shifted coordinates * are used, which means that they are divided by some power of two to save * space in the file. * * You can create one of these with lat/long by calling the constructor with * double args. * * See also http://www.movable-type.co.uk/scripts/latlong.html * * @author Steve Ratcliffe */ public class Coord implements Comparable { private final static short ON_BOUNDARY_MASK = 0x0001; // bit in flags is true if point lies on a boundary private final static short PRESERVED_MASK = 0x0002; // bit in flags is true if point should not be filtered out private final static short REPLACED_MASK = 0x0004; // bit in flags is true if point was replaced private final static short TREAT_AS_NODE_MASK = 0x0008; // bit in flags is true if point should be treated as a node private final static short FIXME_NODE_MASK = 0x0010; // bit in flags is true if a node with this coords has a fixme tag private final static short REMOVE_MASK = 0x0020; // bit in flags is true if this point should be removed private final static short VIA_NODE_MASK = 0x0040; // bit in flags is true if a node with this coords is the via node of a RestrictionRelation private final static short PART_OF_BAD_ANGLE = 0x0080; // bit in flags is true if point should be treated as a node private final static short PART_OF_SHAPE2 = 0x0100; // use only in ShapeMerger private final static short END_OF_WAY = 0x0200; // use only in WrongAngleFixer private final static short HOUSENUMBER_NODE = 0x0400; // start/end of house number interval private final static short ADDED_HOUSENUMBER_NODE = 0x0800; // node was added for house numbers public final static int HIGH_PREC_BITS = 30; public final static int DELTA_SHIFT = 6; public final static double R = 6378137.0; // Radius of earth as defined by WGS84 public final static double U = R * 2 * Math.PI; // circumference of earth (WGS84) private final int latitude; private final int longitude; private byte highwayCount; // number of highways that use this point private short flags; // further attributes private final byte latDelta; // delta to 30 bit lat value private final byte lonDelta; // delta to 30 bit lon value private final static byte MAX_DELTA = 16; // max delta abs value that is considered okay private short approxDistanceToDisplayedCoord = -1; /** * Construct from co-ordinates that are already in map-units. * @param latitude The latitude in map units. * @param longitude The longitude in map units. */ public Coord(int latitude, int longitude) { this.latitude = latitude; this.longitude = longitude; latDelta = lonDelta = 0; } /** * Construct from regular latitude and longitude. * @param latitude The latitude in degrees. * @param longitude The longitude in degrees. */ public Coord(double latitude, double longitude) { this.latitude = Utils.toMapUnit(latitude); this.longitude = Utils.toMapUnit(longitude); int lat30 = toBit30(latitude); int lon30 = toBit30(longitude); this.latDelta = (byte) ((this.latitude << 6) - lat30); this.lonDelta = (byte) ((this.longitude << 6) - lon30); // verify math assert (this.latitude << 6) - latDelta == lat30; assert (this.longitude << 6) - lonDelta == lon30; } private Coord (int lat, int lon, byte latDelta, byte lonDelta){ this.latitude = lat; this.longitude = lon; this.latDelta = latDelta; this.lonDelta = lonDelta; } public static Coord makeHighPrecCoord(int lat30, int lon30){ int lat24 = (lat30 + (1 << 5)) >> 6; int lon24 = (lon30 + (1 << 5)) >> 6; byte dLat = (byte) ((lat24 << 6) - lat30); byte dLon = (byte) ((lon24 << 6) - lon30); return new Coord(lat24,lon24,dLat,dLon); } /** * Construct from other coord instance, copies * the lat/lon values in high precision * @param other */ public Coord(Coord other) { this.latitude = other.latitude; this.longitude = other.longitude; this.latDelta = other.latDelta; this.lonDelta = other.lonDelta; this.approxDistanceToDisplayedCoord = other.approxDistanceToDisplayedCoord; } public int getLatitude() { return latitude; } public int getLongitude() { return longitude; } /** * @return the route node id */ public int getId() { return 0; } public int getHighwayCount() { return highwayCount; } /** * Increase the counter how many highways use this coord. */ public void incHighwayCount() { // don't let it wrap if(highwayCount < Byte.MAX_VALUE) ++highwayCount; } /** * Decrease the counter how many highways use this coord. */ public void decHighwayCount() { // don't let it wrap if(highwayCount > 0) --highwayCount; } /** * Resets the highway counter to 0. */ public void resetHighwayCount() { highwayCount = 0; } public boolean getOnBoundary() { return (flags & ON_BOUNDARY_MASK) != 0; } public void setOnBoundary(boolean onBoundary) { if (onBoundary) this.flags |= ON_BOUNDARY_MASK; else this.flags &= ~ON_BOUNDARY_MASK; } public boolean preserved() { return (flags & PRESERVED_MASK) != 0 || (flags & HOUSENUMBER_NODE) != 0; } public void preserved(boolean preserved) { if (preserved) this.flags |= PRESERVED_MASK; else this.flags &= ~PRESERVED_MASK; } /** * Returns if this coord was marked to be replaced in short arc removal. * @return True means the replacement has to be looked up. */ public boolean isReplaced() { return (flags & REPLACED_MASK) != 0; } /** * Mark a point as replaced in short arc removal process. * @param replaced true or false */ public void setReplaced(boolean replaced) { if (replaced) this.flags |= REPLACED_MASK; else this.flags &= ~REPLACED_MASK; } /** * Should this Coord be treated like a Garmin node in short arc removal? * The value has no meaning outside of short arc removal. * @return true if this coord should be treated like a Garmin node, else false */ public boolean isTreatAsNode() { return (flags & TREAT_AS_NODE_MASK) != 0; } /** * Mark the Coord to be treated like a Node in short arc removal * @param treatAsNode true or false */ public void setTreatAsNode(boolean treatAsNode) { if (treatAsNode) this.flags |= TREAT_AS_NODE_MASK; else this.flags &= ~TREAT_AS_NODE_MASK; } /** * Does this coordinate belong to a node with a fixme tag? * Note that the value is set after evaluating the points style. * @return true if the fixme flag is set, else false */ public boolean isFixme() { return (flags & FIXME_NODE_MASK) != 0; } public void setFixme(boolean b) { if (b) this.flags |= FIXME_NODE_MASK; else this.flags &= ~FIXME_NODE_MASK; } public boolean isToRemove() { return (flags & REMOVE_MASK) != 0; } public void setRemove(boolean b) { if (b) this.flags |= REMOVE_MASK; else this.flags &= ~REMOVE_MASK; } /** * @return true if this coordinate belong to a via node of a restriction relation */ public boolean isViaNodeOfRestriction() { return (flags & VIA_NODE_MASK) != 0; } /** * @param b true: Mark the coordinate as via node of a restriction relation */ public void setViaNodeOfRestriction(boolean b) { if (b) this.flags |= VIA_NODE_MASK; else this.flags &= ~VIA_NODE_MASK; } /** * Should this Coord be treated by the removeWrongAngle method= * The value has no meaning outside of StyledConverter. * @return true if this coord is part of a line that has a big bearing error. */ public boolean isPartOfBadAngle() { return (flags & PART_OF_BAD_ANGLE) != 0; } /** * Mark the Coord to be part of a line which has a big bearing * error because of the rounding to map units. * @param b true or false */ public void setPartOfBadAngle(boolean b) { if (b) this.flags |= PART_OF_BAD_ANGLE; else this.flags &= ~PART_OF_BAD_ANGLE; } /** * Get flag for {@link ShapeMergeFilter} * The value has no meaning outside of {@link ShapeMergeFilter} * @return */ public boolean isPartOfShape2() { return (flags & PART_OF_SHAPE2) != 0; } /** * Set or unset flag for {@link ShapeMergeFilter} * @param b true or false */ public void setPartOfShape2(boolean b) { if (b) this.flags |= PART_OF_SHAPE2; else this.flags &= ~PART_OF_SHAPE2; } /** * Get flag for {@link WrongAngleFixer} * The value has no meaning outside of {@link WrongAngleFixer} * @return */ public boolean isEndOfWay() { return (flags & END_OF_WAY) != 0; } /** * Set or unset flag for {@link WrongAngleFixer} * @param b true or false */ public void setEndOfWay(boolean b) { if (b) this.flags |= END_OF_WAY; else this.flags &= ~END_OF_WAY; } /** * @return if this is the beginning/end of a house number interval */ public boolean isNumberNode(){ return (flags & HOUSENUMBER_NODE) != 0; } /** * @param b true or false */ public void setNumberNode(boolean b) { if (b) this.flags |= HOUSENUMBER_NODE; else this.flags &= ~HOUSENUMBER_NODE; } /** * @return if this is the beginning/end of a house number interval */ public boolean isAddedNumberNode(){ return (flags & ADDED_HOUSENUMBER_NODE) != 0; } /** * @param b true or false */ public void setAddedNumberNode(boolean b) { if (b) this.flags |= ADDED_HOUSENUMBER_NODE; else this.flags &= ~ADDED_HOUSENUMBER_NODE; } public int hashCode() { // Use a factor for latitude to span over the whole integer range: // max lat: 4194304 // max lon: 8388608 // max hashCode: 2118123520 < 2147483647 (Integer.MAX_VALUE) return 503 * latitude + longitude; } /** * Compares the coordinates that are displayed in the map */ public boolean equals(Object obj) { if (obj == null || !(obj instanceof Coord)) return false; Coord other = (Coord) obj; return latitude == other.latitude && longitude == other.longitude; } /** * Compares the coordinates using the delta values. * XXX: Note that * p1.highPrecEquals(p2) is not always equal to p1.equals(p2) * @param other * @return */ public boolean highPrecEquals(Coord other) { if (other == null) return false; if (this == other) return true; return getHighPrecLat() == other.getHighPrecLat() && getHighPrecLon() == other.getHighPrecLon(); } /** * Distance to other point in metres, using * "flat earth approximation" or rhumb-line algo */ public double distance(Coord other) { double d1 = U / 360 * Math.sqrt(distanceInDegreesSquared(other)); if (d1 < 10000) return d1; // error is below 0.01 m // for long distances, use more complex algorithm return distanceOnRhumbLine(other); } /** * Square of distance to other point in metres, using * "flat earth approximation" */ public double distanceInDegreesSquared(Coord other) { if (this == other || highPrecEquals(other)) return 0; double lat1 = getLatDegrees(); double lat2 = other.getLatDegrees(); double long1 = getLonDegrees(); double long2 = other.getLonDegrees(); double latDiff; if (lat1 < lat2) latDiff = lat2 - lat1; else latDiff = lat1 - lat2; if (latDiff > 90) latDiff -= 180; double longDiff; if (long1 < long2) longDiff = long2 - long1; else longDiff = long1 - long2; if (longDiff > 180) longDiff -= 360; // scale longDiff by cosine of average latitude longDiff *= Math.cos(Math.PI / 180 * Math.abs((lat1 + lat2) / 2)); return (latDiff * latDiff) + (longDiff * longDiff); } /** * Distance to other point in metres following a great circle path, without * flat earth approximation, slower but better with large * distances and big deltas in lat AND lon. * Similar to code in JOSM */ public double distanceHaversine (Coord point){ double lat1 = int30ToRadians(getHighPrecLat()); double lat2 = int30ToRadians(point.getHighPrecLat()); double lon1 = int30ToRadians(getHighPrecLon()); double lon2 = int30ToRadians(point.getHighPrecLon()); double sinMidLat = Math.sin((lat1-lat2)/2); double sinMidLon = Math.sin((lon1-lon2)/2); double dRad = 2*Math.asin(Math.sqrt(sinMidLat*sinMidLat + Math.cos(lat1)*Math.cos(lat2)*sinMidLon*sinMidLon)); double distance= dRad * R; return distance; } /** * Distance to other point in metres following the shortest rhumb line. */ public double distanceOnRhumbLine(Coord point){ double lat1 = int30ToRadians(getHighPrecLat()); double lat2 = int30ToRadians(point.getHighPrecLat()); double lon1 = int30ToRadians(getHighPrecLon()); double lon2 = int30ToRadians(point.getHighPrecLon()); // see http://williams.best.vwh.net/avform.htm#Rhumb double dLat = lat2 - lat1; double dLon = Math.abs(lon2 - lon1); // if dLon over 180° take shorter rhumb line across the anti-meridian: if (Math.abs(dLon) > Math.PI) dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon); // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor' // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it double deltaPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4)); double q = Math.abs(deltaPhi) > 10e-12 ? dLat/deltaPhi : Math.cos(lat1); // distance is pythagoras on 'stretched' Mercator projection double distRad = Math.sqrt(dLat*dLat + q*q*dLon*dLon); // angular distance in radians double dist = distRad * R; return dist; } /** * Calculate point on the line this->other. If d is the distance between this and other, * the point is {@code fraction * d} metres from this. * For small distances between this and other we use a flat earth approximation, * for large distances this could result in errors of many metres, so we use * the rhumb line calculations. */ public Coord makeBetweenPoint(Coord other, double fraction) { int dLat30 = other.getHighPrecLat() - getHighPrecLat(); int dLon30 = other.getHighPrecLon() - getHighPrecLon(); if (dLon30 == 0 || Math.abs(dLat30) < 1000000 && Math.abs(dLon30) < 1000000 ){ // distances are rather small, we can use flat earth approximation int lat30 = (int) (getHighPrecLat() + dLat30 * fraction); int lon30 = (int) (getHighPrecLon() + dLon30 * fraction); return makeHighPrecCoord(lat30, lon30); } double brng = this.bearingToOnRhumbLine(other, true); double dist = this.distance(other) * fraction; return this.destOnRhumLine(dist, brng); } /** * returns bearing (in degrees) from current point to another point * following a rhumb line */ public double bearingTo(Coord point) { return bearingToOnRhumbLine(point, false); } /** * returns bearing (in degrees) from current point to another point * following a great circle path * @param point the other point * @param needHighPrec set to true if you need a very high precision */ public double bearingToOnGreatCircle(Coord point, boolean needHighPrec) { // use high precision values for this double lat1 = int30ToRadians(getHighPrecLat()); double lat2 = int30ToRadians(point.getHighPrecLat()); double lon1 = int30ToRadians(getHighPrecLon()); double lon2 = int30ToRadians(point.getHighPrecLon()); double dlon = lon2 - lon1; double y = Math.sin(dlon) * Math.cos(lat2); double x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dlon); double brngRad = needHighPrec ? Math.atan2(y, x) : Utils.atan2_approximation(y, x); return brngRad * 180 / Math.PI; } /** * returns bearing (in degrees) from current point to another point * following shortest rhumb line * @param point the other point * @param needHighPrec set to true if you need a very high precision */ public double bearingToOnRhumbLine(Coord point, boolean needHighPrec){ double lat1 = int30ToRadians(this.getHighPrecLat()); double lat2 = int30ToRadians(point.getHighPrecLat()); double lon1 = int30ToRadians(this.getHighPrecLon()); double lon2 = int30ToRadians(point.getHighPrecLon()); double dLon = lon2-lon1; // if dLon over 180° take shorter rhumb line across the anti-meridian: if (Math.abs(dLon) > Math.PI) dLon = dLon>0 ? -(2*Math.PI-dLon) : (2*Math.PI+dLon); double deltaPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4)); double brngRad = needHighPrec ? Math.atan2(dLon, deltaPhi) : Utils.atan2_approximation(dLon, deltaPhi); return brngRad * 180 / Math.PI; } /** * Sort lexicographically by longitude, then latitude. * * This ordering is used for sorting entries in NOD3. */ public int compareTo(Coord other) { if (longitude == other.getLongitude()) { if (latitude == other.getLatitude()) return 0; return latitude > other.getLatitude() ? 1 : -1; } return longitude > other.getLongitude() ? 1 : -1; } /** * Returns a string representation of the object. * * @return a string representation of the object. */ public String toString() { return (latitude) + "/" + (longitude); } public String toDegreeString() { return String.format(Locale.ENGLISH, "%.6f,%.6f", getLatDegrees(), getLonDegrees()); } protected String toOSMURL(int zoom) { return ("http://www.openstreetmap.org/?mlat=" + String.format(Locale.ENGLISH, "%.6f", getLatDegrees()) + "&mlon=" + String.format(Locale.ENGLISH, "%.6f", getLonDegrees()) + "&zoom=" + zoom); } public String toOSMURL() { return toOSMURL(17); } /** * Convert latitude or longitude to 30 bits value. * This allows higher precision than the 24 bits * used in map units. * @param l The lat or long as decimal degrees. * @return An integer value with 30 bit precision. */ private static int toBit30(double l) { double DELTA = 360.0D / (1 << 30) / 2; //Correct rounding if (l > 0) return (int) ((l + DELTA) * (1 << 30)/360); return (int) ((l - DELTA) * (1 << 30)/360); } /* Factor for conversion to radians using 30 bits * (Math.PI / 180) * (360.0 / (1 << 30)) */ final static double BIT30_RAD_FACTOR = 2 * Math.PI / (1 << 30); /** * Convert to radians using high precision * @param val30 a longitude/latitude value with 30 bit precision * @return an angle in radians. */ public static double int30ToRadians(int val30){ return BIT30_RAD_FACTOR * val30; } /** * @return Latitude as signed 30 bit integer */ public int getHighPrecLat() { return (latitude << 6) - latDelta; } /** * @return Longitude as signed 30 bit integer */ public int getHighPrecLon() { return (longitude << 6) - lonDelta; } /** * @return latitude in degrees with highest avail. precision */ public double getLatDegrees(){ return (360.0D / (1 << 30)) * getHighPrecLat(); } /** * @return longitude in degrees with highest avail. precision */ public double getLonDegrees(){ return (360.0D / (1 << 30)) * getHighPrecLon(); } public Coord getDisplayedCoord(){ return new Coord(latitude,longitude); } public boolean hasAlternativePos(){ if (getOnBoundary()) return false; return (Math.abs(latDelta) > MAX_DELTA || Math.abs(lonDelta) > MAX_DELTA); } /** * Calculate up to three points with equal * high precision coordinate, but * different map unit coordinates. * @return a list of Coord instances, is empty if alternative positions are too far */ public List getAlternativePositions(){ ArrayList list = new ArrayList<>(); if (getOnBoundary()) return list; byte modLatDelta = 0; byte modLonDelta = 0; int modLat = latitude; int modLon = longitude; if (latDelta > MAX_DELTA) modLat--; else if (latDelta < -MAX_DELTA) modLat++; if (lonDelta > MAX_DELTA) modLon--; else if (lonDelta < -MAX_DELTA) modLon++; int lat30 = getHighPrecLat(); int lon30 = getHighPrecLon(); modLatDelta = (byte) ((modLat<<6) - lat30); modLonDelta = (byte) ((modLon<<6) - lon30); assert modLatDelta >= -63 && modLatDelta <= 63; assert modLonDelta >= -63 && modLonDelta <= 63; if (modLat != latitude){ if (modLon != longitude) list.add(new Coord(modLat, modLon, modLatDelta, modLonDelta)); list.add(new Coord(modLat, longitude, modLatDelta, lonDelta)); } if (modLon != longitude) list.add(new Coord(latitude, modLon, latDelta, modLonDelta)); /* verify math for(Coord co:list){ double d = distance(new Coord (co.getLatitude(),co.getLongitude())); assert d < 3.0; } */ return list; } /** * @return approximate distance in cm */ public short getDistToDisplayedPoint(){ if (approxDistanceToDisplayedCoord < 0){ approxDistanceToDisplayedCoord = (short)Math.round(getDisplayedCoord().distance(this)*100); } return approxDistanceToDisplayedCoord; } /** * Get the coord that is {@code dist} metre away travelling with course * {@code brng} on a rhumb-line. * @param dist distance in m * @param brng bearing in degrees * @return a new Coord instance */ public Coord destOnRhumLine(double dist, double brng){ double distRad = dist / R; // angular distance in radians double lat1 = int30ToRadians(this.getHighPrecLat()); double lon1 = int30ToRadians(this.getHighPrecLon()); double brngRad = Math.toRadians(brng); double deltaLat = distRad * Math.cos(brngRad); double lat2 = lat1 + deltaLat; // check for some daft bugger going past the pole, normalise latitude if so if (Math.abs(lat2) > Math.PI/2) lat2 = lat2>0 ? Math.PI-lat2 : -Math.PI-lat2; double lon2; // catch special case: normalised value would be -8388608 if (this.getLongitude() == 8388608 && brng == 0) lon2 = lon1; else { double deltaPhi = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4)); double q = Math.abs(deltaPhi) > 10e-12 ? deltaLat / deltaPhi : Math.cos(lat1); // E-W course becomes ill-conditioned with 0/0 double deltaLon = distRad*Math.sin(brngRad)/q; lon2 = lon1 + deltaLon; lon2 = (lon2 + 3*Math.PI) % (2*Math.PI) - Math.PI; // normalise to -180..+180º } return new Coord(Math.toDegrees(lat2), Math.toDegrees(lon2)); } /** * Calculate the distance in metres to the rhumb line * defined by coords a and b. * @param a start point * @param b end point * @return perpendicular distance in m. */ public double distToLineSegment(Coord a, Coord b){ double ap = a.distance(this); double ab = a.distance(b); double bp = b.distance(this); if (ap == 0 || bp == 0) return 0; double abpa = (ab+ap+bp)/2; double dx = abpa-ab; double dist; if (dx < 0){ // simple calculation using Herons formula will fail // calculate x, the point on line a-b which is as far away from a as this point double b_ab = a.bearingToOnRhumbLine(b, true); Coord x = a.destOnRhumLine(ap, b_ab); // this dist between these two points is not exactly // the perpendicul distance, but close enough dist = x.distance(this); } else dist = 2 * Math.sqrt(abpa * (abpa-ab) * (abpa-ap) * (abpa-bp)) / ab; return dist; } /** * Calculate distance to rhumb line segment a-b * @param a point a * @param b point b * @return distance in m */ public double shortestDistToLineSegment(Coord a, Coord b){ int aLon = a.getHighPrecLon(); int bLon = b.getHighPrecLon(); int pLon = this.getHighPrecLon(); int aLat = a.getHighPrecLat(); int bLat = b.getHighPrecLat(); int pLat = this.getHighPrecLat(); double deltaLon = bLon - aLon; double deltaLat = bLat - aLat; double frac; if (deltaLon == 0 && deltaLat == 0){ frac = 0; } else { // scale for longitude deltas by cosine of average latitude double scale = Math.cos(Coord.int30ToRadians((aLat + bLat + pLat) / 3) ); double deltaLonAP = scale * (pLon - aLon); deltaLon = scale * deltaLon; if (deltaLon == 0 && deltaLat == 0) frac = 0; else frac = (deltaLonAP * deltaLon + (pLat - aLat) * deltaLat) / (deltaLon * deltaLon + deltaLat * deltaLat); } double distance; if (frac <= 0) { distance = a.distance(this); } else if (frac >= 1) { distance = b.distance(this); } else { distance = this.distToLineSegment(a, b); } return distance; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/Writeable.java0000644000076400007640000000033311713757443022405 0ustar stevestevepackage uk.me.parabola.imgfmt.app; /** * Interface that can be implemented by objects that write to an ImgFile. * @author Thomas Lußnig */ public interface Writeable { public void write(ImgFileWriter writer); } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/BitReader.java0000644000076400007640000000545712266537315022342 0ustar stevesteve/* * Copyright (C) 2008 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: 29-Aug-2008 */ package uk.me.parabola.imgfmt.app; /** * Read an array as a bit stream. * * @author Steve Ratcliffe */ public class BitReader { private final byte[] buf; private int bitPosition; public BitReader(byte[] buf) { this.buf = buf; } public boolean get1() { int off = bitPosition % 8; byte b = buf[bitPosition / 8]; bitPosition++; return ((b >> off) & 1) == 1; } public int get(int n) { int res = 0; int pos = 0; while (pos < n) { int index = bitPosition / 8; int off = bitPosition % 8; byte b = buf[index]; b >>= off; int nbits = n - pos; if (nbits > (8-off)) nbits = 8 - off; int mask = ((1 << nbits) - 1); res |= ((b & mask) << pos); pos += nbits; bitPosition += nbits; } return res; } /** * Get a signed quantity. * * The sign bit is the last bit in the field. * * @param n The field width, including the sign bit. * @return A signed number. */ public int sget(int n) { int res = get(n); int top = 1 << (n - 1); if ((res & top) != 0) { int mask = top - 1; res = ~mask | res; } return res; } /** * Get a signed n-bit value, treating 1 << (n-1) as a flag to read another signed n-bit value * for extended range. */ public int sget2(int n) { int top = 1 << (n - 1); int mask = top - 1; int base = 0; int res = get(n); while (res == top) { // Add to the base value, and read another base += mask; res = get(n); } // The final byte determines the sign of the result. Add or subtract the base as // appropriate. if ((res & top) == 0) res += base; else res = (res | ~mask) - base; // Make negative and subtract the base return res; } public int getBitPosition() { return bitPosition; } public int getNumberOfBits() { return buf.length * 8; } /** * Debugging routine that returns the remainder of the stream as a string. * The bits are in little endian order, so that numbers can be read from left * to right, although the whole string has to read from right to left. * * @return A string in binary. */ public String remainder() { int save = bitPosition; StringBuilder sb = new StringBuilder(); while (bitPosition < buf.length * 8) { sb.insert(0, get1() ? "1" : "0"); } bitPosition = save; return sb.toString(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/BufferedImgFileReader.java0000644000076400007640000001516312433605560024570 0ustar stevesteve/* * Copyright (C) 2007 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: Dec 14, 2007 */ package uk.me.parabola.imgfmt.app; import java.io.IOException; import java.nio.ByteBuffer; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * Read from an img file via a buffer. * * @author Steve Ratcliffe */ public class BufferedImgFileReader implements ImgFileReader { private static final Logger log = Logger.getLogger(BufferedImgFileReader.class); // Buffer size, must be a power of 2 private static final int BUF_SIZE = 0x1000; private final ImgChannel chan; // The buffer that we read out of private final ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE); private long bufStart; private int bufSize = -1; // We keep our own idea of the file position. private long position; public BufferedImgFileReader(ImgChannel chan) { this.chan = chan; } /** * Called when the stream is closed. Any resources can be freed. * * @throws IOException When there is an error in closing. */ public void close() throws IOException { chan.close(); } /** * Get the position. Needed because may not be reflected in the underlying * file if being buffered. * * @return The logical position within the file. */ public long position() { return position; } /** * Set the position of the file. * * @param pos The new position in the file. */ public void position(long pos) { position = pos; } /** * Read in a single byte from the current position. * * @return The byte that was read. */ public byte get() throws ReadFailedException { // Check if the current position is within the buffer fillBuffer(); int pos = (int) (position - bufStart); if (pos >= bufSize) return 0; // XXX do something else position++; return buf.get(pos); } /** * Read in two bytes. Done in the correct byte order. * * @return The 2 byte integer that was read. */ public char getChar() throws ReadFailedException { // Slow but sure implementation byte b1 = get(); byte b2 = get(); return (char) (((b2 & 0xff) << 8) + (b1 & 0xff)); } /** * Read a three byte signed quantity. * @return The read value. * @throws ReadFailedException */ public int get3() throws ReadFailedException { // Slow but sure implementation byte b1 = get(); byte b2 = get(); byte b3 = get(); return (b1 & 0xff) | ((b2 & 0xff) << 8) | (b3 << 16) ; } public int getu3() throws ReadFailedException { return get3() & 0xffffff; } /** * Read in a 4 byte value. * * @return A 4 byte integer. */ public int getInt() throws ReadFailedException { // Slow but sure implementation byte b1 = get(); byte b2 = get(); byte b3 = get(); byte b4 = get(); return (b1 & 0xff) | ((b2 & 0xff) << 8) | ((b3 & 0xff) << 16) | ((b4 & 0xff) << 24) ; } public int getUint(int n) throws ReadFailedException { switch (n) { case 1: return get() & 0xff; case 2: return getChar(); case 3: return getu3(); case 4: return getInt(); default: // this is a programming error so exit throw new MapFailedException("bad integer size " + n); } } /** * Read in an arbitrary length sequence of bytes. * * @param len The number of bytes to read. */ public byte[] get(int len) throws ReadFailedException { byte[] bytes = new byte[len]; // Slow but sure implementation. for (int i = 0; i < len; i++) { bytes[i] = get(); } return bytes; } /** * Read a zero terminated string from the file. * @return A string * @throws ReadFailedException For failures. */ public String getZString() throws ReadFailedException { StringBuffer sb = new StringBuffer(); // Slow but sure implementation. for (byte b = get(); b != 0; b = get()) { sb.append((char) b); } return sb.toString(); } /** * Read in a string of digits in the compressed base 11 format that is used * for phone numbers in the POI section. * @param delimiter This will replace all digit 11 characters. Usually a * '-' to separate numbers in a telephone. No doubt there is a different * standard in each country. * @return A phone number possibly containing the delimiter character. */ public String getBase11str(byte firstChar, char delimiter) { // NB totally untested. StringBuilder str11 = new StringBuilder(); int term = 2; int ch = firstChar & 0xff; do { assert !(str11.length() == 0 && (ch & 0x80) == 0); if ((ch & 0x80) != 0) --term; str11.append(base(ch & 0x7F, 11, 2)); if (term != 0) ch = get(); } while (term != 0); // Remove any trailing delimiters while (str11.length() > 0 && str11.charAt(str11.length()-1) == 'A') str11.setLength(str11.length()-1); // Convert in-line delimiters to the char delimiter int len = str11.length(); for (int i = 0; i < len; i++) { if (str11.charAt(i) == 'A') str11.setCharAt(i, delimiter); } return str11.toString(); } private String base(int inNum, int base, int width) { int num = inNum; StringBuilder val = new StringBuilder(); if (base < 2 || base > 36 || width < 1) return ""; String digit = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; while (num != 0) { val.append(digit.charAt(num % base)); num /= base; } while (val.length() < width) val.append('0'); val.reverse(); return val.toString(); } /** * Check to see if the buffer contains the byte at the current position. * If not then it is re-read so that it does. * * @throws ReadFailedException If the buffer needs filling and the file cannot be * read. */ private void fillBuffer() throws ReadFailedException { // If we are no longer inside the buffer, then re-read it. if (position < bufStart || position >= bufStart + bufSize) { // Get channel position on a block boundary. bufStart = position & ~(BUF_SIZE - 1); chan.position(bufStart); log.debug("reading in a buffer start=", bufStart); // Fill buffer buf.clear(); bufSize = 0; try { bufSize = chan.read(buf); } catch (IOException e) { throw new ReadFailedException("failed to fill buffer", e); } log.debug("there were", bufSize, "bytes read"); } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/CoordNode.java0000644000076400007640000000270512533237270022340 0ustar stevesteve/* * Copyright (C) 2008 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: 13-Jul-2008 */ package uk.me.parabola.imgfmt.app; /** * A coordinate that is known to be a routing node. You can tell by the fact * that getId() returns != 0. * * @author Steve Ratcliffe */ public class CoordNode extends Coord { private final int id; /** * Construct from co-ordinates that are already in map-units. * * @param latitude The latitude in map units. * @param longitude The longitude in map units. * @param id The ID of this routing node. * @param boundary This is a routing node on the boundary. */ public CoordNode(int latitude, int longitude, int id, boolean boundary) { super(latitude, longitude); this.id = id; setOnBoundary(boundary); setNumberNode(true); preserved(true); } public CoordNode(Coord other, int id, boolean boundary){ super(other); this.id = id; setOnBoundary(boundary); setNumberNode(true); preserved(true); } public int getId() { return id; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/SectionWriter.java0000644000076400007640000000374511642347335023276 0ustar stevesteve/* * Copyright (C) 2008 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: 19-Jul-2008 */ package uk.me.parabola.imgfmt.app; import java.io.IOException; import java.nio.ByteBuffer; /** * A section writer wraps a regular writer so that all the offsets * are relative to the start of a section. * * @author Steve Ratcliffe */ public class SectionWriter implements ImgFileWriter { private final ImgFileWriter writer; private final Section section; private final int secStart; public SectionWriter(ImgFileWriter writer, Section section) { this.writer = writer; this.secStart = section.getPosition(); this.section = section; } public void sync() throws IOException { writer.sync(); } /** * Note that this does not close the underlying file. */ public void close() { if (section != null) section.setSize(writer.position() - secStart); //writer.close(); } public int position() { return writer.position() - secStart; } public void position(long pos) { writer.position(pos + secStart); } public void put(byte b) { writer.put(b); } public void putChar(char c) { writer.putChar(c); } public void put3(int val) { writer.put3(val); } public void putInt(int val) { writer.putInt(val); } public void put(byte[] val) { writer.put(val); } public void put(byte[] src, int start, int length) { writer.put(src, start, length); } public void put(ByteBuffer src) { writer.put(src); } public long getSize() { throw new UnsupportedOperationException("Cannot get size at this point"); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/BitWriter.java0000644000076400007640000000770712102024715022374 0ustar stevesteve/* * 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: 14-Dec-2006 */ package uk.me.parabola.imgfmt.app; import uk.me.parabola.log.Logger; /** * A class to write the bitstream. * * @author Steve Ratcliffe */ public class BitWriter { private static final Logger log = Logger.getLogger(BitWriter.class); // Choose so that most roads will not fill it. private static final int INITIAL_BUF_SIZE = 20; // The byte buffer and its current length (allocated length) private byte[] buf; // The buffer private int bufsize; // The allocated size private int buflen; // The actual used length // The bit offset into the byte array. private int bitoff; private static final int BUFSIZE_INC = 50; public BitWriter() { bufsize = INITIAL_BUF_SIZE; buf = new byte[bufsize]; } /** * Put exactly one bit into the buffer. * * @param b The bottom bit of the integer is set at the current bit position. */ private void put1(int b) { ensureSize(bitoff + 1); int off = getByteOffset(bitoff); // Get the remaining bits into the byte. int rem = bitoff - 8 * off; // Or it in, we are assuming that the position is never turned back. buf[off] |= (b & 0x1) << rem; // Increment position bitoff++; // If we are in a new byte, increase the byte length. if ((bitoff & 0x7) == 1) buflen++; debugPrint(b, 1); } public void put1(boolean b) { put1(b ? 1 : 0); } /** * Put a number of bits into the buffer, growing it if necessary. * * @param bval The bits to add, the lowest n bits will be added to * the buffer. * @param nb The number of bits. */ public void putn(int bval, int nb) { int val = bval & ((1<= 24) throw new IllegalArgumentException(); ensureSize(bitoff + n); // Get each affected byte and set bits into it until we are done. while (n > 0) { int ind = getByteOffset(bitoff); int rem = bitoff - 8*ind; buf[ind] |= ((val << rem) & 0xff); // Shift down in preparation for next byte. val >>>= 8-rem; // Account for change so far int nput = 8 - rem; if (nput > n) nput = n; bitoff += nput; n -= nput; } buflen = (bitoff+7)/8; } public byte[] getBytes() { return buf; } /** * Get the number of bytes actually used to hold the bit stream. This therefore can be and usually * is less than the length of the buffer returned by {@link #getBytes()}. * @return Number of bytes required to hold the output. */ public int getLength() { return buflen; } /** * Get the byte offset for the given bit number. * * @param boff The number of the bit in question. * @return The index into the byte array where the bit resides. */ private int getByteOffset(int boff) { return boff/8; } /** * Set everything up so that the given size can be accommodated. * The buffer is re-sized if necessary. * * @param newlen The new length of the bit buffer in bits. */ private void ensureSize(int newlen) { if (newlen/8 >= bufsize) reallocBuffer(); } /** * Reallocate the byte buffer. */ private void reallocBuffer() { log.debug("reallocating buffer"); bufsize += BUFSIZE_INC; byte[] newbuf = new byte[bufsize]; System.arraycopy(this.buf, 0, newbuf, 0, this.buf.length); this.buf = newbuf; } private void debugPrint(int b, int i) { if (log.isDebugEnabled()) log.debug("after put", i, "of", b, " bufsize=", bufsize, ", len=", buflen, ", pos=", bitoff); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/0000755000076400007640000000000012651103763020403 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RouteArc.java0000644000076400007640000003110712613617341022774 0ustar stevesteve/* * Copyright (C) 2008 * * 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. * * Create date: 07-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.log.Logger; /** * An arc joins two nodes within a {@link RouteCenter}. This may be renamed * to a Segment. * The arc also references the road that it is a part of. * * There are also links between nodes in different centers. * * @author Steve Ratcliffe */ public class RouteArc { private static final Logger log = Logger.getLogger(RouteArc.class); // Flags A private static final int FLAG_HASNET = 0x80; private static final int FLAG_FORWARD = 0x40; private static final int MASK_DESTCLASS = 0x7; public static final int MASK_CURVE_LEN = 0x38; // Flags B private static final int FLAG_LAST_LINK = 0x80; private static final int FLAG_EXTERNAL = 0x40; private int offset; // heading / bearing: private float initialHeading; // degrees (A-> B in an arc ABCD) private final float directHeading; // degrees (A-> D in an arc ABCD) private final RoadDef roadDef; // The nodes that this arc comes from and goes to private final RouteNode source; private final RouteNode dest; // The index in Table A describing this arc. private byte indexA; // The index in Table B that this arc goes via, if external. private byte indexB; private byte flagA; private byte flagB; private final boolean haveCurve; private final int length; private final byte lengthRatio; private final int pointsHash; private boolean isForward; private final float lengthInMeter; private boolean isDirect; // this field may limit the arcs destination class private byte maxDestClass = -1; // pointer to the reverse arc if this is a direct arc private RouteArc reverseArc; /** * Create a new arc. An arc can contain multiple points (eg. A->B->C->D->E) * @param roadDef The road that this arc segment is part of. * @param source The source node. (A) * @param dest The destination node (E). * @param initialBearing The initial heading (signed degrees) (A->B) * @param directBearing The direct heading (signed degrees) (A->E) * @param arcLength the length of the arc in meter (A->B->C->D->E) * @param pathLength the length of the arc in meter (summed length for additional arcs) * @param directLength the length of the arc in meter (A-E) * @param pointsHash */ public RouteArc(RoadDef roadDef, RouteNode source, RouteNode dest, double initialBearing, double directBearing, double arcLength, double pathLength, double directLength, int pointsHash) { this.isDirect = true; // unless changed this.roadDef = roadDef; this.source = source; this.dest = dest; this.initialHeading = (float) initialBearing; this.directHeading = (directBearing < 180) ? (float) directBearing : -180.0f; int len = NODHeader.metersToRaw(arcLength); if (len >= (1 << 22)) { log.error("Way " + roadDef.getName() + " (id " + roadDef.getId() + ") contains an arc whose length (" + len + " units) is too big to be encoded, using length",((1 << 22) - 1)); len = (1 << 22) - 1; } this.length = len; this.lengthInMeter = (float) arcLength; this.pointsHash = pointsHash; // calculate length ratio between way length on road and direct distance between end points int ratio = 0; int pathEncoded = NODHeader.metersToRaw(pathLength); if (pathEncoded >= 2){ int directEncoded = NODHeader.metersToRaw(directLength); if (pathEncoded > directEncoded){ ratio = (int) Math.min(31, Math.round(32.0d * directLength/ pathLength)); } } if (ratio == 0 && len >= (1 << 14)) ratio = 0x1f; // force encoding of curve info for very long arcs lengthRatio = (byte)ratio; haveCurve = lengthRatio > 0; } public float getInitialHeading() { return initialHeading; } public float getDirectHeading() { return directHeading; } public void setInitialHeading(float ih) { initialHeading = ih; } public float getFinalHeading() { float fh = 0; if (lengthInMeter != 0){ fh = getReverseArc().getInitialHeading(); fh = (fh <= 0) ? 180.0f + fh : -(180.0f - fh) % 180.0f; } return fh; } public RouteNode getSource() { return source; } public RouteNode getDest() { return dest; } public int getLength() { return length; } public int getPointsHash() { return pointsHash; } /** * Provide an upper bound for the written size in bytes. */ public int boundSize() { int[] lendat = encodeLength(); //TODO: take into account that initialHeading and indexA are not always written // 1 (flagA) + 1-2 (offset) + 1 (indexA) + 1 (initialHeading) int size = 5 + lendat.length; if(haveCurve) size += encodeCurve().length; return size; } /** * Is this an arc within the RouteCenter? */ public boolean isInternal() { // we might check that setInternal has been called before return (flagB & FLAG_EXTERNAL) == 0; } public void setInternal(boolean internal) { if (internal) flagB &= ~FLAG_EXTERNAL; else flagB |= FLAG_EXTERNAL; } /** * Set this arc's index into Table A. */ public void setIndexA(byte indexA) { this.indexA = indexA; } /** * Get this arc's index into Table A. * * Required for writing restrictions (Table C). */ public byte getIndexA() { return indexA; } /** * Set this arc's index into Table B. Applies to external arcs only. */ public void setIndexB(byte indexB) { assert !isInternal() : "Trying to set index on internal arc."; this.indexB = indexB; } /** * Get this arc's index into Table B. * * Required for writing restrictions (Table C). */ public int getIndexB() { return indexB & 0xff; } public int getArcDestClass(){ return Math.min(getRoadDef().getRoadClass(), dest.getGroup()); } public float getLengthInMeter(){ return lengthInMeter; } public static byte directionFromDegrees(float dir) { return (byte) Math.round(dir * 256.0 / 360) ; } public void write(ImgFileWriter writer, RouteArc lastArc, boolean useCompactDirs, Byte compactedDir) { boolean first = lastArc == null; if (first){ if (useCompactDirs) flagA |= FLAG_HASNET; // first arc: the 1 tells us that initial directions are in compacted format } else { if (lastArc.getRoadDef() != this.getRoadDef()) flagA |= FLAG_HASNET; // not first arc: the 1 tells us that we have to read table A } if (isForward) flagA |= FLAG_FORWARD; offset = writer.position(); if(log.isDebugEnabled()) log.debug("writing arc at", offset, ", flagA=", Integer.toHexString(flagA)); // fetch destination class -- will have been set correctly by now setDestinationClass(getArcDestClass()); // determine how to write length and curve bit int[] lendat = encodeLength(); writer.put(flagA); if (isInternal()) { // space for 14 bit node offset, written in writeSecond. writer.put(flagB); writer.put((byte) 0); } else { if(indexB < 0 || indexB >= 0x3f) { writer.put((byte) (flagB | 0x3f)); writer.put(indexB); } else writer.put((byte) (flagB | indexB)); } // only write out the local net index if it is the first arc or else if newDir is set. if (first || lastArc.indexA != this.indexA) writer.put(indexA); if(log.isDebugEnabled()) log.debug("writing length", length); for (int aLendat : lendat) writer.put((byte) aLendat); if (first || lastArc.indexA != this.indexA || lastArc.isForward != isForward){ if (useCompactDirs){ // determine if we have to write direction info if (compactedDir != null) writer.put(compactedDir); } else writer.put(directionFromDegrees(initialHeading)); } else { // System.out.println("skipped writing of initial dir"); } if (haveCurve) { int[] curvedat = encodeCurve(); for (int aCurvedat : curvedat) writer.put((byte) aCurvedat); } } /** * Second pass over the nodes in this RouteCenter. * Node offsets are now all known, so we can write the pointers * for internal arcs. */ public void writeSecond(ImgFileWriter writer) { if (!isInternal()) return; writer.position(offset + 1); char val = (char) (flagB << 8); int diff = dest.getOffsetNod1() - source.getOffsetNod1(); assert diff < 0x2000 && diff >= -0x2000 : "relative pointer too large for 14 bits (source offset = " + source.getOffsetNod1() + ", dest offset = " + dest.getOffsetNod1() + ")"; val |= diff & 0x3fff; // We write this big endian if(log.isDebugEnabled()) log.debug("val is", Integer.toHexString((int)val)); writer.put((byte) (val >> 8)); writer.put((byte) val); } /* * length and curve flag are stored in a variety of ways, involving * 1. flagA & 0x38 (3 bits) * 2. 1-3 bytes following the possible Table A index * * There's even more different encodings supposedly. */ private int[] encodeLength() { int[] lendat; if(length < 0x300 || (length < 0x400 && haveCurve == false)) { // 10 bit length optional curve // clear bits flagA &= ~0x38; if(haveCurve) flagA |= 0x20; flagA |= (length >> 5) & 0x18; // top two bits of length (at least one must be zero) lendat = new int[1]; // one byte of data lendat[0] = length; // bottom 8 bits of length assert (flagA & 0x38) != 0x38; } else { flagA |= 0x38; // all three bits set if(length >= (1 << 14)) { // 22 bit length with curve lendat = new int[3]; // three bytes of data lendat[0] = 0xC0 | (length & 0x3f); // 0x80 set, 0x40 set, 6 low bits of length lendat[1] = (length >> 6) & 0xff; // 8 more bits of length lendat[2] = (length >> 14) & 0xff; // 8 more bits of length } else if(haveCurve) { // 15 bit length with curve lendat = new int[2]; // two bytes of data lendat[0] = (length & 0x7f); // 0x80 not set, 7 low bits of length lendat[1] = (length >> 7) & 0xff; // 8 more bits of length } else { // 14 bit length no curve lendat = new int[2]; // two bytes of data lendat[0] = 0x80 | (length & 0x3f); // 0x80 set, 0x40 not set, 6 low bits of length lendat[1] = (length >> 6) & 0xff; // 8 more bits of length } } return lendat; } /** * Encode the curve data into a sequence of bytes. * Curve data contains a ratio between arc length and direct distance * and the direct bearing. This is typically encode in one byte, * for extreme ratios two bytes are used. */ private int[] encodeCurve() { assert lengthRatio != 0; int[] curveData; int dh = directionFromDegrees(directHeading); if (lengthRatio >= 1 && lengthRatio <= 17) { // two byte curve data neeeded curveData = new int[2]; curveData[0] = lengthRatio; curveData[1] = dh; } else { // use compacted form int compactedRatio = lengthRatio / 2 - 8; assert compactedRatio > 0 && compactedRatio < 8; curveData = new int[1]; curveData[0] = (compactedRatio << 5) | ((dh >> 3) & 0x1f); /* check math: int dhx = curveData[0] & 0x1f; int decodedDirectHeading = (dhx <16) ? dhx << 3 : -(256 - (dhx<<3)); if ((byte) (dh & 0xfffffff8) != (byte) decodedDirectHeading) log.error("failed to encode direct heading", directHeading, dh, decodedDirectHeading); int ratio = (curveData[0] & 0xe0) >> 5; if (ratio != compactedRatio) log.error("failed to encode length ratio", lengthRatio, compactedRatio, ratio); */ } return curveData; } public RoadDef getRoadDef() { return roadDef; } public void setForward() { isForward = true; } public boolean isForward() { return isForward; } public void setLast() { flagB |= FLAG_LAST_LINK; } protected void setDestinationClass(int destinationClass) { if(log.isDebugEnabled()) log.debug("setting destination class", destinationClass); flagA |= (destinationClass & MASK_DESTCLASS); } public void setIndirect() { this.isDirect = false; } public boolean isDirect() { return isDirect; } public void setMaxDestClass(int destClass) { if (this.maxDestClass < 0) this.maxDestClass = (byte) destClass; else if (destClass < maxDestClass) maxDestClass = (byte)destClass; } @Override public String toString() { return "RouteArc [" + (isForward ? "->":"<-") + (isDirect ? "direct":"indirect") + roadDef + " " + dest + "]"; } public RouteArc getReverseArc() { return reverseArc; } public void setReverseArc(RouteArc reverseArc) { this.reverseArc = reverseArc; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/Numbers.java0000644000076400007640000002706712533237270022675 0ustar stevesteve/* * Copyright (C) 2008 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 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.imgfmt.app.net; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.CityInfo; import uk.me.parabola.mkgmap.general.ZipCodeInfo; /** * Describes the house numbering from a node in the road. * @author Steve Ratcliffe */ public class Numbers { private static final Logger log = Logger.getLogger(Numbers.class); public static final boolean LEFT = true; public static final boolean RIGHT = false; private static final int MAX_DELTA = 131071; // see NumberPreparer // The node in the road where these numbers apply. In the polish notation it is the // node in the road, whereas in the NET file it is the index of the number node. private int nodeNumber; // node in road index private Integer indexNumber; // the position in the list of Numbers (starting with 0) // the data on side of the road private RoadSide leftSide,rightSide; private class RoadSide { NumDesc numbers; // to be added CityInfo cityInfo; ZipCodeInfo zipCode; boolean isEmpty(){ return cityInfo == null && zipCode == null && numbers == null; } } private class NumDesc{ NumberStyle numberStyle; int start,end; public NumDesc(NumberStyle numberStyle, int start, int end) { this.numberStyle = numberStyle; this.start = start; this.end = end; } public boolean contained(int hn){ boolean isEven = (hn % 2 == 0); if (numberStyle == NumberStyle.BOTH || numberStyle == NumberStyle.EVEN && isEven || numberStyle == NumberStyle.ODD && !isEven){ if (start <= end) { if (start <= hn && hn <= end) return true; } else { if (end <= hn && hn <= start) return true; } } return false; } @Override public String toString() { return String.format("%s,%d,%d", numberStyle, start,end); } } public Numbers() { } /** * This constructor takes a comma separated list as in the polish format. Also used in testing as * it is an easy way to set all common parameters at once. * * @param spec Node number, followed by left and then right parameters as in the polish format. */ public Numbers(String spec) { String[] strings = spec.split(","); nodeNumber = Integer.valueOf(strings[0]); NumberStyle numberStyle = NumberStyle.fromChar(strings[1]); int start = Integer.valueOf(strings[2]); int end = Integer.valueOf(strings[3]); setNumbers(LEFT, numberStyle, start, end); numberStyle = NumberStyle.fromChar(strings[4]); start = Integer.valueOf(strings[5]); end = Integer.valueOf(strings[6]); setNumbers(RIGHT, numberStyle, start, end); if (strings.length > 8){ // zip codes String zip = strings[7]; if ("-1".equals(zip) == false) setZipCode(LEFT, new ZipCodeInfo(zip)); zip = strings[8]; if ("-1".equals(zip) == false) setZipCode(RIGHT, new ZipCodeInfo(zip)); } if (strings.length > 9){ String city,region,country; int nextPos = 9; city = strings[nextPos]; if ("-1".equals(city) == false){ region = strings[nextPos + 1]; country = strings[nextPos + 2]; setCityInfo(LEFT, new CityInfo(city, region, country)); nextPos = 12; } else nextPos = 10; city = strings[nextPos]; if ("-1".equals(city) == false){ region = strings[nextPos + 1]; country = strings[nextPos + 2]; setCityInfo(RIGHT, new CityInfo(city, region, country)); } } } public void setNumbers(boolean left, NumberStyle numberStyle, int start, int end){ if (numberStyle != NumberStyle.NONE || start != -1 || end != -1){ RoadSide rs = assureSideIsAllocated(left); rs.numbers = new NumDesc(numberStyle, start, end); } else { RoadSide rs = (left) ? leftSide : rightSide; if (rs != null) rs.numbers = null; removeIfEmpty(left); } } public void setCityInfo(boolean left, CityInfo ci){ if (ci != null){ RoadSide rs = assureSideIsAllocated(left); rs.cityInfo = ci; } else { RoadSide rs = (left) ? leftSide : rightSide; if (rs != null) rs.cityInfo = null; removeIfEmpty(left); } } public CityInfo getCityInfo(boolean left){ RoadSide rs = (left) ? leftSide : rightSide; return (rs != null) ? rs.cityInfo : null; } public void setZipCode(boolean left, ZipCodeInfo zipCode){ if (zipCode != null){ RoadSide rs = assureSideIsAllocated(left); rs.zipCode = zipCode; } else { RoadSide rs = (left) ? leftSide : rightSide; if (rs != null) rs.zipCode= null; removeIfEmpty(left); } } public ZipCodeInfo getZipCodeInfo (boolean left){ RoadSide rs = (left) ? leftSide : rightSide; return (rs != null) ? rs.zipCode: null; } private void removeIfEmpty(boolean left){ if (left && leftSide != null && leftSide.isEmpty()) leftSide = null; if (!left && rightSide != null && rightSide.isEmpty()) rightSide = null; } // allocate or return allocated RoadSide instance for the given road side private RoadSide assureSideIsAllocated(boolean left){ if (left && leftSide == null) leftSide = new RoadSide(); if (!left && rightSide == null) rightSide = new RoadSide(); return (left) ? leftSide : rightSide; } public int getNodeNumber() { return nodeNumber; } public void setNodeNumber(int nodeNumber) { this.nodeNumber = nodeNumber; } public int getIndex() { if (indexNumber == null) { log.error("WARNING: index not set!!"); return nodeNumber; } return indexNumber; } public boolean hasIndex() { return indexNumber != null; } /** * @param index the nth number node */ public void setIndex(int index) { this.indexNumber = index; } private NumDesc getNumbers(boolean left) { RoadSide rs = (left) ? leftSide : rightSide; return (rs != null) ? rs.numbers : null; } public NumberStyle getNumberStyle(boolean left) { NumDesc n = getNumbers(left); return (n == null) ? NumberStyle.NONE : n.numberStyle; } public int getStart(boolean left) { NumDesc n = getNumbers(left); return (n == null) ? -1 : n.start; // -1 is the default in the polish format } public int getEnd(boolean left) { NumDesc n = getNumbers(left); return (n == null) ? -1 : n.end; // -1 is the default in the polish format } public String toString() { String nodeStr = "0"; if (nodeNumber > 0) nodeStr = String.valueOf(nodeNumber); else if (getIndex() > 0) nodeStr = String.format("(n%d)", getIndex()); nodeStr = String.format("%s,%s,%d,%d,%s,%d,%d", nodeStr, getNumberStyle(LEFT), getStart(LEFT), getEnd(LEFT), getNumberStyle(RIGHT), getStart(RIGHT), getEnd(RIGHT)); if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null || getZipCodeInfo(LEFT) != null || getZipCodeInfo(RIGHT) != null) { nodeStr = String.format("%s,%s,%s", nodeStr, getPolishZipCode(LEFT), getPolishZipCode(RIGHT)); if (getCityInfo(LEFT) != null || getCityInfo(RIGHT) != null) { nodeStr = String.format("%s,%s,%s",nodeStr, getPolishCityInfo(LEFT),getPolishCityInfo(RIGHT)); } } return nodeStr; } public NumberStyle getLeftNumberStyle() { return getNumberStyle(LEFT); } public NumberStyle getRightNumberStyle() { return getNumberStyle(RIGHT); } public int getLeftStart(){ return getStart(LEFT); } public int getRightStart(){ return getStart(RIGHT); } public int getLeftEnd(){ return getEnd(LEFT); } public int getRightEnd(){ return getEnd(RIGHT); } public boolean equals(Object obj) { if (!(obj instanceof Numbers)) return false; Numbers other = (Numbers) obj; return toString().equals(other.toString()); } public int hashCode() { return toString().hashCode(); } public boolean isPlausible(){ if (!isPlausible(getLeftNumberStyle(), getLeftStart(), getLeftEnd())) return false; if (!isPlausible(getRightNumberStyle(), getRightStart(), getRightEnd())) return false; if (getLeftNumberStyle() == NumberStyle.NONE || getRightNumberStyle() == NumberStyle.NONE) return true; if (getCityInfo(LEFT) != null){ if (getCityInfo(LEFT).equals(getCityInfo(RIGHT)) == false) return true; } else if (getCityInfo(RIGHT) != null) return true; if (getZipCodeInfo(LEFT) != null){ if (getZipCodeInfo(LEFT).equals(getZipCodeInfo(RIGHT)) == false) return true; } else if (getCityInfo(RIGHT) != null) return true; if (getLeftNumberStyle() == getRightNumberStyle() || getLeftNumberStyle() == NumberStyle.BOTH || getRightNumberStyle()==NumberStyle.BOTH){ // check if intervals are overlapping int start1, start2,end1,end2; if (getLeftStart() < getLeftEnd()){ start1 = getLeftStart(); end1 = getLeftEnd(); } else { start1 = getLeftEnd(); end1 = getLeftStart(); } if (getRightStart() < getRightEnd()){ start2 = getRightStart(); end2 = getRightEnd(); } else { start2 = getRightEnd(); end2 = getRightStart(); } if (start2 > end1 || end2 < start1) return true; if (getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd() && getLeftStart() == getRightStart()) return true; // single number on both sides of the road return false; } return true; } private static boolean isPlausible(NumberStyle style, int start, int end){ if (Math.abs(start - end) > MAX_DELTA) return false; if (style == NumberStyle.EVEN) return start % 2 == 0 && end % 2 == 0; if (style == NumberStyle.ODD) return start % 2 != 0 && end % 2 != 0; return true; } public boolean isContained(int hn, boolean left){ RoadSide rs = left ? leftSide : rightSide; if (rs == null || rs.numbers == null) return false; return rs.numbers.contained(hn); } /** * @param hn a house number * @param left left or right side * @return 0 if the number is not within the intervals, 1 if it is on one side, 2 if it on both sides */ public int countMatches(int hn) { int matches = 0; if (isContained(hn, LEFT)) matches++; if (isContained(hn, RIGHT)) matches++; if (matches > 1){ if (getLeftStart() == getLeftEnd() && getRightStart() == getRightEnd()) matches = 1; // single number on both sides of the road } return matches; } /** * Compare all fields that describe the interval, but not the position * @param other * @return true if these fields are equal */ public boolean isSimilar(Numbers other){ if (other == null) return false; if (getLeftNumberStyle() != other.getLeftNumberStyle() || getLeftStart() != other.getLeftStart() || getLeftEnd() != other.getLeftEnd() || getRightNumberStyle() != other.getRightNumberStyle() || getRightStart() != other.getRightStart() || getRightEnd() != other.getRightEnd()) return false; return true; } public boolean isEmpty(){ return getLeftNumberStyle() == NumberStyle.NONE && getRightNumberStyle() == NumberStyle.NONE; } private String getPolishCityInfo (boolean left){ CityInfo ci = getCityInfo(left); if (ci == null) return "-1"; StringBuilder sb = new StringBuilder(); if (ci.getCity() != null) sb.append(ci.getCity()); sb.append(","); if (ci.getRegion() != null) sb.append(ci.getRegion()); sb.append(","); if (ci.getCountry() != null) sb.append(ci.getCountry()); return sb.toString(); } private String getPolishZipCode (boolean left){ ZipCodeInfo zip = getZipCodeInfo(left); return (zip != null && zip.getZipCode() != null ) ? zip.getZipCode() : "-1"; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NumberPreparer.java0000644000076400007640000006727012570537264024222 0ustar stevesteve/* * Copyright (C) 2008 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 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.imgfmt.app.net; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.Iterator; import java.util.List; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.lbl.Zip; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.CityInfo; import uk.me.parabola.mkgmap.general.ZipCodeInfo; import static uk.me.parabola.imgfmt.app.net.NumberStyle.*; /** * Class to prepare the bit stream of the house numbering information. * * There are multiple ways to encode the same numbers, the trick is to find a way that is reasonably * small. We recognise a few common cases to reduce the size of the bit stream, but mostly just concentrating * on clarity and correctness. Optimisations only made a few percent difference at most. * * @author Steve Ratcliffe */ public class NumberPreparer { private static final Logger log = Logger.getLogger(NumberPreparer.class); private final List numbers; private boolean valid; // The minimum values of the start and end bit widths. private static final int START_WIDTH_MIN = 5; private static final int END_WIDTH_MIN = 2; private BitWriter bw; private boolean swappedDefaultStyle; CityZipWriter zipWriter; CityZipWriter cityWriter; public NumberPreparer(List numbers) { this.numbers = numbers; this.zipWriter = new CityZipWriter("zip", 0, 0); this.cityWriter = new CityZipWriter("city", 0, 0); } public NumberPreparer(List numbers, Zip zip, City city, int numCities, int numZips) { this.numbers = numbers; zipWriter = new CityZipWriter("zip",(zip == null) ? 0: zip.getIndex(), numZips); cityWriter = new CityZipWriter("city",(city == null) ? 0: city.getIndex(), numCities); } public boolean prepare(){ fetchBitStream(); if (!valid) return false; zipWriter.compile(numbers); cityWriter.compile(numbers); return true; } /** * Make the bit stream and return it. This is only done once, if you call this several times * the same bit writer is returned every time. * @return A bit writer containing the computed house number stream. */ public BitWriter fetchBitStream() { if (bw != null) return bw; int initialValue = setup(); // Write the bitstream bw = new BitWriter(); try { // Look at the numbers and calculate some optimal values for the bit field widths etc. State state = new GatheringState(initialValue); process(new BitWriter(), state); // Write the initial values. writeWidths(state); writeInitialValue(state); state = new WritingState(state); process(bw, state); // If we get this far and there is something there, the stream might be valid! if (bw.getLength() > 1) valid = true; } catch (Abandon e) { log.error(e.getMessage()); valid = false; } return bw; } /** * Do some initial calculation and sanity checking of the numbers that we are to * write. * @return The initial base value that all other values are derived from. */ private int setup() { // Should we use the swapped default numbering style EVEN/ODD rather than // ODD/EVEN and the initialValue. for (Iterator iterator = numbers.listIterator(); iterator.hasNext(); ) { Numbers n = iterator.next(); if (n.getLeftNumberStyle() == NONE && n.getRightNumberStyle() == NONE) iterator.remove(); } if (numbers.isEmpty()) throw new Abandon("no numbers"); Numbers first = numbers.get(0); if (first.getLeftNumberStyle() == EVEN && first.getRightNumberStyle() == ODD) swappedDefaultStyle = true; // Calculate the initial value we want to use int initial = 0; if (first.getLeftNumberStyle() != NONE) initial = first.getLeftStart(); int rightStart = 0; if (first.getRightNumberStyle() != NONE) rightStart = first.getRightStart(); if (initial == 0) initial = rightStart; if (first.getLeftStart() > first.getLeftEnd() || first.getRightStart() > first.getRightEnd()) initial = Math.max(initial, rightStart); else if (rightStart > 0) initial = Math.min(initial, rightStart); return initial; } /** * Process the list of number ranges and compile them into a bit stream. * * This is done twice, once to calculate the sizes of the bit fields needed, and again * to do the actual writing. * * @param bw The bit stream to write to. * @param state Use to keep track of state during the construction process. */ private void process(BitWriter bw, State state) { if (swappedDefaultStyle) state.swapDefaults(); int lastNode = -1; for (Numbers n : numbers) { if (!n.hasIndex()) throw new Abandon("no r node set"); // See if we need to skip some nodes if (n.getIndex() != lastNode + 1) state.writeSkip(bw, n.getIndex() - lastNode - 2); // Normal case write out the next node. state.setTarget(n); state.writeNumberingStyle(bw); state.calcNumbers(); state.writeBitWidths(bw); state.writeNumbers(bw); state.restoreWriters(); lastNode = n.getIndex(); } } /** * The initial base value is written out separately before anything else. * All numbers are derived from differences from this value. * @param state Holds the initial value to write. */ private void writeInitialValue(State state) { assert state.initialValue >= 0 : "initial value is not positive: " + state.initialValue; int width = 32 - Integer.numberOfLeadingZeros(state.initialValue); if (width > 20) throw new Abandon("Initial value too large: " + state.initialValue); if (width > 5) { bw.put1(false); bw.putn(width - 5, 4); } else { bw.put1(true); width = 5; } bw.putn(state.initialValue, width); } /** * Write out a block that describes the number of bits to use. Numbers can be * either all positive or all negative, or they can be signed and each bit field * also has an extra sign bit. This is like how lines are encoded. See the LinePreparer * class. * @param state Holds the width information. */ private void writeWidths(State state) { state.getStartWriter().writeFormat(); state.getEndWriter().writeFormat(); } /** * Returns true if the bit stream was calculated on the basis that the initial even/odd defaults * should be swapped. * @return True to signify swapped default, ie bit 0x20 in the net flags should be set. */ public boolean getSwapped() { return swappedDefaultStyle; } /** * During development, any case that cannot be written correctly is marked invalid so it can * be skipped on output. * * This will probably go away when complete. * * @return True if the preparer believes that the output is valid. */ public boolean isValid() { try { fetchBitStream(); } catch (Exception e) { } return valid; } /** * The current state of the writing process. */ static abstract class State { protected final Side left = new Side(true); protected final Side right = new Side(false); private int initialValue; State() { left.style = ODD; right.style = EVEN; } /** * Set the initial value. All numbers are derived from this by adding differences. */ public void setInitialValue(int val) { initialValue = val; left.base = val; right.base = val; } /** * Set the next number to output. Once the target is set, we then output commands to * transform the current state into the target state. * @param numbers The target numbers. */ public void setTarget(Numbers numbers) { left.setTargets(numbers.getLeftNumberStyle(), numbers.getLeftStart(), numbers.getLeftEnd()); right.setTargets(numbers.getRightNumberStyle(), numbers.getRightStart(), numbers.getRightEnd()); } /** * If the target numbering style is different to the current one, then write out * the command to change it. */ public void writeNumberingStyle(BitWriter bw) { } /** * If we need a larger bit width for this node, then write out a command to * change it. Changes are temporary and it reverts to the default after the * next number output command. */ public void writeBitWidths(BitWriter bw) { } public void writeSkip(BitWriter bw, int n) { } /** * Calculate the number difference to represent the current number range. */ public void calcNumbers() { if (left.style == NONE) left.base = right.base; equalizeBases(); left.calc(right); right.calc(left); } /** * See if we can set the bases of both sides of the road to be equal. Doesn't seem to be * that useful, but does not cost any bits, as long as doing so doesn't cause you to write * a difference when you wouldn't without. * @return True if the bases have been set equal. There are two cases, the left can be set equal to * the right, or visa versa. The flags on the left/right objects will say which. */ private boolean equalizeBases() { left.equalized = right.equalized = false; // Don't if runs are in different directions if (left.direction != right.direction) { return false; } int diff = left.targetStart - left.base; // Do not lose the benefit of a 0 start. if (left.tryStart(left.base)) diff = 0; if (right.tryStart(left.base + diff)) { left.equalized = true; right.base = left.base; left.startDiff = right.startDiff = diff; return true; } diff = right.targetStart - right.base; if (left.tryStart(right.base + diff)) { right.equalized = true; left.base = right.base; left.startDiff = right.startDiff = diff; return true; } return false; } /** * Write the bit stream to the given bit writer. * * When this is called, all the calculations as to what is to be done have been made and * it is just a case of translating those into the correct format. * * @param bw Bit writer to use. In the gathering phase this must be a throw away one. */ public void writeNumbers(BitWriter bw) { boolean doSingleSide = left.style == NONE || right.style == NONE; // Output the command that a number follows. bw.put1(true); boolean equalized = false; if (!doSingleSide) { equalized = left.equalized || right.equalized; bw.put1(equalized); if (equalized) bw.put1(left.equalized); } if (!doSingleSide) { bw.put1(!right.needOverride(left)); } Side firstSide = left; if (doSingleSide && left.style == NONE) firstSide = right; boolean doStart = firstSide.startDiff != 0; boolean doEnd = firstSide.endDiff != 0; bw.put1(!doStart); bw.put1(!doEnd); if (doStart) writeStart(firstSide.startDiff); if (doEnd) writeEnd(firstSide.endDiff); firstSide.finish(); if (doSingleSide) { left.base = right.base = firstSide.base; left.lastEndDiff = right.lastEndDiff = firstSide.lastEndDiff; return; } doStart = right.startDiff != 0; doEnd = right.endDiff != 0; if (!equalized) bw.put1(!doStart); if (right.needOverride(left)) bw.put1(!doEnd); if (doStart && !equalized) writeStart(right.startDiff); if (doEnd) writeEnd(right.endDiff); right.finish(); } protected void restoreWriters() { } /** Write a start difference */ public abstract void writeStart(int diff); /** Write an end difference */ public abstract void writeEnd(int diff); public abstract VarBitWriter getStartWriter(); public abstract VarBitWriter getEndWriter(); /** * By default the left side of the road is odd numbered and the right even. * Calling this swaps that around. If NONE or BOTH is needed then an explicit set of * the numbering styles must be made. */ public void swapDefaults() { left.style = EVEN; right.style = ODD; } } /** * Represents one side of the road. */ static class Side { private final boolean left; private NumberStyle style; private int base; // The calculated end number for the node. Might be different to the actual number // that are wanted that are in targetEnd. private int end; // These are the target start and end numbers for the node. The real numbers are different as there // is an adjustment applied. private NumberStyle targetStyle; private int targetStart; private int targetEnd; // Everything is represented as a difference from a previous value. private int startDiff; private int endDiff; private int lastEndDiff; // This is +1 if the numbers are ascending, and -1 if descending. private int direction; // Bases equalised to this side. private boolean equalized; Side(boolean left) { this.left = left; } /** * Set the wanted values for start and end for this side of the road. */ public void setTargets(NumberStyle style, int start, int end) { this.targetStyle = style; this.targetStart = start; this.targetEnd = end; // In reality should use the calculated start and end values, not the targets. Real start and end // values are not ever the same (in this implementation) so that is why the case where start==end // is given the value +1. if (targetStart < targetEnd) direction = 1; else if (targetEnd < targetStart) direction = -1; else direction = 1; } /** * Try a start value to see if it will work. Obviously a value equal to the target will work * but so will a value that equals it after rounding for odd/even. * @param value The value to test. * @return True if this value would result in the targetStart. */ private boolean tryStart(int value) { return value == targetStart || style.round(value, direction) == targetStart; } /** * For the right hand side, read and end value, or use the last end value as default. * * Otherwise, the same end diff is used for the right side as the left. * @param left Reference to the left hand side. */ public boolean needOverride(Side left) { return endDiff != 0 || left.endDiff == 0; } /** * There is more than one way to represent the same range of numbers. The idea is to pick one of * the shorter ways. We don't make any effort to find the shortest, but just pick a reasonable * strategy for some common cases, and making use of defaults where we can. * * @param other The details of the other side of the road. * */ private void calc(Side other) { if (style == NONE) return; boolean equalized = this.equalized || other.equalized; if (!equalized) startDiff = tryStart(base)? 0: targetStart - base; endDiff = targetEnd - (base+startDiff) + direction; // Special for start == end, we can often do without an end diff. if (targetStart == targetEnd && base == targetStart && lastEndDiff == 0 && !equalized) { if (left || (other.endDiff == 0)) endDiff = 0; } // Now that end is calculated we fix it and see if we can obtain it by default instead. end = base+startDiff+endDiff; if (left) { if (endDiff == lastEndDiff) endDiff = 0; // default is our last diff. } else if (other.style != NONE) { // right side (and left not NONE) if (other.endDiff == 0 && endDiff == lastEndDiff) endDiff = 0; // No left diff, default is our last if (other.endDiff != 0 && other.endDiff == endDiff) endDiff = 0; // Left diff set, that's our default } } /** * Called at the end of processing a number range. Sets up the fields for the next one. */ public void finish() { lastEndDiff = end - (base + startDiff); base = end; } } /** * The calculations are run on this class first, which keeps track of the sizes required to * write the values without actually writing them anywhere. * * When passing a BitWriter to any method on this class, it must be a throw away one, as it * will actually be written to by some of the common methods. */ private class GatheringState extends State { class BitSizes { private boolean positive; private boolean negative; private int diff; private boolean isSigned() { return positive && negative; } private int calcWidth() { int n = diff; if (isSigned()) n++; return 32 - Integer.numberOfLeadingZeros(n); } } private final BitSizes start = new BitSizes(); private final BitSizes end = new BitSizes(); public GatheringState(int initialValue) { setInitialValue(initialValue); } public void writeNumberingStyle(BitWriter bw) { left.style = left.targetStyle; right.style = right.targetStyle; } /** * Calculate the size required for this write and keeps the maximum values. * @param diff The value to examine. */ public void writeStart(int diff) { int val = testSign(start, diff); if (val > start.diff) start.diff = val; } /** * Calculate the size required to hold this write and keeps the maximum. * @param diff The value to be examined. */ public void writeEnd(int diff) { int val = testSign(end, diff); if (val > end.diff) end.diff = val; } /** * Checks the sign properties required for the write. */ private int testSign(BitSizes bs, int val) { if (val > 0) { bs.positive = true; } else if (val < 0) { bs.negative = true; return -val; } return val; } /** * Construct a writer that uses a bit width and sign properties that are sufficient to write * all of the values found in the gathering phase. This is for start differences. */ public VarBitWriter getStartWriter() { return getVarBitWriter(start, START_WIDTH_MIN); } /** * Construct a writer that uses a bit width and sign properties that are sufficient to write * all of the values found in the gathering phase. This is for end differences. */ public VarBitWriter getEndWriter() { return getVarBitWriter(end, END_WIDTH_MIN); } /** * Common code to create the bit writer. * @see #getStartWriter() * @see #getEndWriter() */ private VarBitWriter getVarBitWriter(BitSizes bs, int minWidth) { VarBitWriter writer = new VarBitWriter(bw, minWidth); if (bs.isSigned()) writer.signed = true; else if (bs.negative) writer.negative = true; int width = bs.calcWidth(); if (width > minWidth) writer.bitWidth = width - minWidth; if (writer.bitWidth > 15) throw new Abandon("Difference too large"); return writer; } } /** * This is used to actually write the bit stream. * @see GatheringState */ static class WritingState extends State { private VarBitWriter startWriter; private VarBitWriter endWriter; private boolean restoreBitWriters; private final VarBitWriter savedStartWriter; private final VarBitWriter savedEndWriter; public WritingState(State state) { setInitialValue(state.initialValue); left.base = state.initialValue; right.base = state.initialValue; startWriter = state.getStartWriter(); endWriter = state.getEndWriter(); this.savedStartWriter = startWriter; this.savedEndWriter = endWriter; } public void writeStart(int diff) { startWriter.write(diff); } public void writeEnd(int diff) { endWriter.write(diff); } public void writeNumberingStyle(BitWriter bw) { if (left.targetStyle != left.style || right.targetStyle != right.style) { bw.putn(0, 2); bw.putn(left.targetStyle.getVal(), 2); bw.putn(right.targetStyle.getVal(), 2); left.style = left.targetStyle; right.style = right.targetStyle; } } /** * You can change the number of bits and the sign properties of the writers before writing a nodes * numbers. We don't try and work out the optimum sequence, but use this for tricky cases where * we fail to work out the correct sizes in advance. * * This routine means that we will always be using writers that will deal with the next node numbers. * * @param bw The output stream writer. */ public void writeBitWidths(BitWriter bw) { newWriter(bw, startWriter, left.startDiff, right.startDiff, true); newWriter(bw, endWriter, left.endDiff, right.endDiff, false); } /** * Common code for writeBitWidths. Calculate the width and the sign properties required to * represent the two numbers. * @param leftDiff One of the numbers to be represented. * @param rightDiff The other number to be represented. * @param start Set to true if this is the start writer, else it is for the end writer. */ private void newWriter(BitWriter bw, VarBitWriter writer, int leftDiff, int rightDiff, boolean start) { if (!writer.checkFit(leftDiff) || !writer.checkFit(rightDiff)) { int min = Math.min(leftDiff, rightDiff); int max = Math.max(leftDiff, rightDiff); boolean signed = false; boolean negative = false; if (max < 0) negative = true; else if (min < 0) signed = true; int val = Math.max(Math.abs(min), Math.abs(max)); int width = 32 - Integer.numberOfLeadingZeros(val); if (signed) width++; restoreBitWriters = true; VarBitWriter nw; if (start) { startWriter = nw = new VarBitWriter(bw, START_WIDTH_MIN, negative, signed, width); bw.putn(2, 4); // change width start } else { endWriter = nw = new VarBitWriter(bw, END_WIDTH_MIN, negative, signed, width); bw.putn(0xa, 4); // change width end (0x8 | 0x2) } nw.writeFormat(); } } public void writeSkip(BitWriter bw, int n) { if (n < 0) throw new Abandon("bad skip value:" + n); bw.putn(6, 3); int width = 32 - Integer.numberOfLeadingZeros(n); if (width > 5) { bw.put1(true); width = 10; } else { bw.put1(false); width = 5; } bw.putn(n, width); } public VarBitWriter getStartWriter() { return startWriter; } public VarBitWriter getEndWriter() { return endWriter; } /** * If we used an alternate writer for a node's numbers then we restore the default * writers afterwards. */ protected void restoreWriters() { if (restoreBitWriters) { startWriter = savedStartWriter; endWriter = savedEndWriter; restoreBitWriters = false; } } } } /** * A bit writer that can be configured with different bit width and sign properties. * * The sign choices are: * negative: all numbers are negative and so can be represented without a sign bit. (or all positive * if this is false). * signed: numbers are positive and negative, and so have sign bit. * * The bit width is composed of two parts since it is represented as a difference between * a well known minimum value and the actual value. */ class VarBitWriter { private final BitWriter bw; private final int minWidth; int bitWidth; boolean negative; boolean signed; VarBitWriter(BitWriter bw, int minWidth) { this.bw = bw; this.minWidth = minWidth; } public VarBitWriter(BitWriter bw, int minWidth, boolean negative, boolean signed, int width) { this(bw, minWidth); this.negative = negative; this.signed = signed; if (width > minWidth) this.bitWidth = width - minWidth; } /** * Write the number to the bit stream. If the number cannot be written * correctly with this bit writer then an exception is thrown. This shouldn't * happen since we check before hand and create a new writer if the numbers are not * going to fit. * * @param n The number to be written. */ public void write(int n) { if (!checkFit(n)) throw new Abandon("number does not fit bit space available"); if (n < 0 && negative) n = -n; if (signed) { int mask = (1 << (minWidth + bitWidth+2)) - 1; n &= mask; } bw.putn(n, minWidth+bitWidth + ((signed)?1:0)); } /** * Checks to see if the number that we want to write can be written by this writer. * @param n The number we would like to write. * @return True if all is OK for writing it. */ boolean checkFit(int n) { if (negative) { if (n > 0) return false; else n = -n; } else if (signed && n < 0) n = -1 - n; int mask = (1 << minWidth + bitWidth) - 1; return n == (n & mask); } /** * Write the format of this bit writer to the output stream. Used at the beginning and * when changing the bit widths. */ public void writeFormat() { bw.put1(negative); bw.put1(signed); bw.putn(bitWidth, 4); } } /** * Exception to throw when we detect that we do not know how to encode a particular case. * This should not be thrown any more, when the preparers is called correctly. * * If it is, then the number preparer is marked as invalid and the data is not written to the * output file. */ class Abandon extends RuntimeException { Abandon(String message) { super("HOUSE NUMBER RANGE: " + message); } } class CityZipWriter { private ByteArrayOutputStream buf; private final String type; private final int numItems; private final int defaultIndex; public CityZipWriter(String type, int defIndex, int numItems) { this.type = type; this.defaultIndex = defIndex; this.numItems = numItems; buf = new ByteArrayOutputStream(); } public ByteArrayOutputStream getBuffer(){ return buf; } public boolean compile(List numbers){ try { int lastNodeIndex = -1; // left and right entry in zip or city table int []prevIndexes = new int[2]; prevIndexes[0] = prevIndexes[1] = -1; int []indexes = new int[2]; for (Numbers num : numbers){ for (int i = 0; i < 2; i++){ indexes[i] = -1; boolean left = (i == 0); switch (type) { case "zip": ZipCodeInfo zipInfo = num.getZipCodeInfo(left); if (zipInfo != null){ if (zipInfo.getImgZip() != null) indexes[i] = zipInfo.getImgZip().getIndex(); else indexes[i] = 0; // or default? } break; case "city": CityInfo cityInfo = num.getCityInfo(left); if (cityInfo != null){ if (cityInfo.getImgCity() != null) indexes[i] = cityInfo.getImgCity().getIndex(); else indexes[i] = 0; // or default? } break; default: break; } } if (indexes[0] < 0 && indexes[1] < 0) continue; if (lastNodeIndex < 0){ if (num.getIndex() > 0 ){ int [] defindexes = {defaultIndex,defaultIndex}; write(0, defindexes, prevIndexes); } } int skip = num.getIndex() - lastNodeIndex - 1; assert defaultIndex > 0 : "bad default index"; lastNodeIndex = num.getIndex(); if (indexes[0] < 0) indexes[0] = defaultIndex; if (indexes[1] < 0) indexes[1] = defaultIndex; write(skip, indexes, prevIndexes); } } catch (Abandon e) { return false; } return true; } private void write(int skip, int[] indexes, int[] prevIndexes) { if (Arrays.equals(indexes, prevIndexes)) return; // we can signal new values for left and / or right side int sidesFlag = 0; if (indexes[0] <= 0 && indexes[1] <= 0){ sidesFlag |= 4; // signal end of a zip code/city interval if (indexes[0] == 0) sidesFlag |= 1; if (indexes[1] == 0) sidesFlag |= 2; } else { if (indexes[1] != indexes[0]){ if (indexes[0] > 0 && indexes[0] != prevIndexes[0]) sidesFlag |= 1; if (indexes[1] > 0 && indexes[1] != prevIndexes[1]) sidesFlag |= 2; } } int initFlag = Math.max(skip-1,0); if (initFlag > 31){ // we have to write two bytes buf.write((byte) (initFlag & 0x1f | 0x7<<5)); initFlag >>= 5; } initFlag |= sidesFlag << 5; buf.write((byte) (initFlag & 0xff)); if ((sidesFlag & 4) == 0) { if (indexes[0] > 0 && (sidesFlag == 0 || (sidesFlag & 1) == 1)) writeIndex(indexes[0]); if (indexes[1] > 0 && (sidesFlag & 2) != 0) writeIndex(indexes[1]); } System.arraycopy(indexes, 0, prevIndexes, 0, indexes.length); } void writeIndex(int val){ if (val <= 0) return; if (numItems > 255){ buf.write((byte) val & 0xff); buf.write((byte) (val >> 8)); } else buf.write((byte) val); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RoadDef.java0000644000076400007640000005265412533237270022566 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 5, 2008 */ package uk.me.parabola.imgfmt.app.net; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import uk.me.parabola.imgfmt.MapFailedException; import uk.me.parabola.imgfmt.app.BitWriter; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.lbl.Zip; import uk.me.parabola.imgfmt.app.trergn.Polyline; import uk.me.parabola.log.Logger; import uk.me.parabola.mkgmap.general.CityInfo; import uk.me.parabola.mkgmap.general.ZipCodeInfo; /** * A road definition. This ties together all segments of a single road * and provides street address information. * * This corresponds to an entry in NET1, which is linked with the * polylines making up this road in RGN. Links to RGN are written * via RoadIndex, while writing links from RGN to NET1 is delayed * via setOffsetWriter. * * If the map includes routing, the NET1 record also points to * a NOD2 record, written by writeNod2. * * Edges in the routing graph ("arcs") link to the corresponding * road via the RoadDef, storing the NET1 offset via TableA, * which also includes some road information. * * @author Elrond * @author Steve Ratcliffe * @author Robert Vollmert */ public class RoadDef { private static final Logger log = Logger.getLogger(RoadDef.class); public static final int NET_FLAG_NODINFO = 0x40; public static final int NET_FLAG_ADDRINFO = 0x10; private static final int NET_FLAG_UNK1 = 0x04; // lock on road? private static final int NET_FLAG_ONEWAY = 0x02; private static final int NOD2_FLAG_UNK = 0x01; // private static final int NOD2_FLAG_EXTRA_DATA = 0x80; just documentation // first byte of Table A info in NOD 1 private static final int TABA_FLAG_TOLL = 0x80; // private static final int TABA_MASK_CLASS = 0x70; just documentation private static final int TABA_FLAG_ONEWAY = 0x08; // private static final int TABA_MASK_SPEED = 0x07; just documentation private static final int TABAACCESS_FLAG_CARPOOL = 0x0008; private static final int TABAACCESS_FLAG_NOTHROUGHROUTE = 0x0080; // second byte: access flags, bits 0x08, 0x80 are set separately private static final int TABAACCESS_FLAG_NO_EMERGENCY = 0x8000; private static final int TABAACCESS_FLAG_NO_DELIVERY = 0x4000; private static final int TABAACCESS_FLAG_NO_CAR = 0x0001; private static final int TABAACCESS_FLAG_NO_BUS = 0x0002; private static final int TABAACCESS_FLAG_NO_TAXI = 0x0004; private static final int TABAACCESS_FLAG_NO_FOOT = 0x0010; private static final int TABAACCESS_FLAG_NO_BIKE = 0x0020; private static final int TABAACCESS_FLAG_NO_TRUCK = 0x0040; // true if road should not be added to NOD private boolean skipAddToNOD; // the offset in Nod2 of our Nod2 record private int offsetNod2; // the offset in Net1 of our Net1 record private int offsetNet1; /* * Everything that's relevant for writing to NET1. */ private int netFlags = NET_FLAG_UNK1; // the allowed vehicles in mkgmap internal format private byte mkgmapAccess; // The road length units may be affected by other flags in the header as // there is doubt as to the formula. private int roadLength; // There can be up to 4 labels for the same road. private static final int MAX_LABELS = 4; private final Label[] labels = new Label[MAX_LABELS]; private int numlabels; private final SortedMap> roadIndexes = new TreeMap<>(); private boolean paved = true; private boolean ferry; private boolean roundabout; private boolean linkRoad; private boolean synthesised; private boolean flareCheck; private Set messageIssued; private final List rgnOffsets = new ArrayList<>(); // for the NOD2 bit stream private BitSet nod2BitSet; /* * Everything that's relevant for writing out Nod 2. */ // This is the node associated with the road. I'm not certain about how // this works, but in NOD2 each road has a reference to only one node. // This is that node. private RouteNode node; // the first point in the road is a node (the above routing node) private boolean startsWithNode = true; // number of nodes in the road private int nnodes; // always appears to be set private int nod2Flags = NOD2_FLAG_UNK; // The data for Table A private int tabAInfo; private int tabAAccess; // for diagnostic purposes private final long id; private final String name; private List numbersList; private List cityList; private List zipList; private int nodeCount; public RoadDef(long id, String name) { this.id = id; this.name = name; } /** * A constructor that is used when reading a file and you know the NET1 offset. When writing * the offsetNet1 field is filled in during the writing process. * @param id Road id * @param net1offset The offset in the road defs section of the NET file. * @param name The main of the road. */ public RoadDef(long id, int net1offset, String name) { this.id = id; this.offsetNet1 = net1offset; this.name = name; } // for diagnostic purposes public String toString() { // assumes id is an OSM id String browseURL = "http://www.openstreetmap.org/browse/way/" + id; //if(getName() != null) // return "(" + getName() + ", " + browseURL + ")"; //else return "(" + browseURL + ")"; } public String getName() { if (name != null) return name; if (labels[0] != null) return labels[0].toString(); return null; } public long getId() { return id; } /** * This is for writing to NET1. * @param writer A writer that is positioned within NET1. */ void writeNet1(ImgFileWriter writer, int numCities, int numZips) { if (numlabels == 0) return; assert numlabels > 0; Zip zip = getZips().isEmpty() ? null : getZips().get(0); City city = getCities().isEmpty() ? null: getCities().get(0); offsetNet1 = writer.position(); NumberPreparer numbers = null; if (numbersList != null) { numbers = new NumberPreparer(numbersList, zip, city, numCities, numZips); if (!numbers.prepare()){ numbers = null; log.warn("Invalid housenumbers in",this.toString()); } } writeLabels(writer); if (numbers != null && numbers.getSwapped()) { netFlags |= 0x20; // swapped default; left=even, right=odd } writer.put((byte) netFlags); writer.put3(roadLength); int maxlevel = writeLevelCount(writer); writeLevelDivs(writer, maxlevel); if((netFlags & NET_FLAG_ADDRINFO) != 0) { nodeCount--; if (nodeCount + 2 != nnodes){ log.error("internal error? The nodeCount doesn't match value calculated by RoadNetWork:",this); } writer.put((byte) (nodeCount & 0xff)); // lo bits of node count int code = ((nodeCount >> 8) & 0x3); // top bits of node count int len, flag; ByteArrayOutputStream zipBuf = null, cityBuf = null; len = (numbers == null) ? 0: numbers.zipWriter.getBuffer().size(); if (len > 0){ zipBuf = numbers.zipWriter.getBuffer(); flag = (len > 255) ? 1 : 0; } else flag = (zip == null) ? 3 : 2; code |= flag << 2; len = (numbers == null) ? 0: numbers.cityWriter.getBuffer().size(); if (len > 0){ cityBuf = numbers.cityWriter.getBuffer(); flag = (len > 255) ? 1 : 0; } else flag = (city == null) ? 3 : 2; code |= flag << 4; len = (numbers == null) ? 0 : numbers.fetchBitStream().getLength(); if (len > 0){ flag = (len > 255) ? 1 : 0; } else flag = 3; code |= flag << 6; writer.put((byte)code); // System.out.printf("%d %d %d\n", (code >> 2 & 0x3), (code >> 4 & 0x3), (code >> 6 & 0x3)); if (zipBuf != null){ len = zipBuf.size(); if (len > 255) writer.putChar((char) len); else writer.put((byte) len); writer.put(zipBuf.toByteArray()); } else { if(zip != null) { char zipIndex = (char)zip.getIndex(); if(numZips > 255) writer.putChar(zipIndex); else writer.put((byte)zipIndex); } } if (cityBuf != null){ len = cityBuf.size(); if (len > 255) writer.putChar((char) len); else writer.put((byte) len); writer.put(cityBuf.toByteArray()); } else { if(city != null) { char cityIndex = (char)city.getIndex(); if(numCities > 255) writer.putChar(cityIndex); else writer.put((byte)cityIndex); } } if (numbers != null) { BitWriter bw = numbers.fetchBitStream(); if (bw.getLength() > 255) writer.putChar((char) bw.getLength()); else writer.put((byte) bw.getLength()); writer.put(bw.getBytes(), 0, bw.getLength()); } } if (hasNodInfo()) { // This is the offset of an entry in NOD2 int val = offsetNod2; if (val < 0x7fff) { writer.put((byte) 1); writer.putChar((char) val); } else { writer.put((byte) 2); writer.put3(val); } } } private void writeLabels(ImgFileWriter writer) { for (int i = 0; i < numlabels; i++) { Label l = labels[i]; int ptr = l.getOffset(); if (i == (numlabels-1)) ptr |= 0x800000; writer.put3(ptr); } } public void putSortedRoadEntry(ImgFileWriter writer, Label label) { for(int i = 0; i < labels.length && labels[i] != null; ++i) { if(labels[i].equals(label)) { writer.put3((i << 22) | offsetNet1); return; } } } private int writeLevelCount(ImgFileWriter writer) { int maxlevel = getMaxZoomLevel(); for (int i = 0; i <= maxlevel; i++) { List l = roadIndexes.get(i); int b = (l == null) ? 0 : l.size(); assert b < 0x80 : "too many polylines at level " + i; if (i == maxlevel) b |= 0x80; writer.put((byte) b); } return maxlevel; } private void writeLevelDivs(ImgFileWriter writer, int maxlevel) { for (int i = 0; i <= maxlevel; i++) { List l = roadIndexes.get(i); if (l != null) { for (RoadIndex ri : l) ri.write(writer); } } } public void addLabel(Label l) { int i; for (i = 0; i < MAX_LABELS && labels[i] != null; ++i) { if (l.equals(labels[i])) { // label already present return; } } if (i < MAX_LABELS) { labels[i] = l; ++numlabels; } else log.warn(this.toString() + " discarding extra label (already have " + MAX_LABELS + ")"); } public Label[] getLabels() { return labels; } /** * Add a polyline to this road. * * References to these are written to NET. At a given zoom * level, we're writing these in the order we get them, * which must(!) be the order the segments have * in the road. */ public void addPolylineRef(Polyline pl) { if(log.isDebugEnabled()) log.debug("adding polyline ref", this, pl.getSubdiv()); int level = pl.getSubdiv().getZoom().getLevel(); List l = roadIndexes.get(level); if (l == null) { l = new ArrayList<>(); roadIndexes.put(level, l); } l.add(new RoadIndex(pl)); if (level == 0) { nodeCount += pl.getNodeCount(hasHouseNumbers()); } } private int getMaxZoomLevel() { return roadIndexes.lastKey(); } public boolean connectedTo(RoadDef other) { List l = roadIndexes.get(0); if(l == null) return false; List ol = other.roadIndexes.get(0); if(ol == null) return false; for(RoadIndex ri : l) for(RoadIndex ori : ol) if(ri.getLine().sharesNodeWith(ori.getLine())) return true; return false; } public boolean sameDiv(RoadDef other) { return getStartSubdivNumber() == other.getStartSubdivNumber(); } public int getStartSubdivNumber() { Integer key = roadIndexes.firstKey(); return roadIndexes.get(key).get(0).getLine().getSubdiv().getNumber(); } /** * Set the road length (in meters). */ public void setLength(double lenInMeter) { roadLength = NODHeader.metersToRaw(lenInMeter); } public boolean hasHouseNumbers() { return numbersList != null && !numbersList.isEmpty(); } /* * Everything that's relevant for writing to RGN. */ class Offset { final int position; final int flags; Offset(int position, int flags) { this.position = position; this.flags = flags; } int getPosition() { return position; } int getFlags() { return flags; } } /** * Add a target location in the RGN section where we should write the * offset of this road def when it is written to NET. * * @param position The offset in RGN. * @param flags The flags that should be set. */ public void addOffsetTarget(int position, int flags) { rgnOffsets.add(new Offset(position, flags)); } /** * Write into the RGN the offset in net1 of this road. * @param rgn A writer for the rgn file. */ void writeRgnOffsets(ImgFileWriter rgn) { if (offsetNet1 >= 0x400000) throw new MapFailedException("Overflow of the NET1. The tile (" + log.threadTag() + ") must be split so that there are fewer roads in it"); for (Offset off : rgnOffsets) { rgn.position(off.getPosition()); rgn.put3(offsetNet1 | off.getFlags()); } } private boolean internalNodes; /** * Does the road have any nodes besides start and end? * These can be number nodes or routing nodes. * This affects whether we need to write extra bits in * the bitstream in RGN. */ public boolean hasInternalNodes() { return internalNodes; } public void setInternalNodes(boolean n) { internalNodes = n; } /** * Set the routing node associated with this road. * * This implies that the road has an entry in NOD 2 * which will be pointed at from NET 1. */ public void setNode(RouteNode node) { if (skipAddToNOD) return; netFlags |= NET_FLAG_NODINFO; this.node = node; } public RouteNode getNode(){ return node; } private boolean hasNodInfo() { return (netFlags & NET_FLAG_NODINFO) != 0; } public void setStartsWithNode(boolean s) { startsWithNode = s; } public void setNumNodes(int n) { nnodes = n; } public void setNumbersList(List numbersList) { if (numbersList != null && !numbersList.isEmpty()) { this.numbersList = numbersList; netFlags |= NET_FLAG_ADDRINFO; } } public List getNumbersList() { return numbersList; } /** * Write this road's NOD2 entry. * * Stores the writing position to be able to link here * from NET 1 later. * * @param writer A writer positioned in NOD2. */ public void writeNod2(ImgFileWriter writer) { if (!hasNodInfo()) return; if (skipAddToNOD){ // should not happen log.error("internal error: writeNod2 called for roaddef with skipAddToNOD=true"); return; } log.debug("writing nod2"); offsetNod2 = writer.position(); writer.put((byte) nod2Flags); writer.put3(node.getOffsetNod1()); // offset in nod1 // this is related to the number of nodes, but there // is more to it... // For now, shift by one if the first node is not a // routing node. // If the road has house numbers, we count also // the number nodes, and these get a 0 in the bit stream. int nbits = nnodes; if (!startsWithNode) nbits++; writer.putChar((char) nbits); boolean[] bits = new boolean[nbits]; if (hasHouseNumbers()){ int off = startsWithNode ? 0 :1; for (int i = 0; i < bits.length; i++){ if (nod2BitSet.get(i)) bits[i+off] = true; } } else { for (int i = 0; i < bits.length; i++) bits[i] = true; if (!startsWithNode) bits[0] = false; } for (int i = 0; i < bits.length; i += 8) { int b = 0; for (int j = 0; j < 8 && j < bits.length - i; j++) if (bits[i+j]) b |= 1 << j; writer.put((byte) b); } } /* * Everything that's relevant for writing out Table A. * * Storing this info in the RoadDef means that each * arc gets the same version of the below info, which * makes sense for the moment considering polish format * doesn't provide for different speeds and restrictions * for segments of roads. */ /** * Return the offset of this road's NET1 entry. Assumes * writeNet1() has been called. */ public int getOffsetNet1() { return offsetNet1; } /** * Flag that a toll must be payed when using this road. */ public void setToll() { tabAInfo |= TABA_FLAG_TOLL; } /** * Flag that the road has a carpool lane.
* Warning: This bit does not seem to work. Maybe it does not control * the carpool flag. */ public void setCarpoolLane() { tabAAccess |= TABAACCESS_FLAG_CARPOOL; } /** * Sets the flag that routing is allowed only if the route starts or * end on this road. */ public void setNoThroughRouting() { tabAAccess |= TABAACCESS_FLAG_NOTHROUGHROUTE; } /** * @return allowed vehicles in mkgmap format */ public byte getAccess() { return mkgmapAccess; } /** * Set allowed vehicles * @param mkgmapAccess bit mask in mkgmap format */ public void setAccess(byte mkgmapAccess) { this.mkgmapAccess = mkgmapAccess; // translate internal format to that used in TableA //clear the corresponding bits tabAAccess &= ~(0xc077); if (mkgmapAccess == (byte) 0xff) return; // all vehicles allowed if ((mkgmapAccess & AccessTagsAndBits.FOOT) == 0) tabAAccess |= TABAACCESS_FLAG_NO_FOOT; if ((mkgmapAccess & AccessTagsAndBits.BIKE) == 0) tabAAccess |=TABAACCESS_FLAG_NO_BIKE; if ((mkgmapAccess & AccessTagsAndBits.CAR) == 0) tabAAccess |=TABAACCESS_FLAG_NO_CAR; if ((mkgmapAccess & AccessTagsAndBits.DELIVERY) == 0) tabAAccess |=TABAACCESS_FLAG_NO_DELIVERY; if ((mkgmapAccess & AccessTagsAndBits.TRUCK) == 0) tabAAccess |=TABAACCESS_FLAG_NO_TRUCK; if ((mkgmapAccess & AccessTagsAndBits.BUS) == 0) tabAAccess |=TABAACCESS_FLAG_NO_BUS; if ((mkgmapAccess & AccessTagsAndBits.TAXI) == 0) tabAAccess |=TABAACCESS_FLAG_NO_TAXI; if ((mkgmapAccess & AccessTagsAndBits.EMERGENCY) == 0) tabAAccess |=TABAACCESS_FLAG_NO_EMERGENCY; } public int getTabAInfo() { return tabAInfo; } public int getTabAAccess() { return tabAAccess; } /* * These affect various parts. */ private int roadClass = -1; // road class that goes in various places (really?) public void setRoadClass(int roadClass) { assert roadClass < 0x08; /* for RouteArcs to get as their "destination class" */ this.roadClass = roadClass; /* for Table A */ int shifted = (roadClass << 4) & 0xff; tabAInfo |= shifted; /* for NOD 2 */ nod2Flags |= shifted; } public int getRoadClass() { assert roadClass >= 0 : "roadClass not set"; return roadClass; } public void setSpeed(int speed) { assert speed < 0x08; /* for Table A */ tabAInfo |= speed; /* for NOD 2 */ nod2Flags |= (speed << 1); } public int getRoadSpeed() { return tabAInfo & 7; } public void setOneway() { tabAInfo |= TABA_FLAG_ONEWAY; netFlags |= NET_FLAG_ONEWAY; } public boolean isOneway() { return (netFlags & NET_FLAG_ONEWAY) != 0; } public void addCityIfNotPresent(City city) { if (city == null){ log.error("trying to add null value to city list in road",this); return; } netFlags |= NET_FLAG_ADDRINFO; if (cityList == null){ cityList = new ArrayList<>(2); } if (cityList.contains(city) == false) cityList.add(city); } public void addZipIfNotPresent(Zip zip) { if (zip == null){ log.error("trying to add null value to zip list in road",this); return; } netFlags |= NET_FLAG_ADDRINFO; if (zipList == null){ zipList = new ArrayList<>(2); } if (zipList.contains(zip) == false) zipList.add(zip); } public List getCities(){ if (cityList == null) return Collections.emptyList(); return cityList; } public List getZips(){ if (zipList == null) return Collections.emptyList(); return zipList; } public boolean paved() { return paved; } public void paved(boolean p) { paved = p; } public void ferry(boolean f) { ferry = f; } public boolean ferry() { return ferry; } public void setRoundabout(boolean r) { roundabout = r; } public boolean isRoundabout() { return roundabout; } public void setLinkRoad(boolean lr) { linkRoad = lr; } public boolean isLinkRoad() { return linkRoad; } public void setSynthesised(boolean s) { synthesised = s; } public boolean isSynthesised() { return synthesised; } public void doFlareCheck(boolean fc) { flareCheck = fc; } public boolean doFlareCheck() { return flareCheck; } public boolean messagePreviouslyIssued(String key) { if(messageIssued == null) messageIssued = new HashSet<>(); boolean previouslyIssued = messageIssued.contains(key); messageIssued.add(key); return previouslyIssued; } public void setNod2BitSet(BitSet bs) { if (skipAddToNOD) return; nod2BitSet = bs; } public boolean skipAddToNOD() { return skipAddToNOD; } public void skipAddToNOD(boolean skip) { this.skipAddToNOD = skip; } public void resetImgData() { zipList = null; cityList = null; if (numbersList != null){ for (Numbers num : numbersList){ for (int side = 0; side < 2; side++){ boolean left = side == 0; CityInfo ci = num.getCityInfo(left); if (ci != null) ci.setImgCity(null); ZipCodeInfo z = num.getZipCodeInfo(left); if (z != null) z.setImgZip(null); } } } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RouteNode.java0000644000076400007640000005702212644640626023166 0ustar stevesteve/* * Copyright (C) 2008 * * 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. * * Create date: 07-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import it.unimi.dsi.fastutil.ints.IntArrayList; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.CoordNode; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.log.Logger; /** * A routing node with its connections to other nodes via roads. * * @author Steve Ratcliffe */ public class RouteNode implements Comparable { private static final Logger log = Logger.getLogger(RouteNode.class); /* * 1. instantiate * 2. setCoord, addArc * arcs, coords set * 3. write * node offsets set in all nodes * 4. writeSecond */ // Values for the first flag byte at offset 1 private static final int MAX_DEST_CLASS_MASK = 0x07; private static final int F_BOUNDARY = 0x08; private static final int F_RESTRICTIONS = 0x10; private static final int F_LARGE_OFFSETS = 0x20; private static final int F_ARCS = 0x40; // only used internally in mkgmap private static final int F_DISCARDED = 0x100; // node has been discarded private int offsetNod1 = -1; // arcs from this node private final List arcs = new ArrayList(4); // restrictions at (via) this node private final List restrictions = new ArrayList(); private int flags; private final CoordNode coord; private char latOff; private char lonOff; private List throughRoutes; // contains the maximum of roads this node is on, written with the flags // field. It is also used for the calculation of the destination class on // arcs. private byte nodeClass; private byte nodeGroup = -1; public RouteNode(Coord coord) { this.coord = (CoordNode) coord; setBoundary(this.coord.getOnBoundary()); } private boolean haveLargeOffsets() { return (flags & F_LARGE_OFFSETS) != 0; } protected void setBoundary(boolean b) { if (b) flags |= F_BOUNDARY; else flags &= (~F_BOUNDARY) & 0xff; } public boolean isBoundary() { return (flags & F_BOUNDARY) != 0; } public void addArc(RouteArc arc) { arcs.add(arc); byte cl = (byte) arc.getRoadDef().getRoadClass(); if(log.isDebugEnabled()) log.debug("adding arc", arc.getRoadDef(), cl); if (cl > nodeClass) nodeClass = cl; flags |= F_ARCS; } public void addRestriction(RouteRestriction restr) { restrictions.add(restr); flags |= F_RESTRICTIONS; } /** * get all direct arcs to the given node and the given way id * @param otherNode * @param roadId * @return */ public List getDirectArcsTo(RouteNode otherNode, long roadId) { List result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ if(a.getRoadDef().getId() == roadId) result.add(a); } } return result; } /** * get all direct arcs on a given way id * @param roadId * @return */ public List getDirectArcsOnWay(long roadId) { List result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect()){ if(a.getRoadDef().getId() == roadId) result.add(a); } } return result; } /** * Find arc to given node on given road. * @param otherNode * @param roadDef * @return */ public RouteArc getDirectArcTo(RouteNode otherNode, RoadDef roadDef) { for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ if(a.getRoadDef()== roadDef) return a; } } return null; } /** * Provide an upper bound to the size (in bytes) that * writing this node will take. * * Should be called only after arcs and restrictions * have been set. The size of arcs depends on whether * or not they are internal to the RoutingCenter. */ public int boundSize() { return 1 // table pointer + 1 // flags + 4 // assume large offsets required + arcsSize() + restrSize(); } private int arcsSize() { int s = 0; for (RouteArc arc : arcs) { s += arc.boundSize(); } return s; } private int restrSize() { return 2*restrictions.size(); } /** * Writes a nod1 entry. */ public void write(ImgFileWriter writer) { if(log.isDebugEnabled()) log.debug("writing node, first pass, nod1", coord.getId()); offsetNod1 = writer.position(); assert offsetNod1 < 0x1000000 : "node offset doesn't fit in 3 bytes"; assert (flags & F_DISCARDED) == 0 : "attempt to write discarded node"; writer.put((byte) 0); // will be overwritten later flags |= (nodeClass & MAX_DEST_CLASS_MASK); // max. road class of any outgoing road writer.put((byte) flags); if (haveLargeOffsets()) { writer.putInt((latOff << 16) | (lonOff & 0xffff)); } else { writer.put3((latOff << 12) | (lonOff & 0xfff)); } if (!arcs.isEmpty()) { boolean useCompactDirs = true; IntArrayList initialHeadings = new IntArrayList(arcs.size()+1); RouteArc lastArc = null; for (RouteArc arc: arcs){ if (lastArc == null || lastArc.getIndexA() != arc.getIndexA() || lastArc.isForward() != arc.isForward()){ int dir = RouteArc.directionFromDegrees(arc.getInitialHeading()); dir = dir & 0xf0; if (initialHeadings.contains(dir)){ useCompactDirs = false; break; } initialHeadings.add(dir); } else { // } lastArc = arc; } initialHeadings.add(0); // add dummy 0 so that we don't have to check for existence arcs.get(arcs.size() - 1).setLast(); lastArc = null; int index = 0; for (RouteArc arc: arcs){ Byte compactedDir = null; if (useCompactDirs){ if (lastArc == null || lastArc.getIndexA() != arc.getIndexA() || lastArc.isForward() != arc.isForward()){ if (index % 2 == 0) compactedDir = (byte) ((initialHeadings.get(index) >> 4) | initialHeadings.getInt(index+1)); index++; } } arc.write(writer, lastArc, useCompactDirs, compactedDir); lastArc = arc; } } if (!restrictions.isEmpty()) { restrictions.get(restrictions.size() - 1).setLast(); for (RouteRestriction restr : restrictions) restr.writeOffset(writer); } } /** * Writes a nod3 /nod4 entry. */ public void writeNod3OrNod4(ImgFileWriter writer) { assert isBoundary() : "trying to write nod3 for non-boundary node"; writer.put3(coord.getLongitude()); writer.put3(coord.getLatitude()); writer.put3(offsetNod1); } public void discard() { // mark the node as having been discarded flags |= F_DISCARDED; } public int getOffsetNod1() { if((flags & F_DISCARDED) != 0) { // return something so that the program can continue return 0; } assert offsetNod1 != -1: "failed for node " + coord.getId() + " at " + coord.toDegreeString(); return offsetNod1; } public void setOffsets(Coord centralPoint) { if(log.isDebugEnabled()) log.debug("center", centralPoint, ", coord", coord.toDegreeString()); setLatOff(coord.getLatitude() - centralPoint.getLatitude()); setLonOff(coord.getLongitude() - centralPoint.getLongitude()); } public Coord getCoord() { return coord; } private void checkOffSize(int off) { if (off > 0x7ff || off < -0x800) // does off fit in signed 12 bit quantity? flags |= F_LARGE_OFFSETS; // does off fit in signed 16 bit quantity? assert (off <= 0x7fff && off >= -0x8000); } private void setLatOff(int latOff) { if(log.isDebugEnabled()) log.debug("lat off", Integer.toHexString(latOff)); this.latOff = (char) latOff; checkOffSize(latOff); } private void setLonOff(int lonOff) { if(log.isDebugEnabled()) log.debug("long off", Integer.toHexString(lonOff)); this.lonOff = (char) lonOff; checkOffSize(lonOff); } /** * Second pass over the nodes. Fill in pointers and Table A indices. */ public void writeSecond(ImgFileWriter writer) { for (RouteArc arc : arcs) arc.writeSecond(writer); } /** * Return the node's class, which is the maximum of * classes of the roads it's on. */ public int getNodeClass() { return nodeClass; } public Iterable arcsIteration() { return new Iterable() { public Iterator iterator() { return arcs.iterator(); } }; } public List getRestrictions() { return restrictions; } public String toString() { return String.valueOf(coord.getId()); } /* * For sorting node entries in NOD 3. */ public int compareTo(RouteNode otherNode) { return coord.compareTo(otherNode.getCoord()); } public void checkRoundabouts() { List roundaboutArcs = new ArrayList(); for(RouteArc a : arcs) { // ignore ways that have been synthesised by mkgmap if(!a.getRoadDef().isSynthesised() && a.isDirect() && a.getRoadDef().isRoundabout()) { roundaboutArcs.add(a); } } if(arcs.size() > 1 && roundaboutArcs.size() == 1) { if(roundaboutArcs.get(0).isForward()) log.warn("Roundabout " + roundaboutArcs.get(0).getRoadDef() + " starts at " + coord.toOSMURL()); else log.warn("Roundabout " + roundaboutArcs.get(0).getRoadDef() + " ends at " + coord.toOSMURL()); } if(roundaboutArcs.size() > 2) { for(RouteArc fa : arcs) { if(fa.isForward() && fa.isDirect()) { RoadDef rd = fa.getRoadDef(); for(RouteArc fb : arcs) { if(fb != fa && fb.isDirect() && fa.getPointsHash() == fb.getPointsHash() && ((fb.isForward() && fb.getDest() == fa.getDest()) || (!fb.isForward() && fb.getSource() == fa.getDest()))) { if(!rd.messagePreviouslyIssued("roundabout forks/overlaps")) { log.warn("Roundabout " + rd + " overlaps " + fb.getRoadDef() + " at " + coord.toOSMURL()); } } else if(fa != fb && fb.isForward()) { if(!rd.messagePreviouslyIssued("roundabout forks/overlaps")) { log.warn("Roundabout " + rd + " forks at " + coord.toOSMURL()); } } } } } } } // determine "distance" between two nodes on a roundabout private static int roundaboutSegmentLength(final RouteNode n1, final RouteNode n2) { List seen = new ArrayList(); int len = 0; RouteNode n = n1; boolean checkMoreLinks = true; while(checkMoreLinks && !seen.contains(n)) { checkMoreLinks = false; seen.add(n); for(RouteArc a : n.arcs) { if(a.isForward() && a.getRoadDef().isRoundabout() && !a.getRoadDef().isSynthesised()) { len += a.getLength(); n = a.getDest(); if(n == n2) return len; checkMoreLinks = true; break; } } } // didn't find n2 return Integer.MAX_VALUE; } // sanity check roundabout flare roads - the flare roads connect a // two-way road to a roundabout using short one-way segments so // the resulting sub-junction looks like a triangle with two // corners of the triangle being attached to the roundabout and // the last corner being connected to the two-way road public void checkRoundaboutFlares(int maxFlareLengthRatio) { for(RouteArc r : arcs) { // see if node has a forward arc that is part of a // roundabout if(!r.isForward() || !r.isDirect() || !r.getRoadDef().isRoundabout() || r.getRoadDef().isSynthesised()) continue; // follow the arc to find the first node that connects the // roundabout to a non-roundabout segment RouteNode nb = r.getDest(); List seen = new ArrayList(); seen.add(this); while (true) { if (seen.contains(nb)) { // looped - give up nb = null; break; } // remember we have seen this node seen.add(nb); boolean connectsToNonRoundaboutSegment = false; RouteArc nextRoundaboutArc = null; for (RouteArc nba : nb.arcs) { if (nba.isDirect() == false) continue; if (!nba.getRoadDef().isSynthesised()) { if (nba.getRoadDef().isRoundabout()) { if (nba.isForward()) nextRoundaboutArc = nba; } else connectsToNonRoundaboutSegment = true; } } if (connectsToNonRoundaboutSegment) { // great, that's what we're looking for break; } if (nextRoundaboutArc == null) { // not so good, the roundabout stops in mid air? nb = null; break; } nb = nextRoundaboutArc.getDest(); } if(nb == null) { // something is not right so give up continue; } // now try and find the two arcs that make up the // triangular "flare" connected to both ends of the // roundabout segment for(RouteArc fa : arcs) { if(!fa.isDirect() || !fa.getRoadDef().doFlareCheck()) continue; for(RouteArc fb : nb.arcs) { if(!fb.isDirect() || !fb.getRoadDef().doFlareCheck()) continue; if(fa.getDest() == fb.getDest()) { // found the 3rd point of the triangle that // should be connecting the two flare roads // first, special test required to cope with // roundabouts that have a single flare and no // other connections - only check the flare // for the shorter of the two roundabout // segments if(roundaboutSegmentLength(this, nb) >= roundaboutSegmentLength(nb, this)) continue; if(maxFlareLengthRatio > 0) { // if both of the flare roads are much // longer than the length of the // roundabout segment, they are probably // not flare roads at all but just two // roads that meet up - so ignore them final int maxFlareLength = roundaboutSegmentLength(this, nb) * maxFlareLengthRatio; if(maxFlareLength > 0 && fa.getLength() > maxFlareLength && fb.getLength() > maxFlareLength) { continue; } } // now check the flare roads for direction and // oneway // only issue one warning per flare if(!fa.isForward()) log.warn("Outgoing roundabout flare road " + fa.getRoadDef() + " points in wrong direction? " + fa.getSource().coord.toOSMURL()); else if(fb.isForward()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " points in wrong direction? " + fb.getSource().coord.toOSMURL()); else if(!fa.getRoadDef().isOneway()) log.warn("Outgoing roundabout flare road " + fa.getRoadDef() + " is not oneway? " + fa.getSource().coord.toOSMURL()); else if(!fb.getRoadDef().isOneway()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " is not oneway? " + fb.getDest().coord.toOSMURL()); else { // check that the flare road arcs are not // part of a longer way for(RouteArc a : fa.getDest().arcs) { if(a.isDirect() && a.getDest() != this && a.getDest() != nb) { if(a.getRoadDef() == fa.getRoadDef()) log.warn("Outgoing roundabout flare road " + fb.getRoadDef() + " does not finish at flare? " + fa.getDest().coord.toOSMURL()); else if(a.getRoadDef() == fb.getRoadDef()) log.warn("Incoming roundabout flare road " + fb.getRoadDef() + " does not start at flare? " + fb.getDest().coord.toOSMURL()); } } } } } } } } public void reportSimilarArcs() { for(int i = 0; i < arcs.size(); ++i) { RouteArc arci = arcs.get(i); if (arci.isDirect() == false) continue; for(int j = i + 1; j < arcs.size(); ++j) { RouteArc arcj = arcs.get(j); if (arcj.isDirect() == false) continue; if(arci.getDest() == arcj.getDest() && arci.getLength() == arcj.getLength() && arci.getPointsHash() == arcj.getPointsHash()) { log.error(arci.isForward(),arcj.isForward()); log.warn("Similar arcs (" + arci.getRoadDef() + " and " + arcj.getRoadDef() + ") from " + coord.toOSMURL()); } } } } public void addThroughRoute(long roadIdA, long roadIdB) { if(throughRoutes == null) throughRoutes = new ArrayList(); boolean success = false; for(RouteArc arc1 : arcs) { if(arc1.getRoadDef().getId() == roadIdA) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdB) { throughRoutes.add(new RouteArc[] { arc1.getReverseArc(), arc2 }); success = true; break; } } } else if(arc1.getRoadDef().getId() == roadIdB) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdA) { throughRoutes.add(new RouteArc[] { arc1.getReverseArc(), arc2 }); success = true; break; } } } } /* for(RouteArc arc1 : incomingArcs) { if(arc1.getRoadDef().getId() == roadIdA) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdB) { throughRoutes.add(new RouteArc[] { arc1, arc2 }); success = true; break; } } } else if(arc1.getRoadDef().getId() == roadIdB) { for(RouteArc arc2 : arcs) { if(arc2.getRoadDef().getId() == roadIdA) { throughRoutes.add(new RouteArc[] { arc1, arc2 }); success = true; break; } } } } */ if(success) log.info("Added through route between ways " + roadIdA + " and " + roadIdB + " at " + coord.toOSMURL()); else log.warn("Failed to add through route between ways " + roadIdA + " and " + roadIdB + " at " + coord.toOSMURL() + " - perhaps they don't meet here?"); } /** * For each arc on the road, check if we can add indirect arcs to * other nodes of the same road. This is done if the other node * lies on a different road with a higher road class than the * highest other road of the target node of the arc. We do this * for both forward and reverse arcs. Multiple indirect arcs * may be added for each Node. An indirect arc will * always point to a higher road than the previous arc. * The length and direct bearing of the additional arc is measured * from the target node of the preceding arc to the new target node. * The initial bearing doesn't really matter as it is not written * for indirect arcs. * @param road * @param maxRoadClass */ public void addArcsToMajorRoads(RoadDef road){ assert road.getNode() == this; RouteNode current = this; // the nodes of this road List nodes = new ArrayList<>(); // the forward arcs of this road List forwardArcs = new ArrayList<>(); // will contain the highest other road of each node IntArrayList forwardArcPositions = new IntArrayList(); List reverseArcs = new ArrayList<>(); IntArrayList reverseArcPositions = new IntArrayList(); // collect the nodes of the road and remember the arcs between them nodes.add(current); while (current != null){ RouteNode next = null; for (int i = 0; i < current.arcs.size(); i++){ RouteArc arc = current.arcs.get(i); if (arc.getRoadDef() == road){ if (arc.isDirect()){ if (arc.isForward()){ next = arc.getDest(); nodes.add(next); forwardArcs.add(arc); forwardArcPositions.add(i); } else { reverseArcPositions.add(i); reverseArcs.add(arc); } } } } current = next; } if (nodes.size() < 3) return; // System.out.println(road + " " + nodes.size() + " " + forwardArcs.size()); ArrayList newArcs = new ArrayList<>(); IntArrayList arcPositions = forwardArcPositions; List roadArcs = forwardArcs; for (int dir = 0; dir < 2; dir++){ // forward arcs first for (int i = 0; i + 2 < nodes.size(); i++){ RouteNode sourceNode = nodes.get(i); // original source node of direct arc RouteNode stepNode = nodes.get(i+1); RouteArc arcToStepNode = roadArcs.get(i); assert arcToStepNode.getDest() == stepNode; int currentClass = arcToStepNode.getArcDestClass(); int finalClass = road.getRoadClass(); if (finalClass <= currentClass) continue; newArcs.clear(); double partialArcLength = 0; double pathLength = arcToStepNode.getLengthInMeter(); for (int j = i+2; j < nodes.size(); j++){ RouteArc arcToDest = roadArcs.get(j-1); partialArcLength += arcToDest.getLengthInMeter(); pathLength += arcToDest.getLengthInMeter(); int cl = nodes.get(j).getGroup(); if (cl > currentClass){ if (cl > finalClass) cl = finalClass; currentClass = cl; // create indirect arc from node i+1 to node j RouteNode destNode = nodes.get(j); Coord c1 = sourceNode.getCoord(); Coord c2 = destNode.getCoord(); RouteArc newArc = new RouteArc(road, sourceNode, destNode, roadArcs.get(i).getInitialHeading(), // not used c1.bearingTo(c2), partialArcLength, // from stepNode to destNode on road pathLength, // from sourceNode to destNode on road c1.distance(c2), c1.hashCode() + c2.hashCode()); if (arcToStepNode.isDirect()) arcToStepNode.setMaxDestClass(0); else newArc.setMaxDestClass(cl); if (dir == 0) newArc.setForward(); newArc.setIndirect(); newArcs.add(newArc); arcToStepNode = newArc; stepNode = destNode; partialArcLength = 0; if (cl >= finalClass) break; } } if (newArcs.isEmpty() == false){ int directArcPos = arcPositions.getInt(i); assert nodes.get(i).arcs.get(directArcPos).isDirect(); assert nodes.get(i).arcs.get(directArcPos).getRoadDef() == newArcs.get(0).getRoadDef(); assert nodes.get(i).arcs.get(directArcPos).isForward() == newArcs.get(0).isForward(); nodes.get(i).arcs.addAll(directArcPos + 1, newArcs); if (dir == 0 && i > 0){ // check if the inserted arcs change the position of the direct reverse arc int reverseArcPos = reverseArcPositions.get(i-1); // i-1 because first node doesn't have reverse arc if (directArcPos < reverseArcPos) reverseArcPositions.set(i - 1, reverseArcPos + newArcs.size()); } } } if (dir > 0) break; // reverse the arrays for the other direction Collections.reverse(reverseArcs); Collections.reverse(reverseArcPositions); Collections.reverse(nodes); arcPositions = reverseArcPositions; roadArcs = reverseArcs; } } /** * Find the class group of the node. Rules: * 1. Find the highest class which is used more than once. * 2. Otherwise: use the class if the only one, or else the n-1 class. * (eg: if [1,] then use 1, if [1,2,] then use 1, if [1,2,3,] then use 2. * * @return the class group */ public int getGroup() { if (nodeGroup < 0){ HashSet roads = new HashSet<>(); for (RouteArc arc: arcs){ roads.add(arc.getRoadDef()); } int[] classes = new int[5]; int numClasses = 0; // find highest class that is used more than once for (RoadDef road: roads){ int cl = road.getRoadClass(); int n = ++classes[cl]; if (n == 1) numClasses++; else if (n > 1 && cl > nodeGroup) nodeGroup = (byte) cl; } if (nodeGroup >= 0) return nodeGroup; if (numClasses == 1) nodeGroup = nodeClass; // only one class else { // find n-1 class int n = 0; for (int cl = 4; cl >= 0; cl--){ if (classes[cl] > 0){ if (n == 1){ nodeGroup = (byte) cl; break; } n++; } } } } return nodeGroup; } public List getArcs() { return arcs; } public int hashCode(){ return getCoord().getId(); } public List getDirectArcsBetween(RouteNode otherNode) { List result = new ArrayList<>(); for(RouteArc a : arcs){ if(a.isDirect() && a.getDest() == otherNode){ result.add(a); } } return result; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NETFileReader.java0000644000076400007640000002064612533237270023627 0ustar stevesteve/* * 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 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.imgfmt.app.net; import it.unimi.dsi.fastutil.ints.IntArrayList; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.lbl.LBLFileReader; import uk.me.parabola.imgfmt.app.lbl.Zip; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * Read the NET file. */ public class NETFileReader extends ImgFile { private final NETHeader netHeader = new NETHeader(); // To begin with we only need LBL offsets. private final Map offsetLabelMap = new HashMap(); private List offsets; private List cities; private int citySize; private List zips; private int zipSize; private LBLFileReader labels; public NETFileReader(ImgChannel chan) { setHeader(netHeader); setReader(new BufferedImgFileReader(chan)); netHeader.readHeader(getReader()); readLabelOffsets(); } /** * Get the label offset, given the NET offset. * @param netOffset An offset into NET 1, as found in the road entries in * RGN for example. * @return The offset into LBL as found in NET 1. */ public int getLabelOffset(int netOffset) { Integer off = offsetLabelMap.get(netOffset); if (off == null) return 0; else return off; } /** * Get the list of roads from the net section. * * Saving the bare minimum that is needed, please improve. * @return A list of RoadDefs. Note that currently not everything is * populated in the road def so it can't be written out as is. */ public List getRoads() { ImgFileReader reader = getReader(); int start = netHeader.getRoadDefinitionsStart(); List roads = new ArrayList(); int record = 0; for (int off : offsets) { reader.position(start + off); RoadDef road = new RoadDef(++record, off, null); readLabels(reader, road); byte netFlags = reader.get(); /*int len =*/ reader.getu3(); int[] counts = new int[24]; int level = 0; while (level < 24) { int n = reader.get(); counts[level++] = (n & 0x7f); if ((n & 0x80) != 0) break; } for (int i = 0; i < level; i++) { int c = counts[i]; for (int j = 0; j < c; j++) { /*byte b =*/ reader.get(); /*char sub =*/ reader.getChar(); } } if ((netFlags & RoadDef.NET_FLAG_ADDRINFO) != 0) { char flags2 = reader.getChar(); int zipFlag = (flags2 >> 10) & 0x3; int cityFlag = (flags2 >> 12) & 0x3; int numberFlag = (flags2 >> 14) & 0x3; IntArrayList indexes = new IntArrayList(); fetchZipCityIndexes(reader, zipFlag, zipSize, indexes); for (int index : indexes){ road.addZipIfNotPresent(zips.get(index)); } fetchZipCityIndexes(reader, cityFlag, citySize, indexes); for (int index : indexes){ road.addCityIfNotPresent(cities.get(index)); } fetchNumber(reader, numberFlag); } if ((netFlags & RoadDef.NET_FLAG_NODINFO) != 0) { int nodFlags = reader.get(); int nbytes = nodFlags & 0x3; if (nbytes > 0) { /*int nod = */reader.getUint(nbytes+1); } } roads.add(road); } return roads; } /** * Parse a list of zip/city indexes. * @param reader * @param flag * @param size * @param indexes */ private void fetchZipCityIndexes(ImgFileReader reader, int flag, int size, IntArrayList indexes) { indexes.clear(); if (flag == 2) { // fetch city/zip index int ind = (size == 2)? reader.getChar(): (reader.get() & 0xff); if (ind != 0) indexes.add(ind-1); } else if (flag == 3) { // there is no item } else if (flag == 0) { int n = reader.get() & 0xff; parseList(reader, n, size, indexes); } else if (flag == 1) { int n = reader.getChar(); parseList(reader, n, size, indexes); } else { assert false : "flag is " + flag; } } private void parseList(ImgFileReader reader, int n, int size, IntArrayList indexes) { long endPos = reader.position() + n; int node = 0; // not yet used while (reader.position() < endPos) { int initFlag = reader.get() & 0xff; int skip = (initFlag & 0x1f); initFlag >>= 5; if (initFlag == 7) { // Need to read another byte initFlag = reader.get() & 0xff; skip |= ((initFlag & 0x1f) << 5); initFlag >>= 5; } node += skip + 1; int right = 0, left = 0; if (initFlag == 0) { right = left = getCityOrZip(reader, size, endPos); } else if ((initFlag & 0x4) != 0) { if ((initFlag & 1) == 0) right = 0; if ((initFlag & 2) == 0) left = 0; } else { if ((initFlag & 1) != 0) left = getCityOrZip(reader, size, endPos); if ((initFlag & 2) != 0) right = getCityOrZip(reader, size, endPos); } if (left > 0) indexes.add(left - 1); if (right > 0 && left != right) indexes.add(right - 1); } } private int getCityOrZip(ImgFileReader reader, int size, long endPos) { if (reader.position() > endPos - size) { assert false : "ERRROR overflow"; return 0; } int cnum; if (size == 1) cnum = reader.get() & 0xff; else if (size == 2) cnum = reader.getChar(); else { assert false : "unexpected size value" + size; return 0; } return cnum; } /** * Fetch a block of numbers. * @param reader The reader. * @param numberFlag The flag that says how the block is formatted. */ private void fetchNumber(ImgFileReader reader, int numberFlag) { int n = 0; if (numberFlag == 0) { n = reader.get(); } else if (numberFlag == 1) { n = reader.getChar(); } else if (numberFlag == 3) { // There is no block return; } else { // Possible but don't know what to do in this context assert false; } if (n > 0) reader.get(n); } private void readLabels(ImgFileReader reader, RoadDef road) { for (int i = 0; i < 4; i++) { int lab = reader.getu3(); Label label = labels.fetchLabel(lab & 0x7fffff); road.addLabel(label); if ((lab & 0x800000) != 0) break; } } /** * The first field in NET 1 is a label offset in LBL. Currently we * are only interested in that to convert between a NET 1 offset and * a LBL offset. */ private void readLabelOffsets() { ImgFileReader reader = getReader(); offsets = readOffsets(); int start = netHeader.getRoadDefinitionsStart(); for (int off : offsets) { reader.position(start + off); int labelOffset = reader.getu3(); // TODO what if top bit is not set?, there can be more than one name and we will miss them offsetLabelMap.put(off, labelOffset & 0x7fffff); } } /** * NET 3 contains a list of all the NET 1 record start positions. They * are in alphabetical order of name. So read them in and sort into * memory address order. * @return A list of start offsets in NET 1, sorted by increasing offset. */ private List readOffsets() { int start = netHeader.getSortedRoadsStart(); int end = netHeader.getSortedRoadsEnd(); ImgFileReader reader = getReader(); reader.position(start); List offsets = new ArrayList(); while (reader.position() < end) { int net1 = reader.getu3(); // The offset is stored in the bottom 22 bits. The top 2 bits are an index into the list // of lbl pointers in the net1 entry. Since we pick up all the labels at a particular net1 // entry we only need one of the offsets so pick the first one. int idx = (net1 >> 22) & 0x3; if (idx == 0) offsets.add((net1 & 0x3fffff) << netHeader.getRoadShift()); } // Sort in address order in the hope of speeding up reading. Collections.sort(offsets); return offsets; } public void setCities(List cities) { this.cities = cities; this.citySize = cities.size() > 255? 2: 1; } public void setZips(List zips) { this.zips = zips; this.zipSize = zips.size() > 255? 2: 1; } public void setLabels(LBLFileReader labels) { this.labels = labels; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/GeneralRouteRestriction.java0000644000076400007640000000547212324126566026103 0ustar stevesteve/* * 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 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.imgfmt.app.net; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.CoordNode; /** * A class to collect the data related to routing restrictions * like only-left-turn or no-right-turn * @author GerdP * */ public class GeneralRouteRestriction { public enum RestrType {TYPE_ONLY , TYPE_NOT, TYPE_NO_TROUGH // for elements like barriers, gates, etc. } private final byte exceptionMask; private final RestrType type; private final String sourceDesc; private long fromWayId, toWayId; private CoordNode fromNode, toNode; private List viaWayIds = new ArrayList<>(); private List viaNodes = new ArrayList<>(); private char dirIndicator; // s(traight),l(eft),r(ight),u, ? for unknown public GeneralRouteRestriction(String type, byte exceptionMask, String sourceDesc) { if ("not".equals(type)) this.type = RestrType.TYPE_NOT; else if ("only".equals(type)) this.type = RestrType.TYPE_ONLY; else if ("no_through".equals(type)) this.type = RestrType.TYPE_NO_TROUGH; else throw new IllegalArgumentException("invalid type " + type); this.exceptionMask = exceptionMask; this.sourceDesc = sourceDesc; this.setDirIndicator('?'); } public long getFromWayId() { return fromWayId; } public void setFromWayId(long fromWayId) { this.fromWayId = fromWayId; } public long getToWayId() { return toWayId; } public void setToWayId(long toWayId) { this.toWayId = toWayId; } public byte getExceptionMask() { return exceptionMask; } public RestrType getType() { return type; } public CoordNode getFromNode() { return fromNode; } public void setFromNode(CoordNode fromNode) { this.fromNode = fromNode; } public CoordNode getToNode() { return toNode; } public void setToNode(CoordNode toNode) { this.toNode = toNode; } public List getViaWayIds() { return viaWayIds; } public void setViaWayIds(List viaWayIds) { this.viaWayIds = new ArrayList(viaWayIds); } public List getViaNodes() { return viaNodes; } public void setViaNodes(List viaNodes){ this.viaNodes = new ArrayList<>(viaNodes); } public String getSourceDesc(){ return sourceDesc; } public char getDirIndicator() { return dirIndicator; } public void setDirIndicator(char dirIndicator) { this.dirIndicator = dirIndicator; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RoadNetwork.java0000644000076400007640000005446112613617341023517 0ustar stevesteve/* * Copyright (C) 2008 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: 13-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.CoordNode; import uk.me.parabola.log.Logger; import uk.me.parabola.util.EnhancedProperties; /** * This holds the road network. That is all the roads and the nodes * that connect them together. * * @see Distance / bearing calculations * @author Steve Ratcliffe */ public class RoadNetwork { private static final Logger log = Logger.getLogger(RoadNetwork.class); private final static int MAX_RESTRICTIONS_ARCS = 7; private final Map nodes = new LinkedHashMap<>(); // boundary nodes // a node should be in here if the nodes boundary flag is set private final List boundary = new ArrayList<>(); private final List roadDefs = new ArrayList<>(); private List centers = new ArrayList<>(); private AngleChecker angleChecker = new AngleChecker(); private boolean checkRoundabouts; private boolean checkRoundaboutFlares; private int maxFlareLengthRatio ; private boolean reportSimilarArcs; public void config(EnhancedProperties props) { checkRoundabouts = props.getProperty("check-roundabouts", false); checkRoundaboutFlares = props.getProperty("check-roundabout-flares", false); maxFlareLengthRatio = props.getProperty("max-flare-length-ratio", 0); reportSimilarArcs = props.getProperty("report-similar-arcs", false); angleChecker.config(props); } public void addRoad(RoadDef roadDef, List coordList) { roadDefs.add(roadDef); CoordNode lastCoord = null; int lastIndex = 0; double roadLength = 0; double arcLength = 0; int pointsHash = 0; int npoints = coordList.size(); int numCoordNodes = 0; boolean hasInternalNodes = false; int numNumberNodes = 0; BitSet nodeFlags = new BitSet(); for (int index = 0; index < npoints; index++) { Coord co = coordList.get(index); int id = co.getId(); if (id != 0){ nodeFlags.set(numNumberNodes); ++numCoordNodes; if(index > 0 && index < npoints - 1) hasInternalNodes = true; } if (co.isNumberNode()) ++numNumberNodes; if (index == 0){ if (id == 0) roadDef.setStartsWithNode(false); } else { double d = co.distance(coordList.get(index-1)); arcLength += d; roadLength += d; } if (roadDef.skipAddToNOD()) continue; pointsHash += co.hashCode(); if (id == 0) // not a routing node continue; // The next coord determines the heading // If this is the not the first node, then create an arc from // the previous node to this one (and back again). if (lastCoord != null) { int lastId = lastCoord.getId(); if(log.isDebugEnabled()) { log.debug("lastId = " + lastId + " curId = " + id); log.debug("from " + lastCoord.toDegreeString() + " to " + co.toDegreeString()); log.debug("arclength=" + arcLength + " roadlength=" + roadLength); } RouteNode node1 = getOrAddNode(lastId, lastCoord); RouteNode node2 = getOrAddNode(id, co); if(node1 == node2) log.error("Road " + roadDef + " contains consecutive identical nodes at " + co.toOSMURL() + " - routing will be broken"); else if(arcLength == 0) log.warn("Road " + roadDef + " contains zero length arc at " + co.toOSMURL()); Coord forwardBearingPoint = coordList.get(lastIndex + 1); if(lastCoord.equals(forwardBearingPoint) || forwardBearingPoint.isAddedNumberNode()) { // bearing point is too close to last node to be // useful - try some more points for(int bi = lastIndex + 2; bi <= index; ++bi) { Coord coTest = coordList.get(bi); if (coTest.isAddedNumberNode() || lastCoord.equals(coTest)) continue; forwardBearingPoint = coTest; break; } } Coord reverseBearingPoint = coordList.get(index - 1); if(co.equals(reverseBearingPoint) || reverseBearingPoint.isAddedNumberNode()) { // bearing point is too close to this node to be // useful - try some more points for(int bi = index - 2; bi >= lastIndex; --bi) { Coord coTest = coordList.get(bi); if (coTest.isAddedNumberNode() || co.equals(coTest)) continue; reverseBearingPoint = coTest; break; } } double forwardInitialBearing = lastCoord.bearingTo(forwardBearingPoint); double forwardDirectBearing = (co == forwardBearingPoint) ? forwardInitialBearing: lastCoord.bearingTo(co); double reverseInitialBearing = co.bearingTo(reverseBearingPoint); double directLength = (lastIndex + 1 == index) ? arcLength : lastCoord.distance(co); double reverseDirectBearing = 0; if (directLength > 0){ // bearing on rhumb line is a constant, so we can simply revert reverseDirectBearing = (forwardDirectBearing <= 0) ? 180 + forwardDirectBearing: -(180 - forwardDirectBearing) % 180.0; } // Create forward arc from node1 to node2 RouteArc arc = new RouteArc(roadDef, node1, node2, forwardInitialBearing, forwardDirectBearing, arcLength, arcLength, directLength, pointsHash); arc.setForward(); node1.addArc(arc); // Create the reverse arc RouteArc reverseArc = new RouteArc(roadDef, node2, node1, reverseInitialBearing, reverseDirectBearing, arcLength, arcLength, directLength, pointsHash); node2.addArc(reverseArc); // link the two arcs arc.setReverseArc(reverseArc); reverseArc.setReverseArc(arc); } else { // This is the first node in the road roadDef.setNode(getOrAddNode(id, co)); } lastCoord = (CoordNode) co; lastIndex = index; arcLength = 0; pointsHash = co.hashCode(); } if (roadDef.hasHouseNumbers()){ // we ignore number nodes when we have no house numbers if (numCoordNodes < numNumberNodes) hasInternalNodes = true; roadDef.setNumNodes(numNumberNodes); roadDef.setNod2BitSet(nodeFlags); } else { roadDef.setNumNodes(numCoordNodes); } if (hasInternalNodes) roadDef.setInternalNodes(true); roadDef.setLength(roadLength); } private RouteNode getOrAddNode(int id, Coord coord) { RouteNode node = nodes.get(id); if (node == null) { node = new RouteNode(coord); nodes.put(id, node); if (node.isBoundary()) boundary.add(node); } return node; } public List getRoadDefs() { return roadDefs; } /** * Split the network into RouteCenters. * * The resulting centers must satisfy several constraints, * documented in NOD1Part. */ private void splitCenters() { if (nodes.isEmpty()) return; assert centers.isEmpty() : "already subdivided into centers"; // sort nodes by NodeGroup List nodeList = new ArrayList<>(nodes.values()); nodes.clear(); // return to GC for (int group = 0; group <= 4; group++){ NOD1Part nod1 = new NOD1Part(); int n = 0; for (RouteNode node : nodeList) { if (node.getGroup() != group) continue; if(!node.isBoundary()) { if(checkRoundabouts) node.checkRoundabouts(); if(checkRoundaboutFlares) node.checkRoundaboutFlares(maxFlareLengthRatio); if(reportSimilarArcs) node.reportSimilarArcs(); } nod1.addNode(node); n++; } if (n > 0) centers.addAll(nod1.subdivide()); } } public List getCenters() { if (centers.isEmpty()){ angleChecker.check(nodes); addArcsToMajorRoads(); splitCenters(); } return centers; } /** * add indirect arcs for each road class (in descending order) */ private void addArcsToMajorRoads() { long t1 = System.currentTimeMillis(); for (RoadDef rd: roadDefs){ if (rd.skipAddToNOD()) continue; if (rd.getRoadClass() >= 1) rd.getNode().addArcsToMajorRoads(rd); } log.info(" added major road arcs in " + (System.currentTimeMillis() - t1) + " ms"); } /** * Get the list of nodes on the boundary of the network. * * Currently empty. */ public List getBoundary() { return boundary; } /** * One restriction forbids to travel a specific combination of arcs. * We know two kinds: 3 nodes with two arcs and one via node or 4 nodes with 3 arcs * and two via nodes. Maybe more nodes are possible, but we don't know for sure how * to write them (2014-04-02). * Depending on the data in grr we create one or more such restrictions. * A restriction with 4 (or more) nodes is added to each via node. * * The OSM restriction gives a from way id and a to way id and one or more * via nodes. It is possible that the to-way is a loop, so we have to identify * the correct arc. * @param grr the object that holds the details about the route restriction */ public int addRestriction(GeneralRouteRestriction grr) { if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_NO_TROUGH) return addNoThroughRoute(grr); String sourceDesc = grr.getSourceDesc(); List viaNodes = new ArrayList<>(); for (CoordNode via : grr.getViaNodes()){ RouteNode vn = nodes.get(via.getId()); if (vn == null){ log.error(sourceDesc, "can't locate 'via' RouteNode with id", via.getId()); return 0; } viaNodes.add(vn); } int firstViaId = grr.getViaNodes().get(0).getId(); int lastViaId = grr.getViaNodes().get(grr.getViaNodes().size()-1).getId(); RouteNode firstViaNode = nodes.get(firstViaId); RouteNode lastViaNode = nodes.get(lastViaId); List> viaArcsList = new ArrayList<>(); if (grr.getViaNodes().size() != grr.getViaWayIds().size() + 1){ log.error(sourceDesc, "internal error: number of via nodes and via ways doesn't fit"); return 0; } for (int i = 1; i < grr.getViaNodes().size(); i++){ RouteNode vn = viaNodes.get(i-1); Long viaWayId = grr.getViaWayIds().get(i-1); List viaArcs = vn.getDirectArcsTo(viaNodes.get(i), viaWayId); if (viaArcs.isEmpty()){ log.error(sourceDesc, "can't locate arc from 'via' node at",vn.getCoord().toOSMURL(),"to next 'via' node on way",viaWayId); return 0; } viaArcsList.add(viaArcs); } // determine the from node and arc(s) int fromId = 0; RouteNode fn = null; if (grr.getFromNode() != null){ fromId = grr.getFromNode().getId(); // polish input data provides id fn = nodes.get(fromId); if (fn == null ){ log.error(sourceDesc, "can't locate 'from' RouteNode with id", fromId); return 0; } } else { List possibleFromArcs = firstViaNode.getDirectArcsOnWay(grr.getFromWayId()); for (RouteArc arc : possibleFromArcs){ if (fn == null) fn = arc.getDest(); else if (fn != arc.getDest()){ log.warn(sourceDesc, "found different 'from' arcs for way",grr.getFromWayId(),"restriction is ignored"); return 0; } } if (fn == null){ log.warn(sourceDesc, "can't locate 'from' RouteNode for 'from' way", grr.getFromWayId()); return 0; } fromId = fn.getCoord().getId(); } List fromArcs = fn.getDirectArcsTo(firstViaNode, grr.getFromWayId()); if (fromArcs.isEmpty()){ log.error(sourceDesc, "can't locate arc from 'from' node ",fromId,"to 'via' node",firstViaId,"on way",grr.getFromWayId()); return 0; } // a bit more complex: determine the to-node and arc(s) RouteNode tn = null; int toId = 0; List toArcs = new ArrayList<>(); if (grr.getToNode() != null){ // polish input data provides id toId = grr.getToNode().getId(); tn = nodes.get(toId); if (tn == null ){ log.error(sourceDesc, "can't locate 'to' RouteNode with id", toId); return 0; } } else { // we can have multiple arcs between last via node and to node. The // arcs can be on the same OSM way or on different OSM ways. // We can have multiple arcs with different RoadDef objects that refer to the same // OSM way id. The direction indicator tells us what arc is probably meant. List possibleToArcs = lastViaNode.getDirectArcsOnWay(grr.getToWayId()); RouteArc fromArc = fromArcs.get(0); boolean ignoreAngle = false; if (fromArc.getLengthInMeter() <= 0.0001) ignoreAngle = true; if (grr.getDirIndicator() == '?') ignoreAngle = true; log.info(sourceDesc, "found", possibleToArcs.size(), "candidates for to-arc"); // group the available arcs by angle Map> angleMap = new TreeMap<>(); for (RouteArc arc : possibleToArcs){ if (arc.getLengthInMeter() <= 0.0001) ignoreAngle = true; Integer angle = Math.round(getAngle(fromArc, arc)); List list = angleMap.get(angle); if (list == null){ list = new ArrayList<>(); angleMap.put(angle, list); } list.add(arc); } // find the group that fits best Iterator>> iter = angleMap.entrySet().iterator(); Integer bestAngle = null; while (iter.hasNext()){ Entry> entry = iter.next(); if (ignoreAngle || matchDirectionInfo(entry.getKey(), grr.getDirIndicator()) ){ if (bestAngle == null) bestAngle = entry.getKey(); else { bestAngle = getBetterAngle(bestAngle, entry.getKey(), grr.getDirIndicator()); } } } if (bestAngle == null){ log.warn(sourceDesc,"the angle of the from and to way don't match the restriction"); return 0; } toArcs = angleMap.get(bestAngle); } if (toArcs.isEmpty()){ log.error(sourceDesc, "can't locate arc from 'via' node ",lastViaId,"to 'to' node",toId,"on way",grr.getToWayId()); return 0; } List badArcs = new ArrayList<>(); if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_NOT){ for (RouteArc toArc: toArcs){ badArcs.add(toArc); } } else if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_ONLY){ // this is the inverse logic, grr gives the allowed path, we have to find the others for (RouteArc badArc : lastViaNode.arcsIteration()){ if (!badArc.isDirect() || toArcs.contains(badArc)) continue; badArcs.add(badArc); } if (badArcs.isEmpty()){ log.warn(sourceDesc, "restriction ignored because it has no effect"); return 0; } } // create all possible paths for which the restriction applies List> arcLists = new ArrayList<>(); arcLists.add(fromArcs); arcLists.addAll(viaArcsList); arcLists.add(badArcs); if (arcLists.size() > MAX_RESTRICTIONS_ARCS){ log.warn(sourceDesc, "has more than", MAX_RESTRICTIONS_ARCS, "arcs, this is not supported"); return 0; } // remove arcs which cannot be travelled by the vehicles listed in the restriction for (int i = 0; i < arcLists.size(); i++){ List arcs = arcLists.get(i); int countNoEffect = 0; int countOneway= 0; for (int j = arcs.size()-1; j >= 0; --j){ RouteArc arc = arcs.get(j); if (isUsable(arc.getRoadDef().getAccess(), grr.getExceptionMask()) == false){ countNoEffect++; arcs.remove(j); } else if (arc.getRoadDef().isOneway()){ if (!arc.isForward()){ countOneway++; arcs.remove(j); } } } String arcType = null; if (arcs.isEmpty()){ if (i == 0) arcType = "from way is"; else if (i == arcLists.size()-1){ if (grr.getType() == GeneralRouteRestriction.RestrType.TYPE_ONLY) arcType = "all possible other ways are"; else arcType = "to way is"; } else arcType = "via way is"; String reason; if (countNoEffect > 0 & countOneway > 0) reason = "wrong direction in oneway or not accessible for restricted vehicles"; else if (countNoEffect > 0) reason = "not accessible for restricted vehicles"; else reason = "wrong direction in oneway"; log.warn(sourceDesc, "restriction ignored because",arcType,reason); return 0; } } if (viaNodes.contains(fn)){ log.warn(sourceDesc, "restriction not written because from node appears also as via node"); return 0; } // determine all possible combinations of arcs. In most cases, // this will be 0 or one, but if the style creates multiple roads for one // OSM way, this can be a larger number int numCombis = 1; int [] indexes = new int[arcLists.size()]; for (int i = 0; i < indexes.length; i++){ List arcs = arcLists.get(i); numCombis *= arcs.size(); } List path = new ArrayList<>(); int added = 0; for (int i = 0; i < numCombis; i++){ for (RouteNode vn : viaNodes){ path.clear(); boolean viaNodeFound = false; byte pathNoAccessMask = 0; for (int j = 0; j < indexes.length; j++){ RouteArc arc = arcLists.get(j).get(indexes[j]); if (arc.getDest() == vn || viaNodeFound == false){ arc = arc.getReverseArc(); } if (arc.getSource() == vn) viaNodeFound = true; if (arc.getDest() == vn){ if (added > 0) log.error(sourceDesc, "restriction incompletely written because dest in arc is via node"); else log.warn(sourceDesc, "restriction not written because dest in arc is via node"); return added; } pathNoAccessMask |= ~arc.getRoadDef().getAccess(); path.add(arc); } byte pathAccessMask = (byte)~pathNoAccessMask; if (isUsable(pathAccessMask, grr.getExceptionMask())){ vn.addRestriction(new RouteRestriction(vn, path, grr.getExceptionMask())); ++added; } } // get next combination of arcs ++indexes[indexes.length-1]; for (int j = indexes.length-1; j > 0; --j){ if (indexes[j] >= arcLists.get(j).size()){ indexes[j] = 0; indexes[j-1]++; } } } // double check if (indexes[0] != arcLists.get(0).size()) log.error(sourceDesc, " failed to generate all possible paths"); log.info(sourceDesc, "added",added,"route restriction(s) to img file"); return added; } /** * Compare the disallowed vehicles for the path with the exceptions from the restriction * @param roadNoAccess * @param exceptionMask * @return */ private static boolean isUsable(byte roadAccess, byte exceptionMask) { if ((roadAccess & (byte) ~exceptionMask) == 0) return false; // no allowed vehicle is concerned by this restriction return true; } private int addNoThroughRoute(GeneralRouteRestriction grr) { assert grr.getViaNodes() != null; assert grr.getViaNodes().size() == 1; int viaId = grr.getViaNodes().get(0).getId(); RouteNode vn = nodes.get(viaId); if (vn == null){ log.error(grr.getSourceDesc(), "can't locate 'via' RouteNode with id", viaId); return 0; } int added = 0; for (RouteArc out: vn.arcsIteration()){ if (!out.isDirect()) continue; for (RouteArc in: vn.arcsIteration()){ if (!in.isDirect() || in == out || in.getDest() == out.getDest()) continue; byte pathAccessMask = (byte) (out.getRoadDef().getAccess() & in.getRoadDef().getAccess()); if (isUsable(pathAccessMask, grr.getExceptionMask())){ vn.addRestriction(new RouteRestriction(vn, Arrays.asList(in,out), grr.getExceptionMask())); added++; } else { if (log.isDebugEnabled()) log.debug(grr.getSourceDesc(),"ignored no-through-route",in,"to",out); } } } return added; } public void addThroughRoute(int junctionNodeId, long roadIdA, long roadIdB) { RouteNode node = nodes.get(junctionNodeId); assert node != null : "Can't find node with id " + junctionNodeId; node.addThroughRoute(roadIdA, roadIdB); } /** * Calculate the "angle" between to arcs. The arcs may not be connected. * We do this by "virtually" moving the toArc so that its source * node lies on the destination node of the from arc. * This makes only sense if move is not over a large distance, we assume that this * is the case as via ways should be short. * @param fromArc arc with from node as source and first via node as destination * @param toArc arc with last via node as source * @return angle at in degree [-180;180] */ private static float getAngle(RouteArc fromArc, RouteArc toArc){ // note that the values do not depend on the isForward() attribute float headingFrom = fromArc.getFinalHeading(); float headingTo = toArc.getInitialHeading(); float angle = headingTo - headingFrom; while(angle > 180) angle -= 360; while(angle < -180) angle += 360; return angle; } /** * Find the angle that comes closer to the direction indicated. * * @param angle1 1st angle -180:180 degrees * @param angle2 2nd angle -180:180 degrees * @param dirIndicator l:left, r:right, u:u_turn, s: straight_on * @return */ private static Integer getBetterAngle (Integer angle1, Integer angle2, char dirIndicator){ switch (dirIndicator){ case 'l': if (Math.abs(-90-angle2) < Math.abs(-90-angle1)) return angle2; // closer to -90 break; case 'r': if (Math.abs(90-angle2) < Math.abs(90-angle1)) return angle2; // closer to 90 break; case 'u': double d1 = (angle1 < 0 ) ? -180-angle1 : 180-angle1; double d2 = (angle2 < 0 ) ? -180-angle2 : 180-angle2; if (Math.abs(d2) < Math.abs(d1)) return angle2; // closer to -180 break; case 's': if (Math.abs(angle2) < Math.abs(angle1)) return angle2; // closer to 0 break; } return angle1; } /** * Check if angle is in the range indicated by the direction * @param angle the angle -180:180 degrees * @param dirIndicator l:left, r:right, u:u_turn, s: straight_on * @return */ private static boolean matchDirectionInfo (float angle, char dirIndicator){ switch (dirIndicator){ case 'l': if (angle < -3 && angle > - 177) return true; break; case 'r': if (angle > 3 && angle < 177) return true; break; case 'u': if (angle < -87 || angle > 93) return true; break; case 's': if (angle > -87 && angle < 87) return true; break; case '?': return true; } return false; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RouteRestriction.java0000644000076400007640000001464712325445017024605 0ustar stevesteve/* * Copyright (C) 2008 * * 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. * * Create date: 07-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.app.ImgFileWriter; import static uk.me.parabola.imgfmt.app.net.AccessTagsAndBits.*; /** * A restriction in the routing graph. * * A routing restriction has two or more arcs. * The first arc is the "from" arc, the last is the "to" arc, * and other arc is a "via" arc. * * A from-to-via restriction says you can't go along arc "to" * if you came to node to.getSource() == from.getSource() * via the inverse arc of "from". We're using the inverse of * "from" since that has the information we need for writing * the Table C entry. * * @author Robert Vollmert */ public class RouteRestriction { //private static final Logger log = Logger.getLogger(RouteRestriction.class); // first three bytes of the header -- might specify the type of restriction // and when it is active private static final byte RESTRICTION_TYPE = 0x05; // 0x07 spotted, meaning? // To specify that a node is given by a relative offset instead // of an entry to Table B. private static final int F_INTERNAL = 0x8000; // the arcs private final List arcs; private final RouteNode viaNode; // offset in Table C private byte offsetSize; private int offsetC; // last restriction in a node private boolean last; // mask that specifies which vehicle types the restriction doesn't apply to private final byte exceptMask; private final byte flags; // meaning of bits 0x01 and 0x10 are not clear private final static byte F_EXCEPT_FOOT = 0x02; private final static byte F_EXCEPT_EMERGENCY = 0x04; private final static byte F_MORE_EXCEPTIONS = 0x08; private final static byte EXCEPT_CAR = 0x01; private final static byte EXCEPT_BUS = 0x02; private final static byte EXCEPT_TAXI = 0x04; private final static byte EXCEPT_DELIVERY = 0x10; private final static byte EXCEPT_BICYCLE = 0x20; private final static byte EXCEPT_TRUCK = 0x40; /** * * @param viaNode the node to which this restriction is related * @param traffArcs the arcs that describe the "forbidden" path * @param mkgmapExceptMask the exception mask in the mkgmap format */ public RouteRestriction(RouteNode viaNode, List traffArcs, byte mkgmapExceptMask) { this.viaNode = viaNode; this.arcs = new ArrayList<>(traffArcs); for (int i = 0; i < arcs.size(); i++){ RouteArc arc = arcs.get(i); assert arc.getDest() != viaNode; } byte flags = 0; if ((mkgmapExceptMask & FOOT) != 0) flags |= F_EXCEPT_FOOT; if ((mkgmapExceptMask & EMERGENCY) != 0) flags |= F_EXCEPT_EMERGENCY; exceptMask = translateExceptMask(mkgmapExceptMask); if(exceptMask != 0) flags |= F_MORE_EXCEPTIONS; int numArcs = arcs.size(); assert numArcs < 8; flags |= ((numArcs) << 5); this.flags = flags; } /** * Translate the mkgmap internal representation of vehicles to the one used in the img format * @param mkgmapExceptMask * @return */ private byte translateExceptMask(byte mkgmapExceptMask) { byte mask = 0; if ((mkgmapExceptMask & CAR) != 0) mask |= EXCEPT_CAR; if ((mkgmapExceptMask & BUS) != 0) mask |= EXCEPT_BUS; if ((mkgmapExceptMask & TAXI) != 0) mask |= EXCEPT_TAXI; if ((mkgmapExceptMask & DELIVERY) != 0) mask |= EXCEPT_DELIVERY; if ((mkgmapExceptMask & BIKE) != 0) mask |= EXCEPT_BICYCLE; if ((mkgmapExceptMask & TRUCK) != 0) mask |= EXCEPT_TRUCK; return mask; } private int calcOffset(RouteNode node, int tableOffset) { int offset = tableOffset - node.getOffsetNod1(); assert offset >= 0 : "node behind start of tables"; assert offset < 0x8000 : "node offset too large"; return offset | F_INTERNAL; } public List getArcs(){ return arcs; } /** * Writes a Table C entry with 3 or more nodes. * * @param writer The writer. * @param tableOffset The offset in NOD 1 of the tables area. * */ public void write(ImgFileWriter writer, int tableOffset) { writer.put(RESTRICTION_TYPE); writer.put(flags); writer.put((byte)0); // meaning ? if(exceptMask != 0) writer.put(exceptMask); int numArcs = arcs.size(); int[] offsets = new int[numArcs+1]; int pos = 0; boolean viaWritten = false; for (int i = 0; i < numArcs; i++){ RouteArc arc = arcs.get(i); // the arcs must have a specific order and direction // first arc: dest is from node , last arc: dest is to node // if there only two arcs, both will have the via node as source node. // For more n via nodes, the order is like this: // from <- via(1) <- via(2) <- ... <- this via node -> via( n-1) -> via(n) -> to if (arc.isInternal()) offsets[pos++] = calcOffset(arc.getDest(), tableOffset); else offsets[pos++] = arc.getIndexB(); if (arc.getSource() == viaNode){ // there will be two nodes with source node = viaNode, but we write the source only once if (!viaWritten){ offsets[pos++] = calcOffset(viaNode, tableOffset); viaWritten = true; } } } for (int offset : offsets) writer.putChar((char) offset); for (RouteArc arc: arcs) writer.put(arc.getIndexA()); } /** * Write this restriction's offset within Table C into a node record. */ public void writeOffset(ImgFileWriter writer) { assert 0 < offsetSize && offsetSize <= 2 : "illegal offset size"; int offset = offsetC; if (offsetSize == 1) { assert offset < 0x80; if (last) offset |= 0x80; writer.put((byte) offset); } else { assert offset < 0x8000; if (last) offset |= 0x8000; writer.putChar((char) offset); } } /** * Size in bytes of the Table C entry. */ public int getSize() { int size = 3; // header length if(exceptMask != 0) ++size; size += arcs.size() + (arcs.size()+1) * 2; return size; } public void setOffsetC(int offsetC) { this.offsetC = offsetC; } public int getOffsetC() { return offsetC; } public void setOffsetSize(byte size) { offsetSize = size; } public void setLast() { last = true; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RouteCenter.java0000644000076400007640000001241612321425420023500 0ustar stevesteve/* * Copyright (C) 2008 * * 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. * * Create date: 07-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.List; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.log.Logger; /** * Routing nodes are divided into areas which I am calling RouteCenter's. * The center has a location and it contains nodes that are nearby. * There is routing between nodes in the center and there are links * to nodes in other centers. */ public class RouteCenter { private static final Logger log = Logger.getLogger(RouteCenter.class); private final Area area; private final Coord centralPoint; private final List nodes; private final TableA tabA; private final TableB tabB; private final TableC tabC; public RouteCenter(Area area, List nodes, TableA tabA, TableB tabB) { this.area = area; this.centralPoint = area.getCenter(); this.nodes = nodes; this.tabA = tabA; this.tabB = tabB; this.tabC = new TableC(tabA); log.info("new RouteCenter at " + centralPoint.toDegreeString() + ", nodes: " + nodes.size() + " tabA: " + tabA.size() + " tabB: " + tabB.size()); } /** * update arcs with table indices; populate tabC */ private void updateOffsets(){ for (RouteNode node : nodes) { node.setOffsets(centralPoint); for (RouteArc arc : node.arcsIteration()) { arc.setIndexA(tabA.getIndex(arc)); arc.setInternal(nodes.contains(arc.getDest())); if (!arc.isInternal()) arc.setIndexB(tabB.getIndex(arc.getDest())); } for (RouteRestriction restr : node.getRestrictions()){ if (restr.getArcs().size() >= 3){ // only restrictions with more than 2 arcs can contain further arcs for (RouteArc arc : restr.getArcs()){ if (arc.getSource() == node) continue; arc.setIndexA(tabA.getIndex(arc)); arc.setInternal(nodes.contains(arc.getDest())); if (!arc.isInternal()) arc.setIndexB(tabB.getIndex(arc.getDest())); } } restr.setOffsetC(tabC.addRestriction(restr)); } } // update size of tabC offsets, now that tabC has been populated tabC.propagateSizeBytes(); } /** * Write a route center. * * writer.position() is relative to the start of NOD 1. * Space for Table A is reserved but not written. See writeTableA. */ public void write(ImgFileWriter writer, int[] classBoundaries) { assert !nodes.isEmpty(): "RouteCenter without nodes"; updateOffsets(); int centerPos = writer.position(); for (RouteNode node : nodes){ node.write(writer); int group = node.getGroup(); if (group == 0) continue; if (centerPos < classBoundaries[group-1]){ // update positions (loop is used because style might not use all classes for (int i = group-1; i >= 0; i--){ if (centerPos < classBoundaries[i] ) classBoundaries[i] = centerPos; } } } int alignment = 1 << NODHeader.DEF_ALIGN; int alignMask = alignment - 1; // Calculate the position of the tables. int tablesOffset = (writer.position() + alignment) & ~alignMask; log.debug("write table a at offset", Integer.toHexString(tablesOffset)); // Go back and fill in all the table offsets for (RouteNode node : nodes) { int pos = node.getOffsetNod1(); log.debug("node pos", pos); byte bo = (byte) calcLowByte(pos, tablesOffset); writer.position(pos); log.debug("rewrite taba offset", writer.position(), bo); writer.put(bo); // fill in arc pointers node.writeSecond(writer); } writer.position(tablesOffset); // Write the tables header writer.put(tabC.getFormat()); writer.put3(centralPoint.getLongitude()); writer.put3(centralPoint.getLatitude()); writer.put(tabA.getNumberOfItems()); writer.put(tabB.getNumberOfItems()); tabA.write(writer); tabB.write(writer); tabC.write(writer, tablesOffset); log.info("end of center:", writer.position()); } public void writePost(ImgFileWriter writer) { // NET addresses are now known tabA.writePost(writer); // all RouteNodes now have their NOD1 offsets tabB.writePost(writer); } /** * Inverse of calcTableOffset. */ private static int calcLowByte(int nodeOffset, int tablesOffset) { assert nodeOffset < tablesOffset; int align = NODHeader.DEF_ALIGN; int mask = (1 << align) - 1; if ((tablesOffset & mask) != 0) { log.warn("tablesOffset not a multiple of (1<> align) + 1) << align; } int low = (tablesOffset >> align) - (nodeOffset >> align) - 1; assert 0 <= low && low < 0x100; return low; } public Area getArea() { return area; } public String reportSizes() { int nodesSize = 0; for(RouteNode n : nodes) nodesSize += n.boundSize(); return "n=(" + nodes.size() + "," + nodesSize + "), a=" + tabA.size() + ", b=" + tabB.size(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/RoadIndex.java0000644000076400007640000000237011135200663023116 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 5, 2008 */ package uk.me.parabola.imgfmt.app.net; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.trergn.Polyline; import uk.me.parabola.imgfmt.app.trergn.Subdivision; /** * @author Steve Ratcliffe */ public class RoadIndex { private final Polyline linkedRoad; // int Subdivision.getNumber() public RoadIndex(Polyline road) { linkedRoad = road; } private Subdivision getSubdiv() { return linkedRoad.getSubdiv(); } Polyline getLine() { return linkedRoad; } void write(ImgFileWriter writer) { int roadnum = linkedRoad.getNumber(); assert roadnum < 256; writer.put((byte) roadnum); char subdivnum = (char) getSubdiv().getNumber(); writer.putChar(subdivnum); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/TableA.java0000644000076400007640000001417612310532056022400 0ustar stevesteve/* * Copyright (C) 2008 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: 18-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.HashMap; import java.util.LinkedHashMap; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.log.Logger; /** * Table A that contains road information for segments in one RouteCenter. * * Each arc starting from a node in the RouteCenter has an associated * entry in Table A, shared by the inverse arc for internal arcs. This * entry consists of some routing parameters and a link to the road in * NET. */ public class TableA { private static final Logger log = Logger.getLogger(TableA.class); private static final int ITEM_SIZE = 5; // This table's start position relative to the start of NOD 1 private int offset; // arcs for roundabouts private final HashMap roundaboutArcs = new LinkedHashMap(); // arcs for unpaved ways private final HashMap unpavedArcs = new LinkedHashMap(); // arcs for ferry ways private final HashMap ferryArcs = new LinkedHashMap(); // arcs for paved ways private final HashMap pavedArcs = new LinkedHashMap(); private static int count; private boolean frozen ; // true when no more arcs should be added public TableA() { log.debug("creating TableA", count); count++; } /** * Add an arc to the table if not present and set its index. * * The value may overflow while it isn't certain that * the table fulfils the size constraint. */ public void addArc(RouteArc arc) { assert !frozen : "trying to add arc to Table A after it has been frozen"; int i; RoadDef rd = arc.getRoadDef(); if(rd.isRoundabout()) { if (!roundaboutArcs.containsKey(rd)) { i = roundaboutArcs.size(); roundaboutArcs.put(rd, i); log.debug("added roundabout arc", count, rd, i); } } else if(rd.ferry()) { if (!ferryArcs.containsKey(rd)) { i = ferryArcs.size(); ferryArcs.put(rd, i); log.debug("added ferry arc", count, rd, i); } } else if(rd.paved()) { if (!pavedArcs.containsKey(rd)) { i = pavedArcs.size(); pavedArcs.put(rd, i); log.debug("added paved arc", count, rd, i); } } else { if (!unpavedArcs.containsKey(rd)) { i = unpavedArcs.size(); unpavedArcs.put(rd, i); log.debug("added unpaved arc", count, rd, i); } } } /** * Retrieve an arc's index. * Order in table A: roundabouts, unpaved, ferry, paved */ public byte getIndex(RouteArc arc) { frozen = true; // don't allow any more arcs to be added int i; RoadDef rd = arc.getRoadDef(); if(rd.isRoundabout()) { assert roundaboutArcs.containsKey(rd): "Trying to read Table A index for non-registered arc: " + count + " " + rd; i = roundaboutArcs.get(rd); } else if(rd.ferry()) { assert ferryArcs.containsKey(rd): "Trying to read Table A index for non-registered arc: " + count + " " + rd; i = roundaboutArcs.size() + unpavedArcs.size() + ferryArcs.get(rd); } else if(rd.paved()) { assert pavedArcs.containsKey(rd): "Trying to read Table A index for non-registered arc: " + count + " " + rd; i = roundaboutArcs.size() + unpavedArcs.size() + ferryArcs.size() + pavedArcs.get(rd); } else { assert unpavedArcs.containsKey(rd): "Trying to read Table A index for non-registered arc: " + count + " " + rd; i = roundaboutArcs.size() + unpavedArcs.get(rd); } assert i < 0x100 : "Table A index too large: " + rd; return (byte) i; } /** * Retrieve the size of the Table as an int. * * While Table A is limited to byte size (0x100 entries), * we temporarily build larger tables while subdividing * the network. */ public int size() { return roundaboutArcs.size() + unpavedArcs.size() + ferryArcs.size() + pavedArcs.size(); } public int numRoundaboutArcs() { return roundaboutArcs.size(); } public int numUnpavedArcs() { return unpavedArcs.size(); } public int numFerryArcs() { return ferryArcs.size(); } /** * Retrieve the size of the table as byte. * * This value is what should be written to the table * header. When this is read, the table is assumed to * be fit for writing, so at this point we check * it isn't too large. */ public byte getNumberOfItems() { assert size() < 0x100 : "Table A too large"; return (byte)size(); } /** * This is called first to reserve enough space. It will be rewritten * later. */ public void write(ImgFileWriter writer) { offset = writer.position(); int size = size() * ITEM_SIZE; log.debug("tab a offset", offset, "tab a size", size); for (int i = 0; i < size; i++) writer.put((byte) 0); } /** * Fill in the table once the NET offsets of the roads are known. */ public void writePost(ImgFileWriter writer) { writer.position(offset); // unpaved arcs first for (RoadDef rd: roundaboutArcs.keySet()) { writePost(writer, rd); } for (RoadDef rd: unpavedArcs.keySet()) { writePost(writer, rd); } // followed by the ferry arcs for (RoadDef rd : ferryArcs.keySet()) { writePost(writer, rd); } // followed by the paved arcs for (RoadDef rd : pavedArcs.keySet()) { writePost(writer, rd); } } public void writePost(ImgFileWriter writer, RoadDef rd) { // write the table A entries. Consists of a pointer to net // followed by 2 bytes of class and speed flags and road restrictions. int pos = rd.getOffsetNet1(); int access = rd.getTabAAccess(); // top bits of access go into net1 offset final int ACCESS_TOP_BITS = 0xc000; pos |= (access & ACCESS_TOP_BITS) << 8; access &= ~ACCESS_TOP_BITS; writer.put3(pos); writer.put((byte) rd.getTabAInfo()); writer.put((byte) access); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NETFile.java0000644000076400007640000002370112533237270022477 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 5, 2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.srt.IntegerSortKey; import uk.me.parabola.imgfmt.app.srt.MultiSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * The NET file. This consists of information about roads. It is not clear * what this file brings on its own (without NOD) but may allow some better * searching, street addresses etc. * * @author Steve Ratcliffe */ public class NETFile extends ImgFile { private final NETHeader netHeader = new NETHeader(); private List roads; private Sort sort; public NETFile(ImgChannel chan) { setHeader(netHeader); setWriter(new BufferedImgFileWriter(chan)); position(NETHeader.HEADER_LEN); } /** * Write out NET1. * @param numCities The number of cities in the map. Needed for the size of the written fields. * @param numZips The number of zips in the map. Needed for the size of the written fields. */ public void write(int numCities, int numZips) { // Write out the actual file body. ImgFileWriter writer = netHeader.makeRoadWriter(getWriter()); try { for (RoadDef rd : roads) rd.writeNet1(writer, numCities, numZips); } finally { Utils.closeFile(writer); } } /** * Final writing out of net sections. * * We patch the NET offsets into the RGN file and create the sorted roads section. * * @param rgn The region file, this has to be patched with the calculated net offsets. */ public void writePost(ImgFileWriter rgn) { for (RoadDef rd : roads) rd.writeRgnOffsets(rgn); ImgFileWriter writer = netHeader.makeSortedRoadWriter(getWriter()); try { List labeledRoadDefs = sortRoads(); for (LabeledRoadDef labeledRoadDef : labeledRoadDefs) labeledRoadDef.roadDef.putSortedRoadEntry(writer, labeledRoadDef.label); } finally { Utils.closeFile(writer); } getHeader().writeHeader(getWriter()); } /** * Sort the roads by name and remove duplicates. * * We want a list of roads such that every entry in the list is a different road. Since in osm * roads are frequently chopped into small pieces we have to remove the duplicates. * This doesn't have to be perfect, it needs to be useful when searching for roads. * * So we have a separate entry if the road is in a different city. This would probably be enough * except that associating streets with cities is not always very good in OSM. So I also create an * extra entry for each subdivision. Finally there a search for disconnected roads within the subdivision * with the same name. * * Performance note: The previous implementation was very, very slow when there were a large number * of roads with the same name. Although this was an unusual situation, when it happened it appears * that mkgmap has hung. This implementation takes a fraction of a second even for large numbers of * same named roads. * * @return A sorted list of road labels that identify all the different roads. */ private List sortRoads() { List> sortKeys = new ArrayList<>(roads.size()); Map cache = new HashMap<>(); for (RoadDef rd : roads) { Label[] labels = rd.getLabels(); for (int i = 0; i < labels.length && labels[i] != null; ++i) { Label label = labels[i]; if (label.getLength() == 0) continue; // Sort by name, city, region/country and subdivision number. LabeledRoadDef lrd = new LabeledRoadDef(label, rd); SortKey nameKey = sort.createSortKey(lrd, label, 0, cache); // If there is a city add it to the sort. City city = (rd.getCities().isEmpty() ? null : rd.getCities().get(0)); // what if we more than one? SortKey cityKey; if (city != null) { int region = city.getRegionNumber(); int country = city.getCountryNumber(); cityKey = sort.createSortKey(null, city.getLabel(), (region & 0xffff) << 16 | (country & 0xffff), cache); } else { cityKey = sort.createSortKey(null, Label.NULL_OUT_LABEL, 0, cache); } SortKey sortKey = new MultiSortKey<>(nameKey, cityKey, new IntegerSortKey(null, rd.getStartSubdivNumber(), 0)); sortKeys.add(sortKey); } } Collections.sort(sortKeys); List out = new ArrayList<>(sortKeys.size()); Label lastName = null; City lastCity = null; List dupes = new ArrayList<>(); // Since they are sorted we can easily remove the duplicates. // The duplicates are saved to the dupes list. for (SortKey key : sortKeys) { LabeledRoadDef lrd = key.getObject(); Label name = lrd.label; RoadDef road = lrd.roadDef; City city = (road.getCities().isEmpty() ? null : road.getCities().get(0)); // what if we more than one? if (road.hasHouseNumbers() || !name.equals(lastName) || city != lastCity) { // process any previously collected duplicate road names and reset. addDisconnected(dupes, out); dupes = new ArrayList<>(); lastName = name; lastCity = city; } dupes.add(lrd); } // Finish off the final set of duplicates. addDisconnected(dupes, out); return out; } /** * Take a set of roads with the same name/city etc and find sets of roads that do not * connect with each other. One of the members of each set is added to the road list. * * @param in A list of duplicate roads. * @param out The list of sorted roads. Any new road is added to this. */ private void addDisconnected(List in, List out) { // switch out to different routines depending on the input size. A normal number of // roads with the same name in the same city is a few tens. if (in.size() > 200) { addDisconnectedLarge(in, out); } else { addDisconnectedSmall(in, out); } } /** * Split the input set of roads into disconnected groups and output one member from each group. * * This is done in an accurate manner which is slow for large numbers (eg thousands) of items in the * input. * * @param in Input set of roads with the same name. * @param out List to add the discovered groups. */ private void addDisconnectedSmall(List in, List out) { // Each road starts out with a different group number int[] groups = new int[in.size()]; for (int i = 0; i < groups.length; i++) groups[i] = i; // Go through pairs of roads, any that are connected we mark with the same (lowest) group number. boolean done; do { done = true; for (int current = 0; current < groups.length; current++) { RoadDef first = in.get(current).roadDef; for (int i = current; i < groups.length; i++) { // If the groups are already the same, then no need to test if (groups[current] == groups[i]) continue; if (first.connectedTo(in.get(i).roadDef)) { groups[current] = groups[i] = Math.min(groups[current], groups[i]); done = false; } } } } while (!done); // Output the first road in each group int last = -1; for (int i = 0; i < groups.length; i++) { if (groups[i] > last) { LabeledRoadDef lrd = in.get(i); out.add(lrd); last = groups[i]; } } } /** * Split the input set of roads into disconnected groups and output one member from each group. * * This is an modified algorithm for large numbers in the input set (eg thousands). * First sort into groups by subdivision and then call {@link #addDisconnectedSmall} on each * one. Since roads in the same subdivision are near each other this finds most connected roads, but * since there is a maximum number of roads in a subdivision, the test can be done very quickly. * You will get a few extra duplicate entries in the index. * * In normal cases this routine gives almost the same results as {@link #addDisconnectedSmall}. * * @param in Input set of roads with the same name. * @param out List to add the discovered groups. */ private void addDisconnectedLarge(List in, List out) { Collections.sort(in, new Comparator() { public int compare(LabeledRoadDef o1, LabeledRoadDef o2) { Integer i1 = o1.roadDef.getStartSubdivNumber(); Integer i2 = o2.roadDef.getStartSubdivNumber(); return i1.compareTo(i2); } }); int lastDiv = 0; List dupes = new ArrayList<>(); for (LabeledRoadDef lrd : in) { int sd = lrd.roadDef.getStartSubdivNumber(); if (sd != lastDiv) { addDisconnectedSmall(dupes, out); dupes = new ArrayList<>(); lastDiv = sd; } dupes.add(lrd); } addDisconnectedSmall(dupes, out); } public void setNetwork(List roads) { this.roads = roads; } public void setSort(Sort sort) { this.sort = sort; } /** * A road can have several names. Keep an association between a road def * and one of its names. */ class LabeledRoadDef { private final Label label; private final RoadDef roadDef; LabeledRoadDef(Label label, RoadDef roadDef) { this.label = label; this.roadDef = roadDef; } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NODHeader.java0000644000076400007640000001264312441246225023003 0ustar stevesteve/* * Copyright (C) 2008 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: 06-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.Arrays; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; /** * Header information for the NOD file. * * This is a routing network for the map. * * @author Steve Ratcliffe */ public class NODHeader extends CommonHeader { public static final int HEADER_LEN = 127; static final char DEF_ALIGN = 6; private static final char BOUNDARY_ITEM_SIZE = 9; private final Section nodes = new Section(); private final Section roads = new Section(nodes); private final Section boundary = new Section(roads, BOUNDARY_ITEM_SIZE); private final Section highClassBoundary = new Section(boundary); private final int[] classBoundaries = new int[5]; private int flags; private int align; private int mult1; private int tableARecordLen; private boolean driveOnLeft; public NODHeader() { super(HEADER_LEN, "GARMIN NOD"); Arrays.fill(classBoundaries, Integer.MAX_VALUE); } /** * Read the rest of the header. Specific to the given file. It is guaranteed * that the file position will be set to the correct place before this is * called. * * @param reader The header is read from here. */ protected void readFileHeader(ImgFileReader reader) throws ReadFailedException { nodes.readSectionInfo(reader, false); flags = reader.getChar(); reader.getChar(); align = reader.get(); mult1 = reader.get(); tableARecordLen = reader.getChar(); roads.readSectionInfo(reader, false); reader.getInt(); boundary.readSectionInfo(reader, true); reader.getInt(); if (getHeaderLength() > 0x3f) { highClassBoundary.readSectionInfo(reader, false); classBoundaries[0] = reader.getInt(); classBoundaries[1] = classBoundaries[0] + reader.getInt(); classBoundaries[2] = classBoundaries[1] + reader.getInt(); classBoundaries[3] = classBoundaries[2] + reader.getInt(); classBoundaries[4] = classBoundaries[3] + reader.getInt(); } } /** * Write the rest of the header. It is guaranteed that the writer will be set * to the correct position before calling. * * @param writer The header is written here. */ // multiplier shift for road + arc length values, the smaller the shift the higher the precision and NOD size // as it has an influence on the number of bits needed to encode a length final static int DISTANCE_MULT_SHIFT = 1; // 0..7 1 seems to be a good compromise final static int DISTANCE_MULT = 1 << DISTANCE_MULT_SHIFT; protected void writeFileHeader(ImgFileWriter writer) { nodes.setPosition(HEADER_LEN); nodes.writeSectionInfo(writer); // 0x0001 always set, meaning ? // 0x0002 (enable turn restrictions) // 0x001c meaning ? // 0x00E0 distance multiplier, effects predicted travel time int flags = 0x0207; assert Integer.bitCount(DISTANCE_MULT) == 1; assert DISTANCE_MULT_SHIFT < 8; flags |= DISTANCE_MULT_SHIFT << 5; if(driveOnLeft) flags |= 0x0100; writer.putInt(flags); byte align = DEF_ALIGN; writer.put(align); writer.put((byte) 0); // pointer multiplier writer.putChar((char) 5); roads.writeSectionInfo(writer); writer.putInt(0); boundary.writeSectionInfo(writer); // new fields for header length > 0x3f writer.putInt(2); // no other value spotted, meaning ? highClassBoundary.writeSectionInfo(writer); writer.putInt(classBoundaries[0]); for (int i = 1; i < classBoundaries.length; i++){ writer.putInt(classBoundaries[i] - classBoundaries[i-1]); } } private static final double UNIT_TO_METER = 2.4; public static int metersToRaw(double m) { double d = m / (DISTANCE_MULT * UNIT_TO_METER); return (int) Math.round(d); } public int getNodeStart() { return nodes.getPosition(); } public void setNodeStart(int start) { nodes.setPosition(start); } public int getNodeSize() { return nodes.getSize(); } public void setNodeSize(int size) { nodes.setSize(size); } public Section getNodeSection() { return nodes; } public void setRoadSize(int size) { roads.setSize(size); } public Section getRoadSection() { return roads; } public void setBoundarySize(int size) { boundary.setSize(size); } public Section getBoundarySection() { return boundary; } public void setHighClassBoundarySize(int size) { highClassBoundary.setSize(size); } public Section getHighClassBoundary() { return highClassBoundary; } public int[] getClassBoundaries() { return classBoundaries; } public void setDriveOnLeft(boolean dol) { driveOnLeft = dol; } public int getFlags() { return flags; } public int getAlign() { return align; } public int getMult1() { return mult1; } public int getTableARecordLen() { return tableARecordLen; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/TableB.java0000644000076400007640000000524312321425420022372 0ustar stevesteve/* * Copyright (C) 2008 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: 18-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * Table B contains offsets in NOD1 of neighbouring nodes * outside the containing RouteCenter. */ public class TableB { private final ArrayList nodes = new ArrayList(); private final static int ITEM_SIZE = 3; private int offset; /** * Retrieve the size of the Table as an int. * * While Table B is limited in size (0x100 entries), * we temporarily build larger tables while subdividing * the network. */ public int size() { return nodes.size(); } /** * Retrieve the size of the table as byte. * * This value is what should be written to the table * header. When this is read, the table is assumed to * be fit for writing, so at this point we check * it isn't too large. */ public byte getNumberOfItems() { assert nodes.size() < 0x100 : "Table B too large."; return (byte) nodes.size(); } /** * Add a node (in another RouteCenter) to this Table and return its index. * * This index may overflow while it isn't certain that the * table fulfills the size constraint. */ public void addNode(RouteNode node) { int i = nodes.indexOf(node); if (i < 0) { //i = nodes.size(); nodes.add(node); } } /** * Retrieve a nodes index. Checked for correct bounds. */ public byte getIndex(RouteNode node) { int i = nodes.indexOf(node); assert i >= 0 : "Trying to read Table B index for non-registered node."; assert i < 0x100 : "Table B index too large."; return (byte) i; } /** * Reserve space, since node offsets in other * RoutingCenters need not be known yet. See writePost. */ public void write(ImgFileWriter writer) { offset = writer.position(); int size = nodes.size() * ITEM_SIZE; for (int i = 0; i < size; i++) writer.put((byte) 0); } /** * Fill in node offsets. */ public void writePost(ImgFileWriter writer) { writer.position(offset); for (RouteNode node : nodes) writer.put3(node.getOffsetNod1()); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/AccessTagsAndBits.java0000644000076400007640000001002012332377471024531 0ustar stevesteve/* * 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.imgfmt.app.net; import java.util.LinkedHashMap; import java.util.Map; import uk.me.parabola.mkgmap.reader.osm.Element; import uk.me.parabola.mkgmap.reader.osm.TagDict; /** * mkgmap internal representation of (vehicle) access. * @author GerdP * */ public final class AccessTagsAndBits { // constants for vehicle class public static final byte FOOT = 0x01; public static final byte BIKE = 0x02; public static final byte CAR = 0x04; public static final byte DELIVERY = 0x08; public static final byte TRUCK = 0x10; public static final byte BUS = 0x20; public static final byte TAXI = 0x40; public static final byte EMERGENCY = (byte) 0x80; // other routing attributes public static final byte R_THROUGHROUTE = 0x001; // note: 1 means throughroute is allowed public static final byte R_CARPOOL = 0x002; public static final byte R_ONEWAY = 0x004; public static final byte R_TOLL = 0x008; public static final byte R_UNPAVED = 0x010; public static final byte R_FERRY = 0x020; public static final byte R_ROUNDABOUT = 0x040; public final static Map ACCESS_TAGS = new LinkedHashMap(){{ put("mkgmap:foot", FOOT); put("mkgmap:bicycle", BIKE); put("mkgmap:car", CAR); put("mkgmap:delivery", DELIVERY); put("mkgmap:truck", TRUCK); put("mkgmap:bus", BUS); put("mkgmap:taxi", TAXI); put("mkgmap:emergency", EMERGENCY); }}; public final static Map ACCESS_TAGS_COMPILED = new LinkedHashMap(){{ for (Map.Entry entry : ACCESS_TAGS.entrySet()) put(TagDict.getInstance().xlate(entry.getKey()),entry.getValue()); }}; public final static Map ROUTE_TAGS = new LinkedHashMap(){{ put("mkgmap:throughroute", R_THROUGHROUTE); put("mkgmap:carpool", R_CARPOOL); put("oneway", R_ONEWAY); put("mkgmap:toll", R_TOLL); put("mkgmap:unpaved", R_UNPAVED); put("mkgmap:ferry", R_FERRY); put("junction", R_ROUNDABOUT); }}; public static byte evalAccessTags(Element el){ byte noAccess = 0; for (Map.Entry entry : ACCESS_TAGS_COMPILED.entrySet()){ if (el.tagIsLikeNo(entry.getKey())) noAccess |= entry.getValue(); } return (byte) ~noAccess; } private static final short carpoolTagKey = TagDict.getInstance().xlate("mkgmap:carpool"); private static final short tollTagKey = TagDict.getInstance().xlate("mkgmap:toll"); private static final short unpavedTagKey = TagDict.getInstance().xlate("mkgmap:unpaved"); private static final short ferryTagKey = TagDict.getInstance().xlate("mkgmap:ferry"); private static final short throughrouteTagKey = TagDict.getInstance().xlate("mkgmap:throughroute"); private static final short junctionTagKey = TagDict.getInstance().xlate("junction"); private static final short onewayTagKey = TagDict.getInstance().xlate("oneway"); public static byte evalRouteTags(Element el){ byte routeFlags = 0; // Style has to set "yes" if (el.tagIsLikeYes(carpoolTagKey)) routeFlags |= R_CARPOOL; if (el.tagIsLikeYes(tollTagKey)) routeFlags |= R_TOLL; if (el.tagIsLikeYes(unpavedTagKey)) routeFlags |= R_UNPAVED; if (el.tagIsLikeYes(ferryTagKey)) routeFlags |= R_FERRY; // Style has to set "no" if (el.tagIsLikeNo(throughrouteTagKey)) routeFlags &= ~R_THROUGHROUTE; else routeFlags |= R_THROUGHROUTE; // tags without the mkgmap: prefix if ("roundabout".equals(el.getTag(junctionTagKey))) routeFlags |= R_ROUNDABOUT; if (el.tagIsLikeYes(onewayTagKey)) routeFlags |= R_ONEWAY; return routeFlags; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NOD1Part.java0000644000076400007640000002263612332102461022575 0ustar stevesteve/* * Copyright (C) 2008 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: Robert Vollmert * Create date: 02-Dec-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.Coord; import uk.me.parabola.log.Logger; /** * This is a component of the RoadNetwork. * * Keeps track of outside neighbours and allows subdivision * to satisfy NOD1 constraints. * * The approach to subdivision is to tile the map into RouteCenters. * One could imagine that overlapping RouteCenters would be an option, * say by splitting largely independent networks (motorways, footways). * * Could be rolled into RouteCenter. */ public class NOD1Part { private static final Logger log = Logger.getLogger(NOD1Part.class); /* * Constraints: * * 1. Nodes section smaller than about 0x4000, which gives * a bound on the number of nodes. * 2. At most 0x100 entries in Table A. This gives a bound * on the number of (forward) arcs meeting this * RouteCenter. * 3. At most 0x40 entries in Table B. This gives a bound * on the number of neighboring nodes. * 4. Absolute values of coordinate offsets at most 0x8000, * which translates to about 0.7 degrees, so bounding * box should be at most 1.4 x 1.4 degrees assuming * the reference is in the middle. (With small offsets, * this would be 0.08 x 0.08 degrees.) * 5. Absolute values of relative NOD1 offsets at most * 0x2000, which limits the nodes section to 0x2000 * unless we take care to order the nodes nicely. * 6. Distance between nodes and start of tables must * fit in a char for writing Table C. So nodes * section smaller than 0x10000. */ // maximal width and height of the bounding box, since // NOD 1 coordinate offsets are at most 16 bit wide. private static final int MAX_SIZE_UNSAFE = 1 << 16; // private static final int MAX_SIZE = MAX_SIZE_UNSAFE / 2; private static final int MAX_SIZE = MAX_SIZE_UNSAFE - 0x800; // Table A has at most 0x100 entries private static final int MAX_TABA_UNSAFE = 0x100; // private static final int MAX_TABA = MAX_TABA_UNSAFE / 2; private static final int MAX_TABA = MAX_TABA_UNSAFE - 0x8; // Table B has at most 0x100 entries private static final int MAX_TABB_UNSAFE = 0x100; // private static final int MAX_TABB = MAX_TABB_UNSAFE / 2; private static final int MAX_TABB = MAX_TABB_UNSAFE - 0x2; // Nodes size is max 0x2000 to cope with signed 14 bit node offsets private static final int MAX_NODES_SIZE = 0x2000; private int nodesSize; public class BBox { int maxLat, minLat, maxLon, minLon; boolean empty; BBox() { empty = true; } BBox(Coord co) { empty = false; int lat = co.getLatitude(); int lon = co.getLongitude(); minLat = lat; maxLat = lat+1; minLon = lon; maxLon = lon+1; } BBox(int minLat, int maxLat, int minLon, int maxLon) { empty = false; this.minLat = minLat; this.maxLat = maxLat; this.minLon = minLon; this.maxLon = maxLon; } Area toArea() { return new Area(minLat, minLon, maxLat, maxLon); } boolean contains(BBox bbox) { return minLat <= bbox.minLat && bbox.maxLat <= maxLat && minLon <= bbox.minLon && bbox.maxLon <= maxLon; } boolean contains(Coord co) { return contains(new BBox(co)); } void extend(BBox bbox) { if (bbox.empty) return; if (empty) { empty = false; minLat = bbox.minLat; maxLat = bbox.maxLat; minLon = bbox.minLon; maxLon = bbox.maxLon; } else { minLat = Math.min(minLat, bbox.minLat); maxLat = Math.max(maxLat, bbox.maxLat); minLon = Math.min(minLon, bbox.minLon); maxLon = Math.max(maxLon, bbox.maxLon); } } void extend(Coord co) { extend(new BBox(co)); } BBox[] splitLat() { BBox[] ret = new BBox[2]; int midLat = (minLat + maxLat) / 2; ret[0] = new BBox(minLat, midLat, minLon, maxLon); ret[1] = new BBox(midLat, maxLat, minLon, maxLon); return ret; } BBox[] splitLon() { BBox[] ret = new BBox[2]; int midLon = (minLon + maxLon) / 2; ret[0] = new BBox(minLat, maxLat, minLon, midLon); ret[1] = new BBox(minLat, maxLat, midLon, maxLon); return ret; } int getWidth() { return maxLon - minLon; } int getHeight() { return maxLat - minLat; } int getMaxDimension() { return Math.max(getWidth(), getHeight()); } public String toString() { return "BBox[" + new Coord(minLat,minLon).toDegreeString() + ", " + new Coord(maxLat,maxLon).toDegreeString() + "]"; } } // The area we are supposed to cover. private final BBox bbox; // The area that actually has nodes. private final BBox bboxActual = new BBox(); private List nodes = new ArrayList(); private TableA tabA = new TableA(); private Map destNodes = new LinkedHashMap(); /** * Create an unbounded NOD1Part. * * All nodes will be accepted by addNode and * all arcs will be considered internal. */ public NOD1Part() { log.info("creating new unbounded NOD1Part"); this.bbox = null; } /** * Create a bounded NOD1Part. * * The bounding box is used to decide which arcs * are internal. */ private NOD1Part(BBox bbox) { log.info("creating new NOD1Part:", bbox); this.bbox = bbox; } /** * Add a node to this part. * * The node is used to populate the tables. If an * arc points outside the bbox, we know it's not * an internal arc. It might still turn into an * external arc at a deeper level of recursion. */ public void addNode(RouteNode node) { assert bbox == null || bbox.contains(node.getCoord()) : "trying to add out-of-bounds node: " + node; bboxActual.extend(node.getCoord()); nodes.add(node); for (RouteArc arc : node.arcsIteration()) { tabA.addArc(arc); RouteNode dest = arc.getDest(); if (arc.isInternal() == false){ destNodes.put(dest, dest); } else if (bbox != null && !bbox.contains(dest.getCoord()) || dest.getGroup() != node.getGroup()) { arc.setInternal(false); destNodes.put(dest, dest); } } for (RouteRestriction rr: node.getRestrictions()){ List arcs = rr.getArcs(); if (arcs.size() >= 3){ for (int i = 0; i < arcs.size(); i++){ RouteArc arc = arcs.get(i); if (arc.getSource() != node){ tabA.addArc(arc); RouteNode dest = arc.getDest(); if (arc.isInternal() == false) destNodes.put(dest, dest); else if (bbox != null && !bbox.contains(dest.getCoord()) || dest.getGroup() != node.getGroup()) { arc.setInternal(false); destNodes.put(dest, dest); } } } } } nodesSize += node.boundSize(); } /** * Subdivide this part recursively until it satisfies the constraints. */ public List subdivide() { return subdivideHelper(0); } /** * Subdivide this part recursively until it satisfies the constraints. */ protected List subdivideHelper(int depth) { List centers = new LinkedList(); if (satisfiesConstraints()) { centers.add(this.toRouteCenter()); return centers; } if(depth > 48) { log.error("Region contains too many nodes/arcs (discarding " + nodes.size() + " nodes to be able to continue)"); log.error(" Expect the routing to be broken near " + bbox); for (RouteNode node : nodes) node.discard(); return centers; } log.info("subdividing", bbox, bboxActual); BBox[] split ; if (bboxActual.getWidth() > bboxActual.getHeight()) split = bboxActual.splitLon(); else split = bboxActual.splitLat(); NOD1Part[] parts = new NOD1Part[2]; for (int i = 0; i < split.length; i++) parts[i] = new NOD1Part(split[i]); for (RouteNode node : nodes) { int i = 0; while (!split[i].contains(node.getCoord())) i++; parts[i].addNode(node); } this.tabA = null; this.destNodes = null; this.nodes = null; for (NOD1Part part : parts) if(!part.bboxActual.empty) centers.addAll(part.subdivideHelper(depth + 1)); return centers; } private boolean satisfiesConstraints() { log.debug("constraints:", bboxActual, tabA.size(), destNodes.size(), nodesSize); return bboxActual.getMaxDimension() < MAX_SIZE && tabA.size() < MAX_TABA && destNodes.size() < MAX_TABB && nodesSize < MAX_NODES_SIZE; } /** * Convert to a RouteCenter. * * satisfiesConstraints() should be true for this to * be a legal RouteCenter. */ private RouteCenter toRouteCenter() { Collections.sort(nodes, new Comparator() { public int compare(RouteNode n1, RouteNode n2) { return n1.getCoord().compareTo(n2.getCoord()); } }); TableB tabB = new TableB(); for (RouteNode rn : destNodes.keySet()) tabB.addNode(rn); return new RouteCenter(bboxActual.toArea(), nodes, tabA, tabB); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/AngleChecker.java0000644000076400007640000003315312613617341023566 0ustar stevesteve/* * Copyright (C) 2015 * * 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.imgfmt.app.net; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import uk.me.parabola.log.Logger; import uk.me.parabola.util.EnhancedProperties; /** * Find sharp angles at junctions. The Garmin routing algorithm doesn't * like to route on roads building a sharp angle. It adds a time penalty * from 30 to 150 seconds and often prefers small detours instead. * The penalty depends on the road speed and the vehicle, for pedestrian * mode it is zero, for bicycles it is rather small, for cars it is high. * The sharp angles typically don't exist in the real world, they are * caused by the simplifications done by mappers. * * Maps created for cyclists typically "abuse" the car routing for racing * bikes, but in this scenario the time penalties are much too high, * and detours are likely. * * This method tries to modify the initial heading values of the arcs * which are used to calculate the angles. Where possible, the values are * changed so that angles appear larger. * * @author Gerd Petermann * */ public class AngleChecker { private static final Logger log = Logger.getLogger(AngleChecker.class); private boolean ignoreSharpAngles; private boolean cycleMap; // private final Coord test = new Coord(48.074815,16.272771); private final int MIN_ANGLE = 0x10; private final int MIN_LOW_SPEED_ANGLE = 0x20; private int mask; // helper class to collect multiple arcs with (nearly) the same initial headings private class ArcGroup { float initialHeading; byte imgHeading; int isOneWayTrueCount; int isForwardTrueCount; int maxRoadSpeed; byte orAccessMask; HashSet roadDefs = new HashSet<>(); List arcs = new ArrayList<>(); public void addArc(RouteArc arc) { arcs.add(arc); if (arc.getRoadDef().isOneway()) isOneWayTrueCount++; if (arc.isForward()) isForwardTrueCount++; if (arc.getRoadDef().getRoadSpeed() > maxRoadSpeed) maxRoadSpeed = arc.getRoadDef().getRoadSpeed(); orAccessMask |= arc.getRoadDef().getAccess(); roadDefs.add(arc.getRoadDef()); } public float getInitialHeading() { return initialHeading; } public boolean isOneway() { return isOneWayTrueCount == arcs.size(); } public boolean isForward() { return isForwardTrueCount == arcs.size(); } /** * @return */ public void setInitialHeading(float modIH) { while (modIH > 180) modIH -= 360; while (modIH < -180) modIH += 360; initialHeading = modIH; imgHeading = (byte) (RouteArc.directionFromDegrees(initialHeading) & mask); for (RouteArc arc : arcs){ arc.setInitialHeading(modIH); } } public String toString(){ return arcs.get(0).toString(); } } public void config(EnhancedProperties props) { // undocumented option - usually used for debugging only ignoreSharpAngles = props.getProperty("ignore-sharp-angles", false); cycleMap = props.getProperty("cycle-map", false); // float a = 0; // for (int i = 0; i <= 1440; i++){ // int ar = (int) Math.round(a * 256.0 / 360); // int am = ar & 0xf0; // log.error(a,ar,"0x" + Integer.toHexString(am)); // a +=0.25; // if (a >= 180) // a -= 360; // } return; } public void check(Map nodes) { if (!ignoreSharpAngles){ byte sharpAnglesCheckMask = cycleMap ? (byte) (0xff & ~AccessTagsAndBits.FOOT) : AccessTagsAndBits.BIKE; for (RouteNode node : nodes.values()){ mask = 0xf0; // we assume compacted format fixSharpAngles(node, sharpAnglesCheckMask); } } } public void fixSharpAngles(RouteNode node, byte sharpAnglesCheckMask) { // get direct arcs leaving the node List arcGroups = buildArcGroups(node); int n = arcGroups.size(); if (n <= 1) return; // sort the arcs by initial heading Collections.sort(arcGroups, new Comparator() { public int compare(ArcGroup ag1, ArcGroup ag2) { if (ag1.initialHeading < ag2.initialHeading) return -1; if (ag1.initialHeading > ag2.initialHeading) return 1; return 0; } }); class AngleAttr { int angle; int maskedAngle; int maskedMinAngle = MIN_ANGLE; boolean noAccess; int maskedDeltaToMin(){ return maskedAngle - maskedMinAngle; } void setMaskedMinAngle(int maskedMinAngle){ this.maskedMinAngle = maskedMinAngle; } public String toString(){ return angle + "° " + maskedAngle + " " + maskedMinAngle + " " + noAccess; } } // step one: calculate the existing angles AngleAttr[] angles = new AngleAttr[n]; for (int i = 0; i < n; i++){ ArcGroup ag1 = arcGroups.get(i); ArcGroup ag2 = arcGroups.get(i+1 < n ? i+1 : 0); AngleAttr angleAttr = new AngleAttr(); angles[i] = angleAttr; angleAttr.angle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); angleAttr.maskedAngle = ag2.imgHeading - ag1.imgHeading; if (i + 1 >= n){ angleAttr.angle += 360; } if (angleAttr.maskedAngle < 0) angleAttr.maskedAngle += 256; if (ag1.isOneway() && ag1.isForward()){ // the "incoming" arc is a wrong direction oneway angleAttr.noAccess = true; } else if (ag2.isOneway() && ag2.isForward() == false){ // the "outgoing" arc is a wrong direction oneway angleAttr.noAccess = true; } // if (node.getCoord().distance(test) < 2){ // if (angleAttr.angle == 20){ // angleAttr.maskedMinAngle = 0x30; // continue; // } // } int sumSpeeds = ag1.maxRoadSpeed + ag2.maxRoadSpeed; if (sumSpeeds <= 1) continue; byte pathAccessMask = (byte) (ag1.orAccessMask & ag2.orAccessMask); if (pathAccessMask == 0){ // no common vehicle allowed on both arcs angleAttr.noAccess = true; } if (angleAttr.noAccess) continue; int maskedMinAngle = MIN_LOW_SPEED_ANGLE; // the Garmin algorithm sees rounded values, so the thresholds are probably // near 22.5 (0x10), 45(0x20), 67.5 (0x30), 90, 112.5 (0x40) // the following code doesn't seem to improve anything, I leave it as comment // for further experiments. // if (cycleMap){ // if (sumSpeeds >= 14) // maskedMinAngle = 0x80; // if (sumSpeeds >= 12) // maskedMinAngle = 0x70; // if (sumSpeeds >= 10) // maskedMinAngle = 0x60; // if (sumSpeeds >= 8) // maskedMinAngle = 0x50; // else if (sumSpeeds >= 6) // maskedMinAngle = 0x40; // else if (sumSpeeds >= 4) // maskedMinAngle = 0x30; // } angleAttr.setMaskedMinAngle(maskedMinAngle); if (angleAttr.maskedDeltaToMin() >= 0) continue; String ignoredReason = null; if (pathAccessMask == AccessTagsAndBits.FOOT) ignoredReason = "because it can only be used by pedestrians"; else if ((pathAccessMask & sharpAnglesCheckMask) == 0) ignoredReason = "because it can not be used by bike"; else if (ag1.isOneway() && ag2.isOneway()){ // both arcs are one-ways, probably the road splits here // to avoid the sharp angles we are looking for ignoredReason = "because it seems to be a flare road"; } else if (ag1.roadDefs.size() == 1 && ag2.roadDefs.size() == 1 && ag1.roadDefs.containsAll(ag2.roadDefs)){ ignoredReason = "because both arcs belong to the same road"; } if (ignoredReason != null){ if (log.isInfoEnabled()){ String sharpAngle = "sharp angle " + angleAttr.angle + "° at " + node.getCoord().toDegreeString(); log.info(sharpAngle, "headings",getCompassBearing(ag1.getInitialHeading()) , getCompassBearing(ag2.getInitialHeading()),"speeds",ag1.maxRoadSpeed, ag2.maxRoadSpeed); log.info("ignoring", sharpAngle, ignoredReason); } angleAttr.setMaskedMinAngle(MIN_ANGLE); angleAttr.noAccess = true; } } for (int i = 0; i < n; i++){ AngleAttr aa = angles[i]; if (aa.maskedAngle >= aa.maskedMinAngle || aa.noAccess) continue; int oldAngle = aa.angle; ArcGroup ag1 = arcGroups.get(i); ArcGroup ag2 = arcGroups.get(i+1 < n ? i+1 : 0); String sharpAngle = ""; if (log.isInfoEnabled()){ sharpAngle = "sharp angle " + aa.angle + "° at " + node.getCoord().toDegreeString(); log.info(sharpAngle, "headings",getCompassBearing(ag1.getInitialHeading()) , getCompassBearing(ag2.getInitialHeading()),"speeds",ag1.maxRoadSpeed, ag2.maxRoadSpeed); } // XXX restrictions ? boolean fixed = false; int wantedIncrement = Math.abs(aa.maskedDeltaToMin()) ; AngleAttr predAA = angles[i == 0 ? n - 1 : i - 1]; AngleAttr nextAA = angles[i >= n - 1 ? 0 : i + 1]; // we can increase the angle by changing the heading values of one or both arcs // find out which one to change first byte origImgDir1 = ag1.imgHeading; byte origImgDir2 = ag2.imgHeading; int origImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); int deltaPred = predAA.maskedDeltaToMin(); int deltaNext = nextAA.maskedDeltaToMin(); if (deltaNext > 0 && (deltaNext > deltaPred || deltaPred < wantedIncrement)){ int usedIncrement = Math.min(wantedIncrement, deltaNext); float oldIH = ag2.getInitialHeading(); int modIH = ag2.imgHeading + usedIncrement; if (modIH > 128) modIH -= 256; ag2.setInitialHeading(modIH * 360/256); int modAngle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); if (modAngle < 0) modAngle += 360; int modImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); if (modImgAngle >= aa.maskedMinAngle) fixed = true; log.info(sharpAngle, "changing arc with heading", getCompassBearing(oldIH), "->",getCompassBearing(ag2.getInitialHeading()), "angle is now",modAngle+"°, in img format:",origImgDir2,"->",ag2.imgHeading, "img angle (0-255)",origImgAngle, "->", modImgAngle); aa.angle = modAngle; nextAA.angle -= usedIncrement; } if (!fixed && deltaPred > 0){ wantedIncrement = Math.abs(aa.maskedDeltaToMin()); int usedIncrement = Math.min(wantedIncrement, deltaPred); float oldIH = ag1.getInitialHeading(); int modIH = ag1.imgHeading - usedIncrement; if (modIH < -128) modIH += 256; ag1.setInitialHeading(modIH * 360/256); int modAngle = Math.round(ag2.getInitialHeading() - ag1.getInitialHeading()); if (modAngle < 0) modAngle += 360; int modImgAngle = getImgAngle(ag1.imgHeading, ag2.imgHeading); if (modImgAngle >= aa.maskedMinAngle) fixed = true; log.info(sharpAngle, "changing arc with heading", getCompassBearing(oldIH), "->", getCompassBearing(ag1.getInitialHeading()), "angle is now",modAngle+"°, in img format:",origImgDir1,"->",ag1.imgHeading, "img angle (0-255)",origImgAngle, "->", modImgAngle); aa.angle = modAngle; predAA.angle -= usedIncrement; } if (!fixed){ if (aa.angle == oldAngle) log.info(sharpAngle, "don't know how to fix it"); else log.info(sharpAngle, "don't know how to enlarge it further"); } } return; } /** * Combine arcs with nearly the same initial heading. * @param node * @return */ private List buildArcGroups(RouteNode node) { List arcGroups = new ArrayList<>(); List directArcs = new ArrayList<>(); for (RouteArc arc : node.getArcs()){ if (arc.isDirect()){ directArcs.add(arc); } } if (directArcs.size() < 2) return arcGroups; // should not happen // sort the arcs by initial heading Collections.sort(directArcs, new Comparator() { public int compare(RouteArc ra1, RouteArc ra2) { if (ra1.getInitialHeading() < ra2.getInitialHeading()) return -1; if (ra1.getInitialHeading() > ra2.getInitialHeading()) return 1; int d = Integer.compare(ra1.getPointsHash(), ra2.getPointsHash()); if (d != 0) return d; d = Long.compare(ra1.getRoadDef().getId() , ra2.getRoadDef().getId()); if (d != 0) return d; return d; } }); Iterator iter = directArcs.listIterator(); RouteArc arc1 = iter.next(); boolean addArc1 = false; while (iter.hasNext() || addArc1){ ArcGroup ag = new ArcGroup(); ag.initialHeading = arc1.getInitialHeading(); ag.addArc(arc1); arcGroups.add(ag); addArc1 = false; while (iter.hasNext()){ RouteArc arc2 = iter.next(); if (Math.abs(arc1.getInitialHeading()- arc2.getInitialHeading()) < 1){ if (arc1.getDest() != arc2.getDest() && arc1.getRoadDef().getId() != arc2.getRoadDef().getId()) log.warn("sharp angle < 1° at",node.getCoord().toDegreeString(),",maybe duplicated OSM way with bearing",getCompassBearing(arc1.getInitialHeading())); ag.addArc(arc2); } else{ arc1 = arc2; if (iter.hasNext() == false) addArc1 = true; break; } } } for (ArcGroup ag : arcGroups){ ag.imgHeading = (byte) (RouteArc.directionFromDegrees(ag.initialHeading) & mask); } return arcGroups; } /** * for log messages */ private String getCompassBearing (float bearing){ float cb = (bearing + 360) % 360; return Math.round(cb) + "°"; } /** * Debugging aid: guess what angle the Garmin algorithm is using. * @param heading1 * @param heading2 * @return */ private int getImgAngle(byte heading1, byte heading2){ int angle = heading2 - heading1; if (angle < 0) angle += 256; if (angle > 255) angle -= 256; return angle; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NODFile.java0000644000076400007640000001315612441246225022472 0ustar stevesteve/* * Copyright (C) 2008 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: 06-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.Collections; import java.util.List; import uk.me.parabola.imgfmt.app.BufferedImgFileReader; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; import uk.me.parabola.imgfmt.app.SectionWriter; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; /** * The NOD file that contains routing information. * * NOD1 contains several groups of routing nodes. * NOD2 contains road data with links into NOD1. * * NOD1 contains links back to NET (and NET contains links to NOD2). So there * is a loop and we have to write one section first, retaining the offsets * and then go back and fill in offsets that were found later. * * I'm choosing to this with Table A, as the records are fixed size and so * we can write them blank the first time and then go back and fix them * up, once the NET offsets are known. * * So we are writing NOD first before NET and NOD1 before NOD2. Once NET is * written then go back to Table A and fix the label offsets in RGN. * * @author Steve Ratcliffe */ public class NODFile extends ImgFile { private static final Logger log = Logger.getLogger(NODFile.class); private final NODHeader nodHeader = new NODHeader(); private List centers = new ArrayList(); private List roads = new ArrayList(); private List boundary = new ArrayList(); public NODFile(ImgChannel chan, boolean write) { setHeader(nodHeader); if (write) { setWriter(new BufferedImgFileWriter(chan)); position(NODHeader.HEADER_LEN); } else { setReader(new BufferedImgFileReader(chan)); nodHeader.readHeader(getReader()); } } public void write() { writeNodes(); writeRoadData(); writeBoundary(); writeHighClassBoundary(); } public void writePost() { ImgFileWriter writer = new SectionWriter(getWriter(), nodHeader.getNodeSection()); for (RouteCenter rc : centers) { rc.writePost(writer); } // Refresh the header position(0); getHeader().writeHeader(getWriter()); } /** * Write the nodes (NOD 1). This is done first as the offsets into * this section are needed to write NOD2. */ private void writeNodes() { ImgFileWriter writer = getWriter(); nodHeader.setNodeStart(writer.position()); Section section = nodHeader.getNodeSection(); writer = new SectionWriter(writer, section); int[] classBoundaries = nodHeader.getClassBoundaries(); for (RouteCenter cp : centers){ cp.write(writer, classBoundaries); } for (int i = 4; i >= 0; --i){ if (classBoundaries[i] > writer.position()) classBoundaries[i] = writer.position(); } nodHeader.setNodeSize(writer.position()); log.debug("the nod offset", Integer.toHexString(getWriter().position())); Section.close(writer); } /** * Write the road data (NOD2). */ private void writeRoadData() { log.info("writeRoadData"); ImgFileWriter writer = new SectionWriter(getWriter(), nodHeader.getRoadSection()); boolean debug = log.isDebugEnabled(); for (RoadDef rd : roads) { if(debug) log.debug("wrting nod2", writer.position()); rd.writeNod2(writer); } if(debug) log.debug("ending nod2", writer.position()); nodHeader.setRoadSize(writer.position()); } /** * Write the boundary node table (NOD3). */ private void writeBoundary() { log.info("writeBoundary"); Collections.sort(boundary); ImgFileWriter writer = new SectionWriter(getWriter(), nodHeader.getBoundarySection()); boolean debug = log.isDebugEnabled(); for (RouteNode node : boundary) { if(debug) log.debug("wrting nod3", writer.position()); node.writeNod3OrNod4(writer); } if(debug) log.debug("ending nod3", writer.position()); nodHeader.setBoundarySize(writer.position()); } /** * Write the high class boundary node table (NOD4). * Like NOD3, but contains only nodes on roads with class > 0 */ private void writeHighClassBoundary() { log.info("writeBoundary"); // Collections.sort(boundary); // already sorted for NOD3 Section section = nodHeader.getHighClassBoundary(); int pos = section.getPosition(); pos = (pos + 0x200) & ~0x1ff; // align on 0x200 int numBytesToWrite = pos - section.getPosition(); for (int i = 0; i < numBytesToWrite; i++) getWriter().put((byte)0); section.setPosition(pos); ImgFileWriter writer = new SectionWriter(getWriter(), section); boolean debug = log.isDebugEnabled(); for (RouteNode node : boundary) { if (node.getNodeClass() == 0) continue; if(debug) log.debug("wrting nod4", writer.position()); node.writeNod3OrNod4(writer); } if(debug) log.debug("ending nod4", writer.position()); nodHeader.setHighClassBoundarySize(writer.position()); } public void setNetwork(List centers, List roads, List boundary) { this.centers = centers; this.roads = roads; this.boundary = boundary; } public void setDriveOnLeft(boolean dol) { nodHeader.setDriveOnLeft(dol); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NumberStyle.java0000644000076400007640000000253712102024715023514 0ustar stevestevepackage uk.me.parabola.imgfmt.app.net; /** * The number style down one side of a side of a road. * * @author Steve Ratcliffe */ public enum NumberStyle { NONE(0), // No numbers. EVEN(1) { public int round(int val, int direction) { if ((val & 1) == 1) return val + direction; return val; } }, // Numbers are even on this side of the road. ODD(2) { @Override public int round(int val, int direction) { if ((val & 1) == 0) return val + direction; return val; } }, // Numbers are odd on this side of the road BOTH(3), // Both odd and even numbers (can also be used for a range of a single number) ; private final int val; NumberStyle(int val) { this.val = val; } public int getVal() { return val; } public int round(int val, int direction) { return val; } public static NumberStyle fromInt(int n) { switch (n) { case 0: return NONE; case 1: return EVEN; case 2: return ODD; case 3: return BOTH; default: return NONE; } } public String toString() { return super.toString().substring(0, 1); } public static NumberStyle fromChar(String string) { switch (string.charAt(0)) { case 'N': return NONE; case 'E': return EVEN; case 'O': return ODD; case 'B': return BOTH; case '0': System.err.println("zero instead of capital O in number spec"); return ODD; default: return NONE; } } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/NETHeader.java0000644000076400007640000000614712443534356023022 0ustar stevesteve/* * Copyright (C) 2007 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: Jan 5, 2008 */ package uk.me.parabola.imgfmt.app.net; import uk.me.parabola.imgfmt.ReadFailedException; import uk.me.parabola.imgfmt.app.CommonHeader; import uk.me.parabola.imgfmt.app.ImgFileReader; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Section; import uk.me.parabola.imgfmt.app.SectionWriter; /** * The header of the NET file. * * @author Steve Ratcliffe */ public class NETHeader extends CommonHeader { public static final int HEADER_LEN = 55; // Other lengths are possible private static final char SORTED_ROAD_RECSIZE = 3; private final Section roadDefinitions = new Section(); private final Section segmentedRoads = new Section(roadDefinitions); private final Section sortedRoads = new Section(segmentedRoads, SORTED_ROAD_RECSIZE); private byte roadShift; private byte segmentShift; public NETHeader() { super(HEADER_LEN, "GARMIN NET"); } /** * Read the rest of the header. Specific to the given file. It is guaranteed * that the file position will be set to the correct place before this is * called. * * @param reader The header is read from here. */ protected void readFileHeader(ImgFileReader reader) throws ReadFailedException { roadDefinitions.readSectionInfo(reader, false); roadShift = reader.get(); segmentedRoads.readSectionInfo(reader, false); segmentShift = reader.get(); sortedRoads.readSectionInfo(reader, true); reader.getInt(); reader.get(); reader.get(); } /** * Write the rest of the header. It is guaranteed that the writer will be set * to the correct position before calling. * * @param writer The header is written here. */ protected void writeFileHeader(ImgFileWriter writer) { roadDefinitions.writeSectionInfo(writer); writer.put(roadShift); // offset multiplier segmentedRoads.writeSectionInfo(writer); writer.put(segmentShift); // offset multiplier sortedRoads.writeSectionInfo(writer); writer.putInt(0); writer.put((byte) 1); writer.put((byte) 0); } ImgFileWriter makeRoadWriter(ImgFileWriter writer) { roadDefinitions.setPosition(writer.position()); return new SectionWriter(writer, roadDefinitions); } ImgFileWriter makeSortedRoadWriter(ImgFileWriter writer) { sortedRoads.setPosition(writer.position()); return new SectionWriter(writer, sortedRoads); } public int getRoadDefinitionsStart() { return roadDefinitions.getPosition(); } public int getSortedRoadsStart() { return sortedRoads.getPosition(); } public int getSortedRoadsEnd() { return sortedRoads.getEndPos(); } public int getRoadShift() { return roadShift & 0xff; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/TableC.java0000644000076400007640000000557612310532056022406 0ustar stevesteve/* * Copyright (C) 2008 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, Robert Vollmert * Create date: 18-Jul-2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.List; import uk.me.parabola.imgfmt.FormatException; import uk.me.parabola.imgfmt.app.ImgFileWriter; /** * @author Steve Ratcliffe, Robert Vollmert */ public class TableC { // size of the table, excluding the size field private int size; private final TableA tabA; private final List restrictions = new ArrayList(); public TableC(TableA tabA) { this.tabA = tabA; } /** * Write the table including size field. */ public void write(ImgFileWriter writer, int tablesOffset) { if (!restrictions.isEmpty()) { if (size < 0x100) writer.put((byte) size); else writer.putChar((char) size); for (RouteRestriction restr : restrictions) restr.write(writer, tablesOffset); } if(tabA.numRoundaboutArcs() > 0) writer.put((byte)tabA.numRoundaboutArcs()); if(tabA.numUnpavedArcs() > 0) writer.put((byte)tabA.numUnpavedArcs()); if(tabA.numFerryArcs() > 0) writer.put((byte)tabA.numFerryArcs()); } /** * Add a restriction. * * @param restr A new restriction. * @return The offset into Table C at which the restriction will * be written. */ public int addRestriction(RouteRestriction restr) { int offset = size; restrictions.add(restr); size += restr.getSize(); return offset; } /** * The size of restriction indices in the nodes area. */ private byte getOffsetSize() { if (size < 0x80) return 1; // allows 7 bit index (8th bit is flag) else if (size < 0x8000) return 2; // allows 15 bit index (16th bit is flag) else // XXX: haven't seen larger than 2, may well be possible throw new FormatException("too many restrictions"); } public void propagateSizeBytes() { byte b = getOffsetSize(); for (RouteRestriction restr : restrictions) restr.setOffsetSize(b); } public byte getFormat() { // Table C format bitmask // 0x01 = 1-255 bytes of restrictions // 0x02 = 256-65535 bytes of restrictions // 0x08 = unpaved roads count present // 0x10 = ferry count present int format = 0; if(size > 0) { ++format; if(size > 0xff) ++format; } if(tabA.numRoundaboutArcs() > 0) format |= 0x04; if(tabA.numUnpavedArcs() > 0) format |= 0x08; if(tabA.numFerryArcs() > 0) format |= 0x10; return (byte)format; } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/net/package.html0000644000076400007640000000012611135172606022661 0ustar stevesteve

The NET and NOD files

These files are used for routing.

mkgmap-r3660/src/uk/me/parabola/imgfmt/app/trergn/0000755000076400007640000000000012651103763021116 5ustar stevestevemkgmap-r3660/src/uk/me/parabola/imgfmt/app/trergn/Zoom.java0000644000076400007640000000416611034116042022700 0ustar stevesteve/* * 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 uk.me.parabola.imgfmt.app.ImgFileWriter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * A zoom level (or map level) determines the amount of detail that * is shown as you zoom in and out. * Level 0 is the the most detailed level. * * A zoom level has a number of bits per co-ordinate and the number of * subdivisions at that level. * * The highest level must have one subdivision and have no elements I believe. * * @author Steve Ratcliffe */ public class Zoom { private final int level; private boolean inherited; private final int resolution; private final List subdivs = new ArrayList(); /** * Create a new zoom level. * * @param zoom The level between 0 and 15. * @param resolution The number of bits per coordinate, up to 24. */ Zoom(int zoom, int resolution) { this.level = zoom; this.resolution = resolution; } public Iterator subdivIterator() { return subdivs.iterator(); } public void setInherited(boolean inherited) { this.inherited = inherited; } public int getLevel() { return level; } public int getResolution() { return resolution; } public int getShiftValue() { return 24 - resolution; } public void write(ImgFileWriter file) { file.put((byte) ((level & 0xf) | (inherited ? 0x80 : 0))); file.put((byte) resolution); file.putChar((char) subdivs.size()); } public String toString() { return "L " + level + ':' + resolution; } void addSubdivision(Subdivision div) { subdivs.add(div); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/trergn/Subdivision.java0000644000076400007640000004121512430076064024260 0ustar stevesteve/* * 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 bitsPerCoord 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 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 shift 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(); } } mkgmap-r3660/src/uk/me/parabola/imgfmt/app/trergn/TREFile.java0000644000076400007640000002326412441246225023220 0ustar stevesteve/* * 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: 03-Dec-2006 */ package uk.me.parabola.imgfmt.app.trergn; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.Area; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.fs.ImgChannel; import uk.me.parabola.log.Logger; import uk.me.parabola.util.Configurable; import uk.me.parabola.util.EnhancedProperties; /** * This is the file that contains the overview of the map. There * can be different zoom levels and each level of zoom has an * associated set of subdivided areas. Each of these areas then points * into the RGN file. * * This is quite a complex file as there are quite a few miscellaneous pieces * of information stored. * * @author Steve Ratcliffe */ public class TREFile extends ImgFile implements Configurable { private static final Logger log = Logger.getLogger(TREFile.class); // Zoom levels for map // private List mapLevels = new ArrayList(); private final Zoom[] mapLevels = new Zoom[16]; private final List