Subversion Repositories display

Rev

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

/*
 * 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 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 test.display;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import test.check.CommonCheck;
import test.display.check.BoundChecker;
import test.display.check.MapDetailList;
import test.display.check.MapDetails;
import test.display.check.MdrStrings;
import test.display.check.Street;
import test.display.check.SubsectionList;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.lbl.City;
import uk.me.parabola.imgfmt.app.mdr.Mdr14Record;
import uk.me.parabola.imgfmt.app.mdr.Mdr28Record;
import uk.me.parabola.imgfmt.app.mdr.Mdr29Record;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.app.trergn.MapObject;
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.imgfmt.mdxfmt.MapInfo;
import uk.me.parabola.imgfmt.mdxfmt.MdxFileReader;
import uk.me.parabola.imgfmt.mps.MapBlock;
import uk.me.parabola.imgfmt.mps.MpsFileReader;
import uk.me.parabola.imgfmt.sys.FileImgChannel;
import uk.me.parabola.imgfmt.sys.ImgFS;
import uk.me.parabola.mkgmap.srt.SrtTextReader;

/**
 * Program to check the contents of an mdr file against the files that it indexes.
 *
 * @author Steve Ratcliffe
 */

public class MdrCheck extends CommonCheck {

        private int numberOfMaps;
        private Charset charSet;
        private boolean[] print = new boolean[100];

        private final MapDetailList details = new MapDetailList();

        private MdrStrings mdrStrings;

        private final List<Mdr20Info> cityMdr20 = new ArrayList<>();
        private final List<Name> streets = new ArrayList<>();

        private int[] streetMapNumbers;
        private int[] mdr20ToStreet;

        // These are the key to reading several other 2x sections
        private List<Mdr28Record> regionNames = new ArrayList<>();
        private List<Mdr29Record> countryNames = new ArrayList<>();
        private final List<TypeInfo> typeInfos = new ArrayList<>();
        private final Map<Integer, Integer> poiType = new HashMap<>();
        private final Map<Integer, Integer> groupEnds = new LinkedHashMap<>(); // from MDR9
       
        private final List<TypeInfoMdr10> typesFromMdr10 = new ArrayList<>();
        private Collator collator;
        private boolean mdr9IsOK;
        private int codepage;
        private boolean isMulti;


        public MdrCheck() {
                Arrays.fill(print, true);
        }

        @Override
        protected void print() {

                readHeader();
                readMaps();
               
                HuffmanDecoder decoder = new HuffmanDecoder();
                check16(decoder);

                mdrStrings = new MdrStrings(reader, getSection(15), charSet, decoder, true /* double check */);

                check5();
                check7();
                check20();

                check1();
                streetMapNumbers = null;
                mdr20ToStreet = null;

                check9();
                check10();
                check11();

                check13();

                check18();
                check19();

                // region and country keys to the other sections
                check28();
                check29();

                check23();
                check24();

                check25();
                check27();

                check21();
                check22();

                check26();
        }

        private void check1() {
                setShowLogs(print[1]);
                info("mdr1 check");

                if ((getSection(1).getMagic() & 1) == 0)
                        return;

                SubsectionList list = new SubsectionList(numberOfMaps);
                for (int i = 1; i <= numberOfMaps; i++) {
                        MapDetails map = details.getMap(i);
                        if (map == null)
                                return; // If we don't have all the maps, then give up currently.
                        list.read(reader, i, map.getSubHeaderOffset());
                }

                // Check that the totals summed across all the maps are correct.
                for (int s = 1; s <= 8; s++) {
                        int[] subNumbers = {0, 11, 10, 7, 5, 6, 20, 21, 22};
                        String[] subs = new String[]{
                                        "n/a",
                                        "mdr11", "mdr10", "mdr7", "mdr5",
                                        "mdr6", "mdr20", "mdr21", "mdr22"
                        };
                        int total = list.totalForSection(s);
                        int expected = getSection(subNumbers[(s == 2) ? s - 1 : s]).getNumberOfRecords();
                        checkEqual(expected, total, "sub%d (%s) total number", s, subs[s]);

                        list.setMax(s, expected);
                }

                // Now check that every pointer points to something that is in the correct map
                for (int ss = 1; ss <= 8; ss++) {
                        int max = 0;
                        for (int m = 1; m <= numberOfMaps; m++) {
                                int off = list.getOffset(m, ss);
                                int length = list.getLength(m, ss);

                                int size = list.getSize(ss);
                                int end = off + size * length;

                                reader.position(off);
                                info("mdr1 map%d sub%d; len %d, recsize %d", m, ss, length, size);

                                int count = 0;
                                while (reader.position() < end) {
                                        count++;
                                        int val = read(size);
                                        if (val > max) max = val;
                                        list.checkValue(m, ss, val);

                                        // In some cases we follow the pointer and check that it points to something
                                        // in the correct map.
                                        if (ss == 6) {
                                                int v2 = mdr20ToStreet[val];
                                                assert v2 != 0;
                                                int foundMapNumber = streetMapNumbers[v2];
                                                checkEqual(m, foundMapNumber,
                                                                "mdr1 map%d sub5; map number wrong, pointer %d, value %d",
                                                                m, count, val);
                                        }
                                }
                        }

                        int expectedMax = list.getMax(ss);
                        checkEqual(expectedMax, max, "mdr1 sub%d; expected max value", ss);
                }
        }

        /**
         * Check details of the cities.
         */

        private void check5() {
                setShowLogs(print[5]);
                info("mdr5 check");
                Section section = getSection(5);

                long start = section.getStart();
                long end = section.getEnd();
                int magic = section.getMagic();

                cityMdr20.add(null);

                int cityPtrSize = (magic & 0x3) + 1;
                boolean hasRegionCountry = (magic & 0x4) != 0;
                boolean hasStr = (magic & 0x8) != 0;
                boolean has20 = (magic & 0x100) != 0;
                boolean has20offset = (magic & 0x800) != 0;
                int mdr20PointerSize = 0;
                if (getSection(20).getLen() > 0) {
                        if (has20)
                                mdr20PointerSize = getSection(20).getBytesForRecords();

                        if (has20offset)
                                mdr20PointerSize = getSection(20).getBytesForSize();
                }

                boolean has28_29_offset = (magic & 0x400) != 0;
                int mdr28_29PointerSize = 0;
                if (has28_29_offset)
                        mdr28_29PointerSize = getSection(28).getBytesForRecords();
                if (has28_29_offset && mdr20PointerSize == 0)
                        mdr28_29PointerSize = getSection(29).getBytesForRecords();
                test("Sect5 flags %x %d %d %d", magic, cityPtrSize, mdr20PointerSize, mdr28_29PointerSize);

                reader.position(start);
                int citynum = 0;
                int lastMdr20 = 0;
                Mdr20Info last20Info = null;
                while (reader.position() < end) {
                        citynum++;

                        int mapNumber = readMapNumber();
                        assert mapNumber > 0 && mapNumber <= numberOfMaps : "Bad map number " + mapNumber;

                        int localCityNum = read(cityPtrSize);
                        int lbl = read(3);

                        int region = 0;
                        int country = 0;
                        boolean hasRegion = false;
                        if (hasRegionCountry) {
                                int val = read(2);
                                if ((val & 0x4000) == 0) {
                                        hasRegion = true;
                                        region = val & 0x3fff;
                                } else {
                                        country = val & 0x3fff;
                                }
                        }

                        String strText = null;
                        if (hasStr)
                                strText = readString();

                        int off20 = 0;
                        if (mdr20PointerSize > 0) {
                                off20 = read(mdr20PointerSize);
                                if (off20 != 0 && off20 < lastMdr20)
                                        error("%d map%d; out of order off20=%d last=%d", citynum, mapNumber, off20, lastMdr20);

                                if (off20 != 0)
                                        lastMdr20 = off20;
                        }

                        if (has28_29_offset)
                                read(mdr28_29PointerSize);

                        Mdr20Info m20 = new Mdr20Info(off20);
                        m20.mapNumber = mapNumber;

                        MapDetails map = details.getMap(mapNumber);
                        if (map != null) {
                                if (hasRegionCountry) {
                                        // There is always a country. If there is a region, then it will have a country.
                                        if (hasRegion)
                                                country = map.getCountryFromRegion(region);

                                        m20.regionName = map.getRegionName(region);
                                        m20.countryName = map.getCountryName(country);

                                        checkNotZero(country, "%d no country", citynum);
                                } else {
                                        // Get from the city.
                                        City city = map.getCity(localCityNum);
                                        int nreg = city.getRegionNumber();
                                        int ncountry;

                                        // If region, then get country from the region.
                                        if (hasRegion(city)) {
                                                ncountry = map.getCountryFromRegion(nreg);
                                        } else {
                                                ncountry = city.getCountryNumber();
                                        }

                                        m20.regionName = map.getRegionName(nreg);
                                        m20.countryName = map.getCountryName(ncountry);
                                }

                                // Print out the city information collected
                                String name = map.getLabelText(lbl & 0x7fffff);
                                boolean repeated = (lbl & 0x800000) == 0;
                                info("%d map%d; %s mapCity=%d reg=%d (%s) country=%d (%s) ind20=%d rep=%b\n",
                                                citynum, mapNumber, name, localCityNum,
                                                region, m20.regionName, country, m20.countryName,
                                                m20.mdr20index, repeated);

                                City city = map.getCity(localCityNum);
                                String nameFromCity = city.getName();
                                if (strText != null && !strText.isEmpty())
                                        checkEqual(name, strText, "map %d: city str table %d", mapNumber, citynum);
                                checkEqual(name, nameFromCity, "map %d: city %d city name", mapNumber, citynum);

                                m20.cityName = nameFromCity;

                                if (m20.sameCity(last20Info)) {
                                        if (!repeated)
                                                error("%d map%d; repeat flag not set on repeated name %s", citynum, mapNumber, nameFromCity);
                                } else {
                                        if (repeated)
                                                error("%d map%d; repeat flag set when not a repeat", citynum, mapNumber);
                                }
                                last20Info = m20;
                        } else {
                                if (strText != null && !strText.isEmpty())
                                        m20.cityName = strText;
                        }

                        cityMdr20.add(m20);
                }

                // Mark the last street
                Mdr20Info m = new Mdr20Info(getSection(20).getNumberOfRecords() + 1);
                cityMdr20.add(m);
        }

