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