Subversion Repositories mkgmap

Rev

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

/*
 * Copyright (C) 2009.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.app.ImgFileWriter;
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_HAS_MDR17 = 0x4;

        private static final int MAX_NAME_OFFSET = 127;

        private final int codepage;
        private final boolean isMulti;
        private final boolean splitName;

        private Set<Mdr7Record> roadsPerMap= new HashSet<>();
        private ArrayList<Mdr7Record> allStreets = new ArrayList<>();
        private ArrayList<Mdr7Record> streets = new ArrayList<>();
        private int lastMaxIndex = -1;

        private int partialInfoSize;
        private Set<String> exclNames;
        private final Sort sort;
        private int maxPrefixCount;
        private int maxSuffixCount;
        private boolean magicIsValid;
        private int magic;

        public Mdr7(MdrConfig config) {
                setConfig(config);
                sort = config.getSort();
                splitName = config.isSplitName();
                exclNames = config.getMdr7Excl();
                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 or 0x1c. 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)
                        sep = name.indexOf(0x1b);
                if (sep > 0) {
                        prefix = sep + 1;
                }

                // Find a name suffix which begins with 0x1 or 0x1c
                sep = name.indexOf(0x1f);
                if (sep < 0)
                        sep = name.indexOf(0x1c);
                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);
                storeMdr7(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);
                                if (!exclNames.contains(st.getPartialName()))
                                        storeMdr7(st);

                                start = false;
                        }

                        outOffset += outSize(c);
                        if (outOffset > MAX_NAME_OFFSET)
                                break;
                }
        }

        /**
         * Store in array if not already done
         * @param st the mdr7 record
         */

        private void storeMdr7(Mdr7Record st) {
                if (lastMaxIndex != st.getMapIndex()) {
                        // we process all roads of one map tile sequentially, so we can clear the set with each new map tile
                        lastMaxIndex = st.getMapIndex();
                        roadsPerMap.clear();
                }
               
                if (roadsPerMap.add(st)) {
                        allStreets.add(st);
                }
        }

        /**
         * 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.                  
         */

        @Override
        protected void preWriteImpl() {
               
                LargeListSorter<Mdr7Record> partialSorter = new LargeListSorter<Mdr7Record>(sort) {
                        @Override
                        protected SortKey<Mdr7Record> makeKey(Mdr7Record r, Sort sort, Map<String, byte[]> cache) {
                                return sort.createSortKey(r, r.getPartialName(), 0, cache); // first sort by partial name only
                        }
                };
               
                ArrayList<Mdr7Record> sorted = new ArrayList<>(allStreets);
                allStreets.clear();
                partialSorter.sort(sorted);
                // list is now sorted by partial name only, we have to group by name and map index now
                String lastPartial = null;
                List<Mdr7Record> samePartial = new ArrayList<>();
                Collator collator = sort.getCollator();
                collator.setStrength(Collator.SECONDARY);
                for (int i = 0; i < sorted.size(); i++) {
                        Mdr7Record r = sorted.get(i);
                        String partial = r.getPartialName();
                        if (lastPartial == null || collator.compare(partial, lastPartial) != 0) {
                                groupByNameAndMap(samePartial);
                                samePartial.clear();
                        }
                        samePartial.add(r);
                        lastPartial = partial;
                }
                groupByNameAndMap(samePartial);
               
                allStreets.trimToSize();
                streets.trimToSize();
        }

        /**
         * Group a list of roads with the same partial name.
         * @param samePartial
         */

        private void groupByNameAndMap(List<Mdr7Record> samePartial) {
                if (samePartial.isEmpty())
                        return;
               
                // Basecamp needs the records grouped by partial name, full name, and map index.
                // This sometimes presents search results in the wrong order. The partial sort fields allow to
                // tell the right order.
               
                LargeListSorter<Mdr7Record> fullNameSorter = new LargeListSorter<Mdr7Record>(sort) {
                        @Override
                        protected SortKey<Mdr7Record> makeKey(Mdr7Record r, Sort sort, Map<String, byte[]> cache) {
                                return sort.createSortKey(r, r.getName(), r.getMapIndex(), cache);
                        }
                };
               
               
                fullNameSorter.sort(samePartial);
                Mdr7Record last = null;
                int recordNumber = streets.size();
               
                // list is now sorted by partial name, name, and map index
                // De-duplicate the street names so that there is only one entry
                // per map for the same name.
                for (int i = 0; i < samePartial.size(); i++) {
                        Mdr7Record r = samePartial.get(i);
                        if (last != null && r.getMapIndex() == last.getMapIndex() && r.getName().equals(last.getName())) {
                                // 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++;
                                r.setIndex(recordNumber);
                                streets.add(r);
                        }
                        if (r.getCity() != null)
                                allStreets.add(r);
                        last = r;
                }
        }

        public void writeSectData(ImgFileWriter writer) {
                boolean hasStrings = hasFlag(MDR7_HAS_STRING);
                boolean hasNameOffset = hasFlag(MDR7_HAS_NAME_OFFSET);
                Collator collator = sort.getCollator();
                collator.setStrength(Collator.SECONDARY);
                Mdr7Record last = null;
                for (Mdr7Record s : streets) {
                        addIndexPointer(s.getMapIndex(), s.getIndex());

                        putMapIndex(writer, s.getMapIndex());
                        int lab = s.getLabelOffset();
                       
                        int rr = s.checkRepeat(last, collator);
                        if (rr != 3)
                                lab |= 0x800000;
                       
                        writer.put3u(lab);
                        if (hasStrings)
                                putStringOffset(writer, s.getStringOffset());

                        if (hasNameOffset)
                                writer.put1u(s.getOutNameOffset());
                        if (partialInfoSize > 0) {
                                int trailingFlags = ((rr & 1) == 0) ? 1 : 0;
                                writer.putNu(partialInfoSize, trailingFlags);
                        }
                        last = s;
                }
        }

        /**
         * 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 + partialInfoSize;
                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() {
                if (!magicIsValid) {
                        int bitsPrefix = (maxPrefixCount == 0) ? 0 : Integer.numberOfTrailingZeros(Integer.highestOneBit(maxPrefixCount)) + 1;
                        int bitsSuffix = (maxSuffixCount == 0) ? 0 : Integer.numberOfTrailingZeros(Integer.highestOneBit(maxSuffixCount)) + 1;
                        int bits = bitsPrefix + bitsSuffix + 1;
                        partialInfoSize = 1 + bits / 8;
                        assert bitsSuffix <= 15;
                        magic = MDR7_U1 | MDR7_HAS_NAME_OFFSET | (partialInfoSize << MDR7_PARTIAL_SHIFT) | (bitsPrefix << 9);

                        if (isForDevice()) {
                                if (!getConfig().getSort().isMulti())
                                        magic |= MDR7_HAS_MDR17; // mdr17 sub section present (not with unicode)
                        } else {
                                magic |= MDR7_HAS_STRING;
                        }
                        int unk2size = (magic >> 6) & 0x7;
                        int unk2split = ((magic >> 9) & 0xf);
                        assert unk2size == partialInfoSize;
                        assert unk2split == bitsPrefix;
                        magicIsValid = true;
                }
                return magic;
        }

        @Override
        protected void releaseMemory() {
                allStreets = null;
                streets = null;
        }


        public List<Mdr7Record> getStreets() {
                return Collections.unmodifiableList(allStreets);
        }
       
        public List<Mdr7Record> getSortedStreets() {
                return Collections.unmodifiableList(streets);
        }

       
        /**
         * Free as much memory as possible.
         */

        public void trim() {
                allStreets.trimToSize();
                roadsPerMap = new HashSet<>();
        }
}