        private static boolean hasRegion(City city) {
                return (city.getRegionCountryNumber() & 0x4000) == 0;
        }

        /**
         * Check details of the streets.
         */

        private void check7() {
                setShowLogs(print[7]);
                info("mdr7 check");

                Section section = getSection(7);

                long start = section.getStart();
                long end = section.getEnd();
                int magic = section.getMagic();

                streetMapNumbers = new int[section.getNumberOfRecords() + 1];

                // fill up the 0 element.
                streets.add(null);

                reader.position(start);
                int streetnum = 0;
                Name lastName = null;
                boolean hasStr = (magic & 0x01) != 0;
                boolean hasNameOffset = (magic & 0x20) != 0;

                int partialInfoSize = ((magic >> 6) & 0x7);
                int partialBShift = ((magic >> 9) & 0xf);
                int partialBMask = (1 << partialBShift) - 1;
                Map<Integer, BitSet> roadsWithOffset0 = new HashMap<>();
                Map<Integer, BitSet> roadsWithOffset = new HashMap<>();
               
                while (reader.position() < end) {
                        streetnum++;

                        int mapNumber = readMapNumber();
                        streetMapNumbers[streetnum] = mapNumber;

                        int lblData = read(3);
                        int lblOff = lblData & 0x7fffff;
                        String stringData = (hasStr) ? readString() : null;

                        int nameOffset = 0;
                        if (hasNameOffset) {
                                nameOffset = read(1);
                                Map<Integer, BitSet> map = nameOffset == 0 ? roadsWithOffset0 : roadsWithOffset;
                                BitSet bs = map.get(mapNumber);
                                if (bs == null) {
                                        bs = new BitSet();
                                        map.put(mapNumber, bs);
                                }
                                bs.set(lblOff);
                        }

                        int partialInfo = 0;
                        boolean partialRepeat = false;
                        if (partialInfoSize > 0) {
                                partialInfo = read(partialInfoSize);
                                if ((partialInfo & 0x1) == 0) {
                                        // I call it a partial repeat when you are using the separators between the name
                                        // and the Street, Road etc. and the first part is repeated.
                                        // So
                                        // MAIN^ROAD
                                        // MAIN^ROAD -- full repeat
                                        // MAIN^STREET -- partial repeat
                                        partialRepeat = true;
                                }
                        }

                        MapDetails map = details.getMap(mapNumber);
                        if (map != null) {
                                String sname = map.getLabelText(lblOff);
                                Name name = new Name(mapNumber, sname, nameOffset);

                                streets.add(name);

                                boolean repeated = (lblData & 0x800000) == 0;
                                String repeatStr = repeatString(repeated, partialRepeat);
                                if (partialBShift > 0) {
                                        name.hasOrders = true;
                                        name.b = (partialInfo >>> 1) & partialBMask;
                                        name.s = (partialInfo >>> partialBShift + 1);
                                }
                                info("%d map%d lbl=0x%x street=%s %s off:%d  b=%d t=%d",
                                                streetnum, mapNumber, lblOff, name, repeatStr, nameOffset,
                                                name.b,
                                                name.s);

                                checkNames(lastName, name, repeated, partialRepeat);
                                lastName = name;
                        } else {
                                lastName = null;
                                streets.add(null);
                        }

                        assert streets.size() == streetnum + 1 : "streets size " + streets.size() + "/" + streetnum;
                }
                if (hasNameOffset) {
                        info("mdr7 name offset check");
                        for (Entry<Integer, BitSet> e : roadsWithOffset.entrySet()) {
                                BitSet bs0 = roadsWithOffset0.get(e.getKey());
                                if (bs0 != null) {
                                        e.getValue().andNot(bs0);
                                }
                                e.getValue().stream().forEach(lblOff -> {
                                        MapDetails map = details.getMap(e.getKey());
                                        String sname = null;
                                        if (map != null) {
                                                sname = map.getLabelText(lblOff);
                                        }
                                        if (sname != null) {
                                                error("map%d lbl=0x%x street=%s: no off=0 entry found, but off>0 was used", e.getKey(), lblOff, sname);
                                        } else {
                                                error("map%d lbl=0x%d: no off=0 entry found", e.getKey(), lblOff);
                                        }
                                });
                        }
                }
        }


        private static String repeatString(boolean repeated) {
                return repeatString(repeated, null);
        }

        private static String repeatString(boolean repeated, Boolean partialRepeat) {
                return String.format("(%s%s)", repeated ? "R" : "", Boolean.TRUE.equals(partialRepeat) ? "r" : "");
        }

