/* * 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<Mdr7Record> allStreets = new ArrayList<>(); private List<Mdr7Record> 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<SortKey<Mdr7Record>> 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<Mdr7Record> 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<Mdr8Record> getIndex() { List<Mdr8Record> 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<Mdr7Record> getStreets() { return Collections.unmodifiableList(allStreets); } public List<Mdr7Record> getSortedStreets() { return Collections.unmodifiableList(streets); } }