/*
* 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);
}
}