        private void check9() {
                setShowLogs(print[9]);
                info("mdr9 check");
                Section section = getSection(9);

                long start = section.getStart();
                long end = section.getEnd();
                if (start == end)
                        return;
                int recsize = section.getRecordSize();

                reader.position(start);
                final Set<Integer> knownGroups = new HashSet<>(Arrays.asList(1,2,3,4,5,6,7,8,9,11,12,13,64,65,66));
                Map<Integer, Integer> groups = new LinkedHashMap<>();

                while (reader.position() < end) {
                        int group = read(1);
                        int mdr10Start = read(recsize - 1);
                        if (knownGroups.contains(group))
                                info("group %d starts with mdr-10 record %d", group, mdr10Start);
                        else {
                                error("unknown group %d starts with mdr-10 record %d",group, mdr10Start);
                        }
                        if (groups.isEmpty() && mdr10Start != 1) {
                                error("first group doesn't point to first mdr-10 record");
                                mdr9IsOK = false;
                        }
                        groups.put(mdr10Start, group);
                }
                Iterator<Entry<Integer, Integer>> iter = groups.entrySet().iterator();
                Entry<Integer, Integer> prevEntry = iter.next();
                while (iter.hasNext()) {
                        Entry<Integer, Integer> e = iter.next();
                        groupEnds.put(e.getKey()-1, prevEntry.getValue());
                        prevEntry = e;
                }
                groupEnds.put(Integer.MAX_VALUE, prevEntry.getValue());
               
        }
        private void check10() {
                setShowLogs(print[10]);
                info("mdr10 check");
                Section section = getSection(10);
                long start = section.getStart();
                long end = section.getEnd();
                if (start == end)
                        return;
                int numPois = getSection(11).getNumberOfRecords();
                int recsize = Section.numberOfBytes(numPois*2) + 1;
                int expectedItems = section.getLen() / recsize;
                if (expectedItems != numPois) {
                        error("Unexpected section size %d for %d pois in Mdr10, cannot check", section.getLen(),numPois);
                        return;
                }
                int mask = (1 << ((recsize -1) * 8) - 1) - 1;

                reader.position(start);
                int record = 0;
                Iterator<Entry<Integer, Integer>> iter = groupEnds.entrySet().iterator();
                Entry<Integer, Integer> groupEntry = iter.next();
                while (reader.position() < end) {
                        record++;
                        if (record > groupEntry.getKey())
                                groupEntry = iter.next();
                        int group = groupEntry.getValue();
                        int typeOrSubType = read(1);
                        TypeInfoMdr10 ti = new TypeInfoMdr10();
                        ti.isPolygon = group >= 64;
                        ti.type = toType(group, typeOrSubType);
                        ti.mdr11Rec = read(recsize -1) & mask;
                        typesFromMdr10.add(ti);
                }
                typesFromMdr10.sort((o1, o2) -> Integer.compare(o1.mdr11Rec, o2.mdr11Rec));
        }
        private void check11() {
                setShowLogs(print[11]);
                info("mdr11 check");
                Section section = getSection(11);

                long start = section.getStart();
                long end = section.getEnd();
                int recsize = section.getRecordSize();

                reader.position(start);
                int record = 0;
                while (reader.position() < end) {
                        record++;
                        int next = (int) (reader.position() + recsize);

                        int mapNumber = readMapNumber();
                        int num = read(1);
                        int subdiv = read(2);
                        int lbloff = read(3);
                        boolean ok = true;
                        MapDetails map = details.getMap(mapNumber);
                        if (map != null) {
                                String text = map.getLabelText(lbloff);
                                TypeInfoMdr10 expectedType = null;
                                if (record - 1 < typesFromMdr10.size()) {
                                        expectedType = typesFromMdr10.get(record-1);
                                        assert expectedType.mdr11Rec == record;
                                        MapObject o;
                                        String kind ;
                                        if (expectedType.isPolygon) {
                                                kind = "polygon";
                                                o = map.getShape(subdiv, num);
                                        } else {
                                                kind = "point";
                                                o = map.getPoint(subdiv, num);
                                        }
                                        if (o == null) {
                                                error("%d: map%d; %s; %s not found subdiv=%d, ind=%d", record, mapNumber, kind, text, subdiv, num);
                                        } else {
                                                if (o.getType() != expectedType.type) {
                                                        error("%d: map%d; %s; %s subdiv/num=%d/%d index says t=0x%x , RGN says t=0x%04x", record, mapNumber, kind, text, subdiv, num, expectedType.type,o.getType());
                                                } else {
                                                        info("%d: map%d; t=0x%04x subdiv/num=%d/%d %s %s", record, mapNumber, o.getType(), subdiv, num, kind, text);

                                                        int type = o.getType();
                                                        poiType.put(record, type);
                                                }
                                        }
                                }
                        }
                        reader.position(next);
                }
        }

        /**
         * Regions. We read this mainly to get a mapping of region names to country names
         * to check within other sections.
         */

        private void check13() {
                setShowLogs(print[13]);
                info("mdr13 check");
                Section section = getSection(13);

                long start = section.getStart();
                long end = section.getEnd();

                reader.position(start);
                int record = 0;
                while (reader.position() < end) {
                        record++;

                        int mapNumber = readMapNumber();
                        assert mapNumber > 0 && mapNumber <= numberOfMaps : "Bad map number " + mapNumber;

                        int regNum = read(2);
                        int countryNum = read(2);
                        String strText = readString();

                        MapDetails map = details.getMap(mapNumber);
                        if (map != null) {
                                String regionName = map.getRegionName(regNum);
                                String countryName = map.getCountryName(countryNum);

                                info("%d map%d; region %s country %s", record, mapNumber, regionName, countryName);

                                if (!regionName.equals(strText))
                                        error("%d map%d; region %s, text string %s", record, mapNumber, regionName, strText);

                        }
                }
        }

       
        private void check16(HuffmanDecoder decoder) {
                setShowLogs(print[16]);
                info("mdr16 check");
                Section section = getSection(16);
                if (checkEmpty(section))
                        return;
               
                long start = section.getStart();
                long end = section.getEnd();

                reader.position(start);
                decoder.setHuffmanDecodingFailed(true); // being pessimistic here
                AtomicInteger varLength = new AtomicInteger();
                int remSize = reader.readVarLength(varLength);
                if (section.getLen() - remSize != varLength.get()) {
                        error("bad remaining size %d", remSize);
                        return;
                }

                decoder.setLookupBits(reader.get1u() & 0xf); // spotted values: 0x15 and 0x16
                /** typically a 32x2 byte lookup table */
                int huffmanTab2Size = (1 << decoder.getLookupBits()) * 2;
                int maxDepth = reader.get1u(); // max code length
                if (maxDepth >= 32) {
                        error("maximum depth too high: %d", maxDepth);
                        return; // don't know how to handle this
                       
                }
                int rowsTab1 = reader.get1u(); // rows in search table
                if (rowsTab1 >= 32) {
                        error("number of rows for search table too high: %d", rowsTab1);
                        return; // don't know how to handle this
                       
                }
                int symWidth = reader.get1u();
                if (symWidth != 8) {
                        return; // don't know yet how to handle this
                }
                int symBytes = reader.readVarLength(varLength);
                int offsetSize = varLength.get();
                int headerSize = (int) (reader.position() - start);
                int minCodeBytes = (int) Math.ceil(maxDepth / 8.0);
                int tab1Width = 1 + offsetSize + minCodeBytes;

                int tab1Bytes = section.getLen() - huffmanTab2Size - symBytes - headerSize;
                if (rowsTab1 * tab1Width != tab1Bytes) {
                        error("search table: rows x width doesn't match table expected size");
                        //error("rows=%d, width=%d, secLen=%d, hdrSize=%d, tab2Size=%d symBytes=%d",
                        //        rowsTab1, tab1Width, section.getLen(), headerSize, huffmanTab2Size, symBytes);
                        return; // don't know how to handle this
                }
                for (int i = 0; i< rowsTab1; i++) {
                        // the minCode value is shifted to allow a binary search
                        int minCodeShifted = reader.getNu(minCodeBytes);
                        int depth = reader.get1u();
                        int offset = reader.getNu(offsetSize);
                        decoder.addSearchTab(minCodeShifted, depth, offset);
                }
                decoder.setLookupTable(reader.get(huffmanTab2Size));
               
                decoder.setSymbols(reader.get((int) (end - reader.position())));  
               
                decoder.setHuffmanDecodingFailed(false); // probably
                decoder.buildHuffmanTree();
                if (decoder.isHuffmanDecodingFailed())
                        info("building Huffman tree for double check");
                if (!decoder.isHuffmanDecodingFailed())
                        info("Huffman decoder table and tree OK");
        }


        private void check18() {
                setShowLogs(print[18]);
                info("mdr18 check");
                Section section = getSection(18);

                long start = section.getStart();
                long end = section.getEnd();


                int psize = getSection(19).getBytesForRecords();
                reader.position(start);
                while (reader.position() < end) {

                        char type = (char)reader.get2u();
                        int rec = read(psize);
                        TypeInfo ti = new TypeInfo();
                        ti.type = type;
                        ti.start = rec;
                        typeInfos.add(ti);
                }
        }

