/* * Copyright (c) 2009, 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 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 uk.me.parabola.splitter; import java.awt.Point; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.xmlpull.v1.XmlPullParserException; import uk.me.parabola.splitter.geo.City; import uk.me.parabola.splitter.geo.CityFinder; import uk.me.parabola.splitter.geo.CityLoader; import uk.me.parabola.splitter.geo.DefaultCityFinder; import uk.me.parabola.splitter.kml.KmlParser; import uk.me.parabola.splitter.kml.KmlWriter; import uk.me.parabola.splitter.solver.PolygonDesc; /** * A list of areas. It can be read and written to a file. */ public class AreaList { private final List areas; private final String description; private String geoNamesFile; /** * This constructor is called when you are going to be reading in the list from * a file, rather than making it from an already constructed list. */ public AreaList(String description) { this(new ArrayList(), description); } public AreaList(List areas, String description) { this.description = description; this.areas = areas; } /** * Write out a file containing the list of areas that we calculated. This allows us to reuse the * same areas on a subsequent run without having to re-calculate them. * * @param filename The filename to write to. */ public void write(String filename) { try (Writer w = new FileWriter(filename); PrintWriter pw = new PrintWriter(w);) { pw.println("# List of areas"); pw.format("# Generated %s%n", new Date()); pw.println("#"); for (Area area : areas) { pw.format(Locale.ROOT, "%08d: %d,%d to %d,%d%n", area.getMapId(), area.getMinLat(), area.getMinLong(), area.getMaxLat(), area.getMaxLong()); pw.format(Locale.ROOT, "# : %f,%f to %f,%f%n", Utils.toDegrees(area.getMinLat()), Utils.toDegrees(area.getMinLong()), Utils.toDegrees(area.getMaxLat()), Utils.toDegrees(area.getMaxLong())); pw.println(); } } catch (IOException e) { System.err.println("Could not write areas.list file, processing continues"); } } public void read(String filename) throws IOException { String lower = filename.toLowerCase(); if (lower.endsWith(".kml") || lower.endsWith(".kml.gz") || lower.endsWith(".kml.bz2")) { readKml(filename); } else { readList(filename); } } /** * Read in an area definition file that we previously wrote. * Obviously other tools could create the file too. */ private void readList(String filename) throws IOException { areas.clear(); Pattern pattern = Pattern.compile("([0-9]{8})[ ]*:" + "[ ]*([\\p{XDigit}x-]+),([\\p{XDigit}x-]+)" + " to ([\\p{XDigit}x-]+),([\\p{XDigit}x-]+)"); try (Reader r = new FileReader(filename); BufferedReader br = new BufferedReader(r)) { String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty() || line.charAt(0) == '#') continue; try { Matcher matcher = pattern.matcher(line); matcher.find(); String mapid = matcher.group(1); Area area = new Area( Integer.decode(matcher.group(2)), Integer.decode(matcher.group(3)), Integer.decode(matcher.group(4)), Integer.decode(matcher.group(5))); if (!area.verify()) throw new IllegalArgumentException("Invalid area in file "+ filename+ ": " + line); area.setMapId(Integer.parseInt(mapid)); areas.add(area); } catch (IllegalStateException e) { throw new IllegalArgumentException("Cannot parse line " + line); } } } catch (NumberFormatException e) { throw new IllegalArgumentException("Bad number in areas list file"); } } private void readKml(String filename) throws IOException { try { KmlParser parser = new KmlParser(); parser.setReader(Utils.openFile(filename, false)); parser.parse(); List newAreas = parser.getAreas(); areas.clear(); areas.addAll(newAreas); } catch (XmlPullParserException e) { throw new IOException("Unable to parse KML file " + filename, e); } } public List getAreas() { return Collections.unmodifiableList(areas); } public void dump() { System.out.println("Areas read from file"); for (Area area : areas) { System.out.println(area.getMapId() + " " + area.toString()); } } public void dumpHex() { System.out.println(areas.size() + " areas:"); for (Area area : areas) { System.out.format("Area %08d: %d,%d to %d,%d covers %s", area.getMapId(), area.getMinLat(), area.getMinLong(), area.getMaxLat(), area.getMaxLong(), area.toHexString()); if (area.getName() != null) System.out.print(' ' + area.getName()); System.out.println(); } } /** * Write out a poly file containing the bounding polygon for the areas * that we calculated. * * @param filename The poly filename to write to. */ public void writePoly(String filename) { java.awt.geom.Area polygonArea = new java.awt.geom.Area(); for (Area area : areas) { polygonArea.add(new java.awt.geom.Area(Utils.area2Rectangle(area, 0))); } List> shapes = Utils.areaToShapes(polygonArea); // start with outer polygons Collections.reverse(shapes); try (PrintWriter pw = new PrintWriter(filename)) { pw.println("area"); for (int i = 0; i < shapes.size(); i++) { List shape = shapes.get(i); if (Utils.clockwise(shape)) pw.println(i + 1); else pw.println("!" + (i + 1)); Point point = null; for (int j = 0; j < shape.size(); j++) { point = shape.get(j); if (j > 0 && j + 1 < shape.size()) { Point lastPoint = shape.get(j - 1); Point nextPoint = shape.get(j + 1); if ((point.x == nextPoint.x && point.x == lastPoint.x) || (point.y == nextPoint.y && point.y == lastPoint.y)) continue; } pw.format(Locale.ROOT, " %f %f%n", Utils.toDegrees(point.x), Utils.toDegrees(point.y)); } pw.println("END"); } pw.println("END"); } catch (IOException e) { System.err.println("Could not write polygon file " + filename + ", processing continues"); } } /** * Write a file that can be given to mkgmap that contains the correct arguments * for the split file pieces. You are encouraged to edit the file and so it * contains a template of all the arguments that you might want to use. */ public void writeArgsFile(String filename, String outputType, int startMapId) { try (PrintWriter w = new PrintWriter(new FileWriter(filename))){ w.println("#"); w.println("# This file can be given to mkgmap using the -c option"); w.println("# Please edit it first to add a description of each map."); w.println("#"); w.println(); w.println("# You can set the family id for the map"); w.println("# family-id: 980"); w.println("# product-id: 1"); w.println(); w.println("# Following is a list of map tiles. Add a suitable description"); w.println("# for each one."); int mapId = startMapId; if (mapId % 100 == 0) mapId++; for (Area a : areas) { w.println(); w.format("mapname: %08d%n", (startMapId <0) ? a.getMapId() : mapId++); if (a.getName() == null) w.println("# description: OSM Map"); else w.println("description: " + (a.getName().length() > 50 ? a.getName().substring(0, 50) : a.getName())); String ext; if("pbf".equals(outputType)) ext = ".osm.pbf"; else if("o5m".equals(outputType)) ext = ".o5m"; else ext = ".osm.gz"; w.format("input-file: %08d%s%n", a.getMapId(), ext); } w.println(); } catch (IOException e) { throw new SplitFailedException("Could not write template.args file " + filename, e.getCause()); } } private static CityFinder cityFinder = null; public void setAreaNames() { synchronized (this) { if (geoNamesFile != null){ CityLoader cityLoader = new CityLoader(true); List cities = cityLoader.load(geoNamesFile); if (cities == null) return; cityFinder = new DefaultCityFinder(cities); } } for (Area area : getAreas()) { City bestMatch = null; if (cityFinder != null) { // Decide what to call the area Set found = cityFinder.findCities(area); // Select largest population city in area/ for (City city : found) { if (bestMatch == null || city.getPopulation() > bestMatch.getPopulation()) { bestMatch = city; } } } area.setName( (bestMatch == null ? description : bestMatch.getCountryCode() + '-' + bestMatch.getName()) + ":"+plusCode(area)); } } /** * * @param mapId */ public void setMapIds(int mapId) { for (Area area : getAreas()) { area.setMapId(mapId++); } } public void setGeoNamesFile(String geoNamesFile) { this.geoNamesFile = geoNamesFile; } public void setAreas(List calculateAreas) { areas.clear(); areas.addAll(calculateAreas); } /** * * @param fileOutputDir * @param polygons * @param kmlOutputFile * @param outputType */ public void writeListFiles(File fileOutputDir, List polygons, String kmlOutputFile, String outputType) { for (PolygonDesc pd : polygons) { List areasPart = new ArrayList<>(); for (uk.me.parabola.splitter.Area a : areas) { if (pd.getArea().intersects(a.getRect())) areasPart.add(a); } if (kmlOutputFile != null) { File out = new File(kmlOutputFile); String kmlOutputFilePart = pd.getName() + "-" + out.getName(); if (out.getParent() != null) out = new File(out.getParent(), kmlOutputFilePart); else out = new File(kmlOutputFilePart); if (out.getParent() == null) out = new File(fileOutputDir, kmlOutputFilePart); KmlWriter.writeKml(out.getPath(), areasPart); } AreaList al = new AreaList(areasPart, null); al.setGeoNamesFile(geoNamesFile); al.writePoly(new File(fileOutputDir, pd.getName() + "-" + "areas.poly").getPath()); al.writeArgsFile(new File(fileOutputDir, pd.getName() + "-" + "template.args").getPath(), outputType, pd.getMapId()); } } private static final String codes = "23456789CFGHJMPQRVWX"; private static final double GarminFactor = 360.0/((double)(1<<24)); private String plusCode(Area area) { double south = area.getMinLat()*GarminFactor; double west = area.getMinLong()*GarminFactor; double north = area.getMaxLat()*GarminFactor; double east = area.getMaxLong()*GarminFactor; double centerLatitude = 0.5 *(south + north)+90.; double centerLongitude = 0.5 * (west + east)+180.; double boxHeight = north - south; double boxWidth = east - west; double height = 400.; double width = 400.; StringBuilder code = new StringBuilder(); boolean done = false; while(code.length() < 8 & !done) { int p; height /= 20.; width /= 20.; p = (int)Math.floor(centerLatitude/height); code.append(codes.charAt(p)); centerLatitude -= p * height; p = (int)Math.floor(centerLongitude/width); code.append(codes.charAt(p)); centerLongitude -= p * width; done = width <= boxWidth && height <= boxHeight; } while(code.length() < 8) code.append("00"); code.append("+"); while(!done) { int la, lo; height /= 5.; width /= 4.; la = (int)Math.floor(centerLatitude/height); centerLatitude -= la * height; lo = (int)Math.floor(centerLongitude/width); centerLongitude -= lo * width; code.append(codes.charAt(4*la+lo)); done = width <= boxWidth && height <= boxHeight; } return code.toString(); } }