/* * Copyright (C) 2007 Steve Ratcliffe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * * Author: Steve Ratcliffe * Create date: Jan 5, 2008 */ package uk.me.parabola.imgfmt.app.net; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.me.parabola.imgfmt.Utils; import uk.me.parabola.imgfmt.app.BufferedImgFileWriter; import uk.me.parabola.imgfmt.app.ImgFile; import uk.me.parabola.imgfmt.app.ImgFileWriter; import uk.me.parabola.imgfmt.app.Label; import uk.me.parabola.imgfmt.app.lbl.City; import uk.me.parabola.imgfmt.app.srt.IntegerSortKey; import uk.me.parabola.imgfmt.app.srt.MultiSortKey; import uk.me.parabola.imgfmt.app.srt.Sort; import uk.me.parabola.imgfmt.app.srt.SortKey; import uk.me.parabola.imgfmt.fs.ImgChannel; /** * The NET file. This consists of information about roads. It is not clear * what this file brings on its own (without NOD) but may allow some better * searching, street addresses etc. * * @author Steve Ratcliffe */ public class NETFile extends ImgFile { private final NETHeader netHeader = new NETHeader(); private List<RoadDef> roads; private Sort sort; public NETFile(ImgChannel chan) { setHeader(netHeader); setWriter(new BufferedImgFileWriter(chan)); position(NETHeader.HEADER_LEN); } /** * Write out NET1. * @param numCities The number of cities in the map. Needed for the size of the written fields. * @param numZips The number of zips in the map. Needed for the size of the written fields. */ public void write(int numCities, int numZips) { // Write out the actual file body. ImgFileWriter writer = netHeader.makeRoadWriter(getWriter()); try { for (RoadDef rd : roads) rd.writeNet1(writer, numCities, numZips); } finally { Utils.closeFile(writer); } } /** * Final writing out of net sections. * * We patch the NET offsets into the RGN file and create the sorted roads section. * * @param rgn The region file, this has to be patched with the calculated net offsets. */ public void writePost(ImgFileWriter rgn) { for (RoadDef rd : roads) rd.writeRgnOffsets(rgn); ImgFileWriter writer = netHeader.makeSortedRoadWriter(getWriter()); try { List<LabeledRoadDef> labeledRoadDefs = sortRoads(); for (LabeledRoadDef labeledRoadDef : labeledRoadDefs) labeledRoadDef.roadDef.putSortedRoadEntry(writer, labeledRoadDef.label); } finally { Utils.closeFile(writer); } getHeader().writeHeader(getWriter()); } /** * Sort the roads by name and remove duplicates. * * We want a list of roads such that every entry in the list is a different road. Since in osm * roads are frequently chopped into small pieces we have to remove the duplicates. * This doesn't have to be perfect, it needs to be useful when searching for roads. * * So we have a separate entry if the road is in a different city. This would probably be enough * except that associating streets with cities is not always very good in OSM. So I also create an * extra entry for each subdivision. Finally there a search for disconnected roads within the subdivision * with the same name. * * Performance note: The previous implementation was very, very slow when there were a large number * of roads with the same name. Although this was an unusual situation, when it happened it appears * that mkgmap has hung. This implementation takes a fraction of a second even for large numbers of * same named roads. * * @return A sorted list of road labels that identify all the different roads. */ private List<LabeledRoadDef> sortRoads() { List<SortKey<LabeledRoadDef>> sortKeys = new ArrayList<>(roads.size()); Map<Label, byte[]> cache = new HashMap<>(); for (RoadDef rd : roads) { Label[] labels = rd.getLabels(); for (int i = 0; i < labels.length && labels[i] != null; ++i) { Label label = labels[i]; if (label.getLength() == 0) continue; // Sort by name, city, region/country and subdivision number. LabeledRoadDef lrd = new LabeledRoadDef(label, rd); SortKey<LabeledRoadDef> nameKey = sort.createSortKey(lrd, label, 0, cache); // If there is a city add it to the sort. City city = (rd.getCities().isEmpty() ? null : rd.getCities().get(0)); // what if we more than one? SortKey<LabeledRoadDef> cityKey; if (city != null) { int region = city.getRegionNumber(); int country = city.getCountryNumber(); cityKey = sort.createSortKey(null, city.getLabel(), (region & 0xffff) << 16 | (country & 0xffff), cache); } else { cityKey = sort.createSortKey(null, Label.NULL_OUT_LABEL, 0, cache); } SortKey<LabeledRoadDef> sortKey = new MultiSortKey<>(nameKey, cityKey, new IntegerSortKey<LabeledRoadDef>(null, rd.getStartSubdivNumber(), 0)); sortKeys.add(sortKey); } } Collections.sort(sortKeys); List<LabeledRoadDef> out = new ArrayList<>(sortKeys.size()); Label lastName = null; City lastCity = null; List<LabeledRoadDef> dupes = new ArrayList<>(); // Since they are sorted we can easily remove the duplicates. // The duplicates are saved to the dupes list. for (SortKey<LabeledRoadDef> key : sortKeys) { LabeledRoadDef lrd = key.getObject(); Label name = lrd.label; RoadDef road = lrd.roadDef; City city = (road.getCities().isEmpty() ? null : road.getCities().get(0)); // what if we more than one? if (road.hasHouseNumbers() || !name.equals(lastName) || city != lastCity) { // process any previously collected duplicate road names and reset. addDisconnected(dupes, out); dupes = new ArrayList<>(); lastName = name; lastCity = city; } dupes.add(lrd); } // Finish off the final set of duplicates. addDisconnected(dupes, out); return out; } /** * Take a set of roads with the same name/city etc and find sets of roads that do not * connect with each other. One of the members of each set is added to the road list. * * @param in A list of duplicate roads. * @param out The list of sorted roads. Any new road is added to this. */ private void addDisconnected(List<LabeledRoadDef> in, List<LabeledRoadDef> out) { // switch out to different routines depending on the input size. A normal number of // roads with the same name in the same city is a few tens. if (in.size() > 200) { addDisconnectedLarge(in, out); } else { addDisconnectedSmall(in, out); } } /** * Split the input set of roads into disconnected groups and output one member from each group. * * This is done in an accurate manner which is slow for large numbers (eg thousands) of items in the * input. * * @param in Input set of roads with the same name. * @param out List to add the discovered groups. */ private void addDisconnectedSmall(List<LabeledRoadDef> in, List<LabeledRoadDef> out) { // Each road starts out with a different group number int[] groups = new int[in.size()]; for (int i = 0; i < groups.length; i++) groups[i] = i; // Go through pairs of roads, any that are connected we mark with the same (lowest) group number. boolean done; do { done = true; for (int current = 0; current < groups.length; current++) { RoadDef first = in.get(current).roadDef; for (int i = current; i < groups.length; i++) { // If the groups are already the same, then no need to test if (groups[current] == groups[i]) continue; if (first.connectedTo(in.get(i).roadDef)) { groups[current] = groups[i] = Math.min(groups[current], groups[i]); done = false; } } } } while (!done); // Output the first road in each group int last = -1; for (int i = 0; i < groups.length; i++) { if (groups[i] > last) { LabeledRoadDef lrd = in.get(i); out.add(lrd); last = groups[i]; } } } /** * Split the input set of roads into disconnected groups and output one member from each group. * * This is an modified algorithm for large numbers in the input set (eg thousands). * First sort into groups by subdivision and then call {@link #addDisconnectedSmall} on each * one. Since roads in the same subdivision are near each other this finds most connected roads, but * since there is a maximum number of roads in a subdivision, the test can be done very quickly. * You will get a few extra duplicate entries in the index. * * In normal cases this routine gives almost the same results as {@link #addDisconnectedSmall}. * * @param in Input set of roads with the same name. * @param out List to add the discovered groups. */ private void addDisconnectedLarge(List<LabeledRoadDef> in, List<LabeledRoadDef> out) { Collections.sort(in, new Comparator<LabeledRoadDef>() { public int compare(LabeledRoadDef o1, LabeledRoadDef o2) { Integer i1 = o1.roadDef.getStartSubdivNumber(); Integer i2 = o2.roadDef.getStartSubdivNumber(); return i1.compareTo(i2); } }); int lastDiv = 0; List<LabeledRoadDef> dupes = new ArrayList<>(); for (LabeledRoadDef lrd : in) { int sd = lrd.roadDef.getStartSubdivNumber(); if (sd != lastDiv) { addDisconnectedSmall(dupes, out); dupes = new ArrayList<>(); lastDiv = sd; } dupes.add(lrd); } addDisconnectedSmall(dupes, out); } public void setNetwork(List<RoadDef> roads) { this.roads = roads; } public void setSort(Sort sort) { this.sort = sort; } /** * A road can have several names. Keep an association between a road def * and one of its names. */ class LabeledRoadDef { private final Label label; private final RoadDef roadDef; LabeledRoadDef(Label label, RoadDef roadDef) { this.label = label; this.roadDef = roadDef; } } }