        private void check19() {
                setShowLogs(print[19]);
                info("mdr19 check");
                Section section = getSection(19);
                if (section.getNumberOfRecords() == 0)
                        return;

                if (typeInfos.isEmpty()) {
                        error("no type info from mdr 18 available");
                        return;
                }

                long start = section.getStart();
                // The number of bytes required to represent a POI (mdr11) record number and a flag bit.
                int recsize = Section.numberOfBytes(getSection(11).getNumberOfRecords() << 1);
                int flag = 0;
                switch (recsize) {
                case 1:
                        flag = 0x80;
                        break;
                case 2:
                        flag = 0x8000;
                        break;
                case 3:
                        flag = 0x800000;
                        break;
                }

                reader.position(start);
                int record = 0;
                for (int i = 0, typeInfosSize = typeInfos.size(); i < typeInfosSize - 1; i++) {
                        TypeInfo ti = typeInfos.get(i);
                        TypeInfo ti2 = typeInfos.get(i + 1);
                        int expected = ti.type & ~0xe000;
                        expected = ((expected << 3) & ~0xff) | (expected & 0x1f);

                        if (ti2.start == ti.start)
                                error("empty range for type %x", ti.type);
                        for (int j = ti.start; j < ti2.start; j++) {
                                record++;

                                int rec = read(recsize);
                                rec &= ~flag;
                                Integer type = poiType.get(rec);
                                if (type == null) {
                                        error("could not get type for %d", rec);
                                } else {
                                        if (type == expected || (type <= 0x7f && (expected >> 8) == type)) info("%d: poi %d type %x", record, rec, type);
                                        else error("%d: poi %d type was %x, expected %x", record, rec, type, expected);
                                }
                        }
                }
        }

        /**
         * Check that the mdr20 records point to streets in the correct city.
         */

        private void check20() {
                setShowLogs(print[20]);
                info("mdr20 check");

                Section section = getSection(20);

                long start = section.getStart();
                int magic = section.getMagic();

                mdr20ToStreet = new int[section.getNumberOfRecords() + 1];

                int size = Section.numberOfBytes(getSection(7).getNumberOfRecords() << 1);

//              Mdr20Info first = cityMdr20.get(1);
//              checkEqual(1, first.mdr20index, "first mdr20 not 1");

                reader.position(start);
                int record = 1;

                int partialInfoSize = ((magic >> 3) & 0x7);
                int partialBShift = ((magic >> 6) & 0xf);
                int partialBMask = (1 << partialBShift) - 1;
                for (int i      = 1; i < cityMdr20.size() - 1; i++) {
                        Mdr20Info from = cityMdr20.get(i);
                        if (from.mdr20index == 0)
                                continue;

                        Mdr20Info next = null;
                        for (int j = i + 1; next == null || next.mdr20index == 0; j++)
                                next = cityMdr20.get(j);

                        if (record != from.mdr20index) {
                                error("mdr20 record %d out of step with index %d", record, from.mdr20index);
                                record = from.mdr20index;
                        }
                        info("mdr20 %d city=%s r=%s c=%s, from=%d to=%d %s", record, from.cityName,
                                        from.regionName, from.countryName, from.mdr20index, next.mdr20index,
                                        (from.mdr20index == next.mdr20index) ? "empty" : "");

                        Name lastName = null;

                        boolean hasLabel = (magic & 0x2) != 0;  // A guess
                        boolean hasOffset = (magic & 0x4) != 0;  // A guess, but less so
                        boolean hasStreetIndex = (magic & 0x800) != 0; // Guess

                        while (record < next.mdr20index) {
                                assert from.mdr20index != next.mdr20index : "from " + from.mdr20index + " to " + next.mdr20index;

                                Name name = null;
                                List<Street> streetList = null;
                                boolean repeated = false;

                                // As far as I know the hasStreetIndex and hasLabel are mutually exclusive
                                if (hasStreetIndex) {
                                        int val = read(size);
                                        repeated = (val & 1) == 0;
                                        int ind = val >> 1;
                                        mdr20ToStreet[record] = ind;

                                        //info("street index %d", ind);
                                        name = streets.get(ind);
                                        streetList = getStreetsByName(name.mapNumber, name.name);
                                }

                                if (hasLabel) {
                                        int mapNumber = readMapNumber();
                                        int val = read(3);
                                        repeated = (val & 0x800000) == 0;
                                        int nameOffset = 0;
                                        if (hasOffset) {
                                                nameOffset = read(1);
                                        }

                                        MapDetails map = details.getMap(mapNumber);
                                        if (map != null) {
                                                int lblOffset = val & 0x7fffff;
                                                String sname = map.getLabelText(lblOffset);
                                                streetList = map.getStreetsWithName(sname);
                                                name = new Name(mapNumber, sname, nameOffset);
                                        }
                                }

                                Boolean partialRepeat = null;
                                if (partialInfoSize > 0) {
                                        int partialInfo = read(partialInfoSize);
                                        partialRepeat = (partialInfo & 1) == 0;
                                        if (partialBShift > 0) {
                                                name.hasOrders = true;
                                                name.b = (partialInfo >>> 1) & partialBMask;
                                                name.s = (partialInfo >>> partialBShift + 1);
                                        }
                                }

                                // At this point we have a name, nameOffset and a streetList
                                if (streetList != null && !streetList.isEmpty()) {

                                        boolean found = false;
                                        for (Street s : streetList) {
                                                //System.out.println("from city " + from.cityName + "/r=" + from.regionName);
                                                //System.out.println("s         " + s.getCityName() + "/r=" + s.getRegionName());
                                                if (from.cityName.equals(s.getCityName()) && from.regionName.equals(s.getRegionName()) && from.countryName.equals(s.getCountryName())) {
                                                        found = true;
                                                        info("%d map%d st=%s%s, city=%s, r=%s, c=%s; b=%d s=%d",
                                                                        record, s.getMapIndex(),
                                                                        s.getName(), repeatString(repeated, partialRepeat), s.getCityName(),
                                                                        s.getRegionName(), s.getCountryName(),
                                                                        name.b,
                                                                        name.s
                                                        );
                                                        break;
                                                }
                                        }
                                        if (!found) {
                                                error("%d map%d no road name=%s in city %s", record, name.mapNumber, name.name, from.cityName);
                                                for (Street s : streetList) {
                                                        info("  ... map%d %s, in %s r=%s c=%s", s.getMapIndex(), s.getName(), s.getCityName(), s.getRegionName(), s.getCountryName());
                                                }
                                        }

                                        checkNames(lastName, name, repeated, partialRepeat);

                                } else {
                                        // This is an error if we have saved all the maps
                                        if (details.mapSaved(numberOfMaps)) {
                                                error("%d could not find any streets", record);
                                        }
                                }

                                lastName = name;
                                record++;
                        }
                }
        }

        private List<Street> getStreetsByName(int mapNumber, String name) {
                MapDetails map = details.getMap(mapNumber);
                if (map == null)
                        return null;
                return map.getStreetsWithName(name);
        }

        private void checkNames(Name last, Name name, boolean repeatFlat) {
                checkNames(last, name, repeatFlat, null);
        }

        private void checkNames(Name last, Name name, boolean repeatFlag, Boolean partialFlag) {
                if (last == null)
                        return;

                // Checks on the partial name
                String lastPartial = last.partialName();
                String partial = name.partialName();

                int cmp = collator.compare(lastPartial, partial);
                if (partialFlag != null) {
                        if (cmp == 0) {
                                if (!partialFlag)
                                        error("partial repeat flag not set for partial repeated name");
                        } else {
                                if (partialFlag)
                                        error("partial repeat flag set when not repeated {%s}{%s}", last, name);
                        }
                }

                // Last partial name must not be larger than this one
                if (cmp > 0) {
                        error("Unordered partial name /%s/%s/ off=%d/%d",
                                        lastPartial, partial, last.offset, name.offset);
                } else if (cmp == 0) {
                        // If the partial name is equal, check the beginning part of the name
                        cmp = collator.compare(last.name, name.name);
                        if (cmp > 0) {
                                error("Unordered partial name including initial part {%s}{%s}",
                                                last, name);
                        }
                }

                cmp = collator.compare(last.name, name.name);
                if (cmp == 0 && (partialFlag == null || partialFlag)) {
                        // A full repeat is not flagged, unless it is also a partial repeat.
                        if (!repeatFlag)
                                error("repeat flag not set for repeated name {%s}{%s}",
                                                last, name);
                } else {
                        if (repeatFlag)
                                error("repeat flag set when not repeated");
                }

                if (repeatFlag && Boolean.FALSE.equals(partialFlag)) {
                        error("Flagged as full repeat but not partial repeat");
                }

                // If there is no partial flag given, or no orderings available then we are done.
                if (partialFlag == null || !name.hasOrders)
                        return;

                // Compare the initial part of the name
                cmp = last.compareInitial(name);
                if (repeatFlag) {
                        if (last.b != name.b)
                                error("B is different for repeated name %d/%d", last.b, name.b);

                        if (last.s != name.s)
                                error("S is different for repeated name %d/%d", last.s, name.s);
                } else if (partialFlag) {
                        if (name.b == 0 && name.startOffset() > 0 && name.s == 0)
                                if (cmp < 0)
                                        error("B is zero for partial repeat");
                }

                // If the repeat flag is set, then we are done
                if (repeatFlag)
                        return;

                if (name.startOffset() > 0 && partialFlag) {
                        // Check b
                        if (cmp < 0) {
                                //info("B normal initial order");
                                if (last.b >= name.b)
                                        error("B does not order the initial part /%s/%s/", last.initialPart(), name.initialPart());
                        } else if (cmp > 0){
                                //info("B reversed initial order");
                                if (name.b >= last.b)
                                        error("B does not order the initial part (reversed) /%s/%s/", last.initialPart(), name.initialPart());
                        } else {
                                if (name.s == 0)
                                        error("S is unexpectedly zero");
                        }
                }

                // Compare suffix part
                if (name.suffix > 0 && last.suffix > 0 && partialFlag) {
                        cmp = last.compareSuffix(name);
                        if (cmp < 0) {
                                //info("S normal initial order");
                                if (last.s >= name.s)
                                        error("S does not order the suffix /%s/%s/", last.suffixPart(), name.suffixPart());
                        } else if (cmp > 0) {
                                //info("S reversed initial order");
                                if (name.s >= last.s)
                                        error("S does not order the suffix (reversed) /%s/%s/", last.suffixPart(), name.suffixPart());
                        }
                }
        }

        private void check21() {
                setShowLogs(print[21]);
                info("mdr21 check");

                Section section = getSection(21);

                long start = section.getStart();
                long end = section.getEnd();
                if (checkEmpty(section))
                        return;

                int size = Section.numberOfBytes(getSection(7).getNumberOfRecords() << 1);
                if (section.getRecordSize() != size) {
                        error("mdr21: record size %d, byte required for mdr7 is %d", section.getRecordSize(), size);
                        error("  ... don't know how to deal with this version of the section");
                        return;
                }

                reader.position(start);
                Name lastName = null;
                for (int i = 1; i < regionNames.size() - 1; i++) {
                        Mdr28Record from = regionNames.get(i);
                        if (from.getMdr21() == 0)
                                continue;

                        Mdr28Record next = null;
                        for (int j = i + 1; next == null || next.getMdr21() == 0; j++)
                                next = regionNames.get(j);


                        String expectedName = from.getName();
                        info("mdr21 from %d to %d; expect region %s", from.getMdr21(), next.getMdr21(),
                                        expectedName);
                        lastName = null;

                        for (int record = from.getMdr21(); record < next.getMdr21() && reader.position() < end; record++) {
                                int val = read(size);

                                int idx = val >> 1;

                                Name name = streets.get(idx);
                                List<Street> streetList = getStreetsByName(name.mapNumber, name.name);
                                if (streetList != null && !streetList.isEmpty()) {
                                        boolean repeated = (val & 1) == 0;

                                        checkNames(lastName, name, repeated);
                                        lastName = name;

                                        boolean found = false;
                                        for (Street s1 : streetList) {
                                                if (expectedName.equals(s1.getRegionName())) {
                                                        found = true;
                                                        info("%d map%d st=%d, %s%s, r=%s, c=%s",
                                                                        record, s1.getMapIndex(), idx,
                                                                        s1.getName(), repeatString(repeated), s1.getRegionName(),
                                                                        s1.getCountryName());
                                                }
                                        }
                                        if (!found) {
                                                error("%d no road %d name=%s in region %s", record, idx, streetList.get(0).getName(), expectedName);
                                                for (Street s1 : streetList) {
                                                        info("  ... %s, in %s", s1.getName(), s1.getRegionName());
                                                }
                                        }
                                } else {
                                        // This is an error if we have saved all the maps
                                        if (details.mapSaved(numberOfMaps)) {
                                                error("%d; could not find any streets", idx);
                                        }
                                }
                        }
                }
        }

        private void check22() {
                setShowLogs(print[22]);
                info("mdr22 check");

                Section section = getSection(22);

                long start = section.getStart();
                int magic = section.getMagic();

                setShowLogs(print[22]);
                info("mdr22");

                int recordSize = Section.numberOfBytes(getSection(7).getNumberOfRecords() << 1);

                reader.position(start);
                int record = 1;
                boolean hasLabel = (magic & 0x2) != 0;  // A guess
                boolean hasOffset = (magic & 0x4) != 0;  // A guess, but less so
                int partialInfoSize = ((magic >> 3) & 0x7);
                int partialBShift = ((magic >> 6) & 0xf);
                int partialBMask = (1 << partialBShift) - 1;

                for (int i = 1; i < countryNames.size() - 1; i++) {
                        Name lastName = null;
                        Mdr29Record from = countryNames.get(i);
                        if (from.getMdr22() == 0)
                                continue;

                        Mdr29Record next = null;
                        for (int j = i + 1; next == null || next.getMdr22() == 0; j++)
                                next = countryNames.get(j);

                        if (record != from.getMdr22()) {
                                error("mdr22 record %d out of step with index %d", record, from.getMdr22());
                                record = from.getMdr22();
                        }

                        String expectedName = from.getName();
                        info("mdr22 from %d to %d; expect country %s", from.getMdr22(), next.getMdr22(), expectedName);

                        for (; record < next.getMdr22(); record++) {
                                assert from.getMdr22() != next.getMdr22() : "from " + from.getMdr22() + " to " + next.getMdr22();

                                Name name = null;

                                // 1 map number, 2 lbl offset, 3 a flag
                                int val;
                                List<Street> streetList = null;
                                boolean repeated;
                                if (hasLabel) {
                                        int mapNumber = readMapNumber();
                                        val = read(3);
                                        repeated = (val & 0x800000) == 0;

                                        int nameOffset = 0;
                                        if (hasOffset)
                                                nameOffset = read(1);

                                        MapDetails map = details.getMap(mapNumber);
                                        if (map != null) {
                                                int lblOffset = val & 0x7fffff;
                                                name = new Name(mapNumber, map.getLabelText(lblOffset), nameOffset);
                                                streetList = map.getStreetsWithName(name.getName());
                                        }
                                } else {
                                        // Just has a LBL pointer
                                        val = read(recordSize);

                                        int idx = val >> 1;
                                        repeated = (val & 1) == 0;

                                        name = streets.get(idx);
                                        streetList = getStreetsByName(name.mapNumber, name.name);
                                }

                                Boolean partialRepeat = null;
                                int partialInfo = 0;
                                if (partialInfoSize > 0) {
                                        partialInfo = read(partialInfoSize);
                                        partialRepeat = (partialInfo & 1) == 0;
                                }

                                if (name != null) {

                                        if (partialBShift > 0) {
                                                name.hasOrders = true;
                                                name.b = (partialInfo >>> 1) & partialBMask;
                                                name.s = (partialInfo >>> partialBShift + 1);
                                        }

                                        if (streetList != null && !streetList.isEmpty()) {

                                                boolean found = false;
                                                for (Street s1 : streetList) {
                                                        if (expectedName.equals(s1.getCountryName())) {
                                                                found = true;
                                                                info("%d map%d %s%s, c=%s b=%d s=%d",
                                                                                record, s1.getMapIndex(),
                                                                                s1.getName(), repeatString(repeated, partialRepeat),
                                                                                s1.getCountryName(),
                                                                                name.b,
                                                                                name.s
                                                                );
                                                                break;
                                                        }
                                                }
                                                if (!found) {
                                                        error("%d no road name=%s in country %s", record, streetList.get(0).getName(),
                                                                        expectedName);
                                                        for (Street s1 : streetList) {
                                                                info("  ... %s, in %s", s1.getName(), s1.getCountryName());
                                                        }
                                                }
                                        } else {
                                                // This is an error if we have saved all the maps
                                                if (details.mapSaved(numberOfMaps)) {
                                                        error("%d; could not find any streets", record);
                                                }
                                        }

                                        checkNames(lastName, name, repeated, partialRepeat);
                                } else {
                                        error("%d No name", record);
                                }

                                lastName = name;
                        }
                }
        }

        private void check23() {
                setShowLogs(print[23]);
                info("mdr23 check");

                Section section = getSection(23);

                long start = section.getStart();
                long end = section.getEnd();

                reader.position(start);
                int regionnum = 0;
                Name lastName = null;
                while (reader.position() < end) {
                        regionnum++;

                        int mapNumber = readMapNumber();

                        int region = read(2);
                        int country = read(2);
                        int lbl = read(3);

                        MapDetails map = details.getMap(mapNumber);
                        if (map != null) {
                                int lblOffset = lbl & 0x7fffff;
                                Name name = new Name(mapNumber, map.getLabelText(lblOffset));
                                info("%d map%d; region %s, r %d, c %d", regionnum, mapNumber, name, region, country);

                                if (name.equals(lastName)) {
                                        error("%d map%d; region name repeated in same map %s",
                                                        regionnum, mapNumber, name);
                                }

                                boolean repeated = (lbl & 0x800000) == 0;
                                checkNames(lastName, name, repeated);
                                lastName = name;
                        }
                }
        }

        private void check24() {
                setShowLogs(print[24]);
                info("mdr24 check (countries)");

                Section section = getSection(24);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();

                reader.position(start);
                Name lastName = null;
                for (int i = 1; i < countryNames.size() - 1; i++) {
                        Mdr29Record from = countryNames.get(i);
                        Mdr29Record next = countryNames.get(i + 1);
                        info("mdr24 from %d to %d", from.getMdr24(), next.getMdr24());

                        for (int countrynum = from.getMdr24(); countrynum < next.getMdr24() && reader.position() < end; countrynum++) {
                                String expectedCountry = from.getName();

                                int mapNumber = readMapNumber();

                                int country = read(2);
                                int lbl = read(3);

                                MapDetails map = details.getMap(mapNumber);
                                if (map != null) {
                                        int lblOffset = lbl & 0x7fffff;
                                        String text = map.getLabelText(lblOffset);
                                        Name name = new Name(mapNumber, text);
                                        if (expectedCountry.isEmpty()) {
                                                expectedCountry = text;
                                                from.setName(text);
                                        }

                                        info("%d map%d; country %s, %d", countrynum, mapNumber, name, country);
                                        checkEqual(expectedCountry, name.getName(), "%d map%d; country name", countrynum, mapNumber);

                                        if (name.equals(lastName)) {
                                                error("%d map%d; name repeated in same map %s", countrynum, mapNumber, name);
                                        }

                                        boolean repeated = (lbl & 0x800000) == 0;
                                        checkNames(lastName, name, repeated);

                                        lastName = name;
                                }
                        }
                }
        }

        /**
         * Cities by country.
         */

        private void check25() {
                setShowLogs(print[25]);
                info("mdr25 check");

                Section section = getSection(25);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();

                int size = Section.numberOfBytes(getSection(5).getNumberOfRecords());
                if (section.getRecordSize() != size) {
                        error("mdr25 record size %d, bytes required for mdr5 is %d", section.getRecordSize(), size);
                        error("  ... don't know how to deal with this version of the section");
                        return;
                }

                reader.position(start);

                int lastMapNumber = 0;
                Mdr20Info lastInfo = null;
                int record = 0;
                while (reader.position() < end) {
                        record++;

                        int val = read(size);
                        Mdr20Info info = cityMdr20.get(val);

                        String countryName = info.countryName;
                        String cityName = info.cityName;
                        info("%d country %s; map%d city %d %s", record, countryName, info.mapNumber, val,
                                        cityName);

                        if (info.mapNumber == lastMapNumber && isSameByName(info, lastInfo))
                                error("%d country %s; repeated city name %s in same map", record, countryName, cityName);

                        lastInfo = info;
                        lastMapNumber = info.mapNumber;
                }
        }

        private static boolean isSameByName(Mdr20Info first, Mdr20Info other) {
                return other != null && first.cityName.equals(other.cityName) && first.regionName
                                .equals(other.regionName) && first.countryName.equals(other.countryName);
        }

        /**
         * Mdr 28 ordered by country.
         */

        private void check26() {
                setShowLogs(print[26]);
                info("mdr26 check");

                Section section = getSection(26);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();

                int size = Section.numberOfBytes(getSection(28).getNumberOfRecords());
                if (section.getRecordSize() != size) {
                        error("mdr26 record size %d, bytes required for mdr25 is %d", section.getRecordSize(), size);
                        error("  ... don't know how to deal with this version of the section");
                        return;
                }

                reader.position(start);
                for (int i = 1; i < countryNames.size() - 1; i++) {
                        Mdr29Record from = countryNames.get(i);
                        Mdr29Record next = countryNames.get(i + 1);
                        if (from.getMdr26() == 0)
                                continue;

                        info("mdr26 from %d to %d; country %s", from.getMdr26(), next.getMdr26(),
                                        from.getName());

                        for (int record = from.getMdr26(); record < next.getMdr26(); record++) {
                                if (reader.position() >= end) {
                                        error("mdr26 from %d; read beyond end of section", from.getMdr26());
                                        break;
                                }
                                int idx = read(size);
                                checkNotZero(idx, "%d: pointer zero", record);

                                Mdr28Record region = regionNames.get(idx);
                                if (region != null) {
                                        info("%d region %s; country %s", record, region.getName(),
                                                        region.getMdr14().getName());
                                }
                        }
                }
        }

        /**
         * Cities sorted by region.
         */

        private void check27() {
                setShowLogs(print[27]);
                info("mdr27 check");

                Section section = getSection(27);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();

                int size = Section.numberOfBytes(getSection(5).getNumberOfRecords());
                if (section.getRecordSize() != size) {
                        error("mdr27 record size %d, bytes required for mdr5 is %d", section.getRecordSize(), size);
                        error("  ... don't know how to deal with this version of the section");
                        return;
                }

                reader.position(start);
                boolean mdr28HasName = (sections.get(28).getMagic() & 0x1) != 0;
                int lastMapNumber = 0;
                String lastCityName = "";
                for (int i = 1; i < regionNames.size() - 1; i++) {
                        Mdr28Record from = regionNames.get(i);
                        if (from.getMdr27() == 0)
                                continue;

                        Mdr28Record next = regionNames.get(i + 1);

                        for (int j = i + 2; next.getMdr27() == 0; j++)
                                next = regionNames.get(j);

                        assert next.getMdr27() != 0;
                        //checkNotZero(next.getMdr27(), "mdr27 %d; zero mdr27 pointer", from.getIndex());

                        String expectedName = from.getName();
                        if (mdr28HasName)
                                info("mdr27 from %d to %d; expect region %s", from.getMdr27(), next.getMdr27(), expectedName);
                        else
                                info("mdr27 from %d to %d", from.getMdr27(), next.getMdr27());
                        for (int regionnum = from.getMdr27(); regionnum < next.getMdr27() && reader.position() < end; regionnum++) {

                                int val = read(size);
                                Mdr20Info info = cityMdr20.get(val);

                                String regionName = info.regionName;
                                if (expectedName != null || mdr28HasName)
                                        checkEqual(expectedName, regionName, "%d mdr27 region name", regionnum);

                                String cityName = info.cityName;
                                if (info.mapNumber == lastMapNumber && cityName.equals(lastCityName))
                                        error("%d region %s; repeated city name %s in same map", regionnum, regionName, cityName);

                                info("%d region %s; map%d city %d %s", regionnum, regionName, info.mapNumber, val,
                                                cityName);

                                lastCityName = cityName;
                                lastMapNumber = info.mapNumber;
                        }
                }
        }

        private void check28() {
                setShowLogs(print[28]);
                info("mdr28 check");

                Section section = getSection(28);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();
                int magic = section.getMagic();
               
                int size21 = getSection(21).getBytesForRecords();
                int size23 = getSection(23).getBytesForRecords();
                int size27 = getSection(27).getBytesForRecords();
                int strSize = 0;
                if ((magic & 0x1) != 0)
                        strSize = (getSection(15).getLen() > 0xffffff) ? 4 : 3;
                int unkSize = section.getRecordSize() - (size21 + size23 + size27 + strSize);
                if (unkSize != 0) {
                        error("section size has unexpected value, don't know how to interpret mdr28");
                }

                regionNames.add(null);

                reader.position(start);
                int number = 0;

                BoundChecker bound23 = new BoundChecker("mdr23", getSection(23).getNumberOfRecords());
                BoundChecker bound21 = new BoundChecker("mdr21", getSection(21).getNumberOfRecords());
                BoundChecker bound27 = new BoundChecker("mdr27", getSection(27).getNumberOfRecords());
                while (reader.position() < end) {
                        number++;
                        int m23 = read(size23);
                        bound23.update(number, m23);

                       
                        String name = null;
                        if (strSize > 0)
                                name = readString();

                        int m21 = read(size21);
                        bound21.update(number, m21);

                        int m27 = read(size27);
                        bound27.update(number, m27);

                        info("%d m21 %d, m23 %d, m27 %d, %s", number, m21, m23, m27, name);

                        Mdr28Record reg = new Mdr28Record();
                        reg.setName(name);
                        reg.setMdr21(m21);
                        reg.setMdr23(m23);
                        reg.setMdr27(m27);
                        Mdr14Record country = new Mdr14Record();
                        reg.setMdr14(country);
                        regionNames.add(reg);
                        if (unkSize > 0)
                                read(unkSize);
                }

                Mdr28Record last = new Mdr28Record();
                last.setMdr21(getSection(21).getNumberOfRecords() + 1);
                last.setMdr23(getSection(23).getNumberOfRecords() + 1);
                last.setMdr27(getSection(27).getNumberOfRecords() + 1);
                regionNames.add(last);
        }

        private void check29() {
                setShowLogs(print[29]);
                info("mdr29 check");

                countryNames.add(null);

                Section section = getSection(29);
                if (checkEmpty(section))
                        return;

                long start = section.getStart();
                long end = section.getEnd();
                int magic = section.getMagic();

                boolean hasStr = (magic & 0x1) != 0;
                boolean has17 = (magic & 0x30) != 0;
                boolean has26 = (magic & 0x8) != 0;
                int size24 = getSection(24).getBytesForRecords();
                int size22 = getSection(22).getBytesForRecords();
                int size25 = getSection(25).getBytesForRecords();
                int size17 = (has17) ? ((magic & 0x30) >> 4) : 0;
                int size26 = (has26) ? getSection(26).getBytesForRecords() : 0;

                reader.position(start);
                int number = 0;

                BoundChecker bound24 = new BoundChecker("mdr24", getSection(24).getNumberOfRecords());
                BoundChecker bound22 = new BoundChecker("mdr22", getSection(22).getNumberOfRecords());
                BoundChecker bound25 = new BoundChecker("mdr25", getSection(25).getNumberOfRecords());
                BoundChecker bound26 = has26 ? new BoundChecker("mdr26", getSection(26).getNumberOfRecords()) : null;
                BoundChecker bound17 = has17 ? new BoundChecker("mdr17", getSection(17).getNumberOfRecords()) : null;
                while (reader.position() < end) {
                        number++;
                        int m24 = read(size24);
                        bound24.update(number, m24);

                        String name = "";
                        if (hasStr)
                                name = readString();

                        int m22 = read(size22);
                        bound22.update(number, m22);

                        int m25 = read(size25);
                        bound25.update(number, m25);

                        int m17_26 = 0; // Assume is either a 17 or 26 not both
                        if (has26) {
                                m17_26 = read(size26);
                                bound26.update(number, m17_26);
                        }
                        if (has17) {
                                m17_26 = read(size17);
                                bound17.update(number, m17_26);
                        }

                        info("%d m22 %d, m24 %d, m25 %d, m%s %d, %s", number, m22, m24, m25,
                                        has17 ? "17" : "26", m17_26, name);

                        Mdr29Record c = new Mdr29Record();
                        c.setName(name);
                        c.setMdr22(m22);
                        c.setMdr24(m24);
                        c.setMdr25(m25);
                        c.setMdr26(m17_26);

                        countryNames.add(c);
                }

                Mdr29Record last = new Mdr29Record();
                last.setMdr22(getSection(22).getNumberOfRecords() + 1);
                last.setMdr24(getSection(24).getNumberOfRecords() + 1);
                last.setMdr25(getSection(25).getNumberOfRecords() + 1);
                last.setMdr26(getSection(has17 ? 17 : 26).getNumberOfRecords() + 1);
                countryNames.add(last);
        }

        /**
         * Read in a map number, the number of bytes depends on the number of maps.
         *
         * @return A map number. If the number read is out of range then an exception is
         * thrown.
         */

        private int readMapNumber() {
                int mn;
                if (numberOfMaps > 255)
                        mn = reader.get2u();
                else
                        mn = reader.get1u();
                if (mn > numberOfMaps) {
                        assert false : "not a map number " + mn;
                }
                return mn;
        }

        private String readString() {
                int off;
                if (getSection(15).getLen() > 0xffffff)
                        off = reader.get4();
                else
                        off = reader.get3u();
                String s = mdrStrings.getTextString(off);
                if (off != 0 && s == null)
                        throw new IllegalArgumentException("did not find String for offset" + off);
                return s;
        }

        private int read(int n) {
                switch (n) {
                case 1:
                        return reader.get1u();
                case 2:
                        return reader.get2u();
                case 3:
                        return reader.get3u();
                case 4:
                        return reader.get4();
                }
                throw new IllegalArgumentException("integer size must be 1 to 4");
        }

        /**
         * Read mdr1 to obtain all the maps.
         */

        private void readMaps() {
                Section section = getSection(1);

                long start = section.getStart();
                long end = section.getEnd();
                int magic = section.getMagic();

                reader.position(start);
                int mapIndex = 0;
                while (reader.position() < end) {
                        mapIndex++;
                        int mapid = reader.get4();
                        int offset = 0;
                        if ((magic & 0x1) != 0) {
                                offset = reader.get4();
                        }

                        // Read in the actual map tile and save enough information to help us
                        // cross check the mdr file.
                        details.readMap(mapIndex, mapid, offset);
                }
        }

        /**
         * Read in the complete header.
         */

        private void readHeader() {
                readHeaderLen();

                codepage = reader.get2u();

                charSet = Sort.charsetFromCodepage(codepage);
                // %%% TODO: might need to get the decoder if strings can be in Format6
                Sort sort = SrtTextReader.sortForCodepage(codepage);
                isMulti = sort.isMulti();
                collator = sort.getCollator();
                collator.setStrength(Collator.TERTIARY);

                reader.get2u(); // id1
                reader.get2u(); // id2
                reader.get2u(); // unknown

                Section sect = addSection(1, true, true);
                numberOfMaps = sect.getNumberOfRecords();

                addSection(2, true, true);
                addSection(3, true, true);
                addSection(4, true, true);
                addSection(5, true, true);
                addSection(6, true, true);
                addSection(7, true, true);
                addSection(8, true, true);
                addSection(9, true, true);
                addSection(10, false, true);
                addSection(11, true, true);
                addSection(12, true, true);
                addSection(13, true, true);
                addSection(14, true, true);
                addSection(15, false, false);
                getSection(15).setMagic(reader.get());
                addSection(16, true, true);
                addSection(17, false, true);
                addSection(18, true, true);
                addSection(19, true, true);
                if (getHeaderLen() <= 286) // 0x011e
                        return;
                addSection(20, true, true);
                addSection(21, true, true);
                addSection(22, true, true);
                addSection(23, true, true);
                addSection(24, true, true);
                addSection(25, true, true);
                addSection(26, true, true);
                addSection(27, true, true);
                addSection(28, true, true);
                addSection(29, true, true);
                if (getHeaderLen() <= 426) // 0x01aa
                        return;
                addSection(30, true, true);
                addSection(31, false, false);
                addSection(32, true, true);
                addSection(33, false, false);
                addSection(34, true, true);
                addSection(35, true, true);
                addSection(36, true, true);
                addSection(37, true, true);
                addSection(38, true, true);
                addSection(39, true, true);
                addSection(40, true, true);
                //if (getHeaderLen() <= 568) // 0x0238
                //      return;
                // looks like three sections, two with 4 ints, one with 5 ints
                //if (getHeaderLen() <= 620) // 0x26c
                //      return;
                // mostly zeros
                //if (getHeaderLen() <= 668) // 0x029c
                //      return;
        // possibly some sections, but mostly zeros so difficult to tell
                //if (getHeaderLen() <= 700) // 0x02bc
                //      return;
                // at 0x02dc, maybe 5 sections without magic or recsize
                // haven't seen anything after 772 0x0304
        }

        /**
         * Read the header for a section.
         *
         * @param number     The section number.
         * @param hasRecSize True if the section to be read has a record size field of two bytes. It
         *                   will be read and saved.
         * @param hasMagic   True if the section has a flags field of 4 bytes. It will be read and saved.
         * @return
         */

        private Section addSection(int number, boolean hasRecSize, boolean hasMagic) {
                assert number != 0;

                int start = reader.get4();
                int len = reader.get4();
                Section section = new Section("", start, len);

                if (hasRecSize) {
                        int recordSize = reader.get2u();
                        section.setRecordSize(recordSize);
                }

                if (hasMagic) {
                        int magic = reader.get4();
                        section.setMagic(magic);
                }

                while (sections.size() < number - 1)
                        sections.add(null);
                sections.add(number - 1, section);
                return section;
        }

        private static Map<Integer, Integer> makeNumberMapping(String name) {
                Map<Integer, Integer> numberMap = new HashMap<>();
                if (name.toLowerCase().endsWith("_mdr.img")) {
                        String base = name.substring(0, name.length() - 8);

                        String[] look = {
                                        base + ".MDX",
                                        base + ".mdx",
                                        base.toLowerCase() + ".MDX",
                                        base.toLowerCase() + ".mdx",
                        };
                        for (String fname : look) {
                                RandomAccessFile raf = null;
                                try {
                                        raf = new RandomAccessFile(fname, "r");
                                        ImgChannel chan = new FileImgChannel(raf.getChannel());
                                        MdxFileReader mdx = new MdxFileReader(chan);
                                        List<MapInfo> maps = mdx.getMaps();
                                        for (MapInfo bl : maps) {
                                                int mapNumber = bl.getMapname();
                                                int hexNumber = bl.getHexMapname();
                                                numberMap.put(hexNumber, mapNumber);
                                        }
                                        break;
                                } catch (IOException e) {
                                        // can't find a usable mdx file, try the next
                                } finally {
                                        Utils.closeFile(raf);
                                }
                        }
                } else if (name.toLowerCase().endsWith(".img")) {
                        FileSystem fs = null;
                        try {
                                fs = ImgFS.openFs(name);
                                List<DirectoryEntry> list = fs.list();
                                for (DirectoryEntry ent : list) {
                                        if (ent.getExt().equalsIgnoreCase("mps")) {
                                                String fullName = ent.getFullName();
                                                ImgChannel r = fs.open(fullName, "r");
                                                MpsFileReader mps = new MpsFileReader(r, 1252);
                                                List<MapBlock> maps = mps.getMaps();
                                                for (MapBlock bl : maps) {
                                                        int mapNumber = bl.getMapNumber();
                                                        int hexNumber = bl.getHexNumber();
                                                        numberMap.put(hexNumber, mapNumber);
                                                }
                                                System.out.println("# Using mps file");
                                                break;
                                        }
                                }
                        } catch (FileNotFoundException e) {
                                // There isn't a usable mps file.
                        } finally {
                                Utils.closeFile(fs);
                        }
                }
                return numberMap;
        }

        private void setNumberMap(Map<Integer, Integer> numberMap) {
                this.details.setNumberMap(numberMap);
        }

        public void extraArgs(Map<String, String> args) {
                String val = args.remove("errors");
                if (val != null) {
                        // show errors only
                        setShowLogs(false);
                        Arrays.fill(print, false);
                }

                val = args.remove("print");
                if (val != null) {
                        // only print logs for the given sections
                        String[] strings = val.split(",");
                        Arrays.fill(print, false);
                        for (String sect : strings)
                                print[Integer.parseInt(sect)] = true;
                }

                Map<Integer, Integer> numberMap = makeNumberMapping(getName());
                setNumberMap(numberMap);

                // Anything else is common or an error
                System.out.println("remaining " + args);
                super.extraArgs(args);
        }

        public static void main(String[] args) {
                runMain(MdrCheck.class, "MDR", args);
        }

        private class Name {
                private final int mapNumber;
                private final String name;
                private int offset;
                private int prefix;
                private int suffix;
                private int b;
                private int s;
                private boolean hasOrders;

                Name(int mapNumber, String name) {
                        this.mapNumber = mapNumber;
                        this.name = name;
                }

                Name(int mapNumber, String name, int offset) {
                        this.mapNumber = mapNumber;
                        this.name = name;
                        this.offset = offset;
                       
                        // the given offset refers to the utf8 encoding, convert it to the position in the java string
                        int togo = offset;
                        int nameOffset = 0;
                        while (togo > 0) {
                                int c = name.codePointAt(prefix + nameOffset);
                                togo -= outSize(c);
                                nameOffset++;
                        }
                        this.offset = nameOffset;
                        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;

                        sep = name.indexOf(0x1f);
                        if (sep < 0)
                                sep = name.indexOf(0x1c);
                        if (sep > 0)
                                suffix = sep;
                }

                public String getName() {
                        return name;
                }

                String partialName() {
                        if (suffix > 0)
                                return name.substring(prefix + offset, suffix);
                        else
                                return name.substring(prefix + offset);
                }

                private String initialPart() {
                        int off = prefix + offset;
                        if (off == 0)
                                return "";

                        return name.substring(0, off);
                }

                private String suffixPart() {
                        if (suffix == 0)
                                return "";
                        return name.substring(suffix, name.length());
                }

                int compareInitial(Name other) {
                        String n1 = this.initialPart();
                        String n2 = other.initialPart();

                        return collator.compare(n1, n2);
                }

                public boolean equals(Object o) {
                        if (this == o) return true;
                        if (o == null || getClass() != o.getClass()) return false;

                        Name name1 = (Name) o;

                        if (mapNumber != name1.mapNumber) return false;
                        if (offset != name1.offset) return false;
                        if (!name.equals(name1.name)) return false;

                        return true;
                }

                public int getB() {
                        return b;
                }

                public void setB(int b) {
                        this.b = b;
                }

                public int getS() {
                        return s;
                }

                public void setS(int s) {
                        this.s = s;
                }

                public int hashCode() {
                        int result = mapNumber;
                        result = 31 * result + name.hashCode();
                        result = 31 * result + offset;
                        return result;
                }

                public String toString() {
                        StringBuilder sb = new StringBuilder(name);
                        if (offset != 0)
                                sb.insert(prefix + offset, '|');
                        return sb.toString();
                }

                int startOffset() {
                        return prefix + offset;
                }

                int compareSuffix(Name other) {
                        return collator.compare(this.suffixPart(), other.suffixPart());
                }
        }
       
        /**
         * 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 IllegalArgumentException(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;
                }
        }

        private static class Mdr20Info {
                private int mdr20index;
                private String cityName;
                private int mapNumber;
                private String regionName;
                private String countryName;

                private Mdr20Info(int mdr20index) {
                        this.mdr20index = mdr20index;
                }

                boolean sameCity(Mdr20Info prev) {
                        return prev != null
                                        && cityName.equals(prev.cityName)
                                        && regionName.equals(prev.regionName)
                                        && countryName.equals(prev.countryName);
                }
        }

        private static class TypeInfo {
                int type;
                int start;
        }

        private static class TypeInfoMdr10 {
                int type;
                int mdr11Rec;
                boolean isPolygon;
        }
       
        private static int toType(int group, int typeOrSubType) {
                // see mkgmap imgfmt/app/mdr/MdrUtils.java:getGroupForPoi()
                // and mkgmap imgfmt/app/mdr/Mdr10.java:addPoiType()
                if (group == 1)
                        return typeOrSubType << 8;
                if (group >= 64)
                        return typeOrSubType;
                int t = -1;
                if (group >= 2 && group <= 8)
                        t = (group + 0x28) << 8;
                else if (group == 9)
                        return 0x28 << 8;
                else if (group >= 11 && group <= 13)
                        t = (group + 0x59) << 8;
                if (typeOrSubType != 0)
                        t |= typeOrSubType;
                return t;
        }

        private boolean checkEmpty(Section section) {
                if (section.getLen() == 0) {
                        info("section is empty");
                }
                return section.getLen() == 0;
        }
       
}