Rev 4786 | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* Copyright (C) 2010.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 or
* version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package uk.me.parabola.mkgmap.combiners;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.FileExistsException;
import uk.me.parabola.imgfmt.FileNotWritableException;
import uk.me.parabola.imgfmt.FileSystemParam;
import uk.me.parabola.imgfmt.MapFailedException;
import uk.me.parabola.imgfmt.MapTooBigException;
import uk.me.parabola.imgfmt.Utils;
import uk.me.parabola.imgfmt.app.Area;
import uk.me.parabola.imgfmt.app.Coord;
import uk.me.parabola.imgfmt.app.map.Map;
import uk.me.parabola.imgfmt.app.map.MapReader;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.app.trergn.Point;
import uk.me.parabola.imgfmt.app.trergn.Polygon;
import uk.me.parabola.imgfmt.app.trergn.Polyline;
import uk.me.parabola.imgfmt.app.trergn.Zoom;
import uk.me.parabola.log.Logger;
import uk.me.parabola.mkgmap.CommandArgs;
import uk.me.parabola.mkgmap.build.MapBuilder;
import uk.me.parabola.mkgmap.build.MapSplitter;
import uk.me.parabola.mkgmap.general.LevelInfo;
import uk.me.parabola.mkgmap.general.MapLine;
import uk.me.parabola.mkgmap.general.MapPoint;
import uk.me.parabola.mkgmap.general.MapShape;
import uk.me.parabola.mkgmap.reader.overview.OverviewMapDataSource;
import uk.me.parabola.mkgmap.srt.SrtTextReader;
import uk.me.parabola.util.EnhancedProperties;
/**
* Build the overview map. This is a low resolution map that covers the whole
* of a map set. It also contains polygons that correspond to the areas
* covered by the individual map tiles.
*
* @author Steve Ratcliffe
*/
public class OverviewBuilder implements Combiner {
Logger log = Logger.getLogger(OverviewBuilder.class);
public static final String OVERVIEW_PREFIX = "ovm_";
private OverviewMapDataSource overviewSource;
private String areaName;
private String overviewMapname;
private String overviewMapnumber;
private String outputDir;
private Integer codepage;
private Integer encodingType;
private List<String[]> copyrightMsgs = new ArrayList<>();
private List<String[]> licenseInfos = new ArrayList<>();
private LevelInfo[] wantedLevels;
private Area bounds;
private boolean hasBackground;
private EnhancedProperties overviewProps = new EnhancedProperties();
private int maxRes = 16; // we can write a 0x4a polygon for planet in res 16.
public OverviewBuilder() {
this.overviewSource = new OverviewMapDataSource();
}
public void init(CommandArgs args) {
areaName = args.get("area-name", "Overview Map");
overviewMapname = args.get("overview-mapname", "osmmap");
overviewMapnumber = args.get("overview-mapnumber", "63240000");
outputDir = args.getOutputDir();
overviewProps = new EnhancedProperties(args.getProperties());
}
public void onMapEnd(FileInfo finfo) {
if (!finfo.isImg())
return;
try {
readFileIntoOverview(finfo);
} catch (FileNotFoundException e) {
throw new MapFailedException("Could not read detail map " + finfo.getFilename(), e);
}
}
public void onFinish() {
if (!hasBackground) {
List<MapShape> shapes = overviewSource.getShapes();
int inx = shapes.size();
overviewSource.addBackground();
// for --order-by-decreasing-area, need the background to be first:
if (shapes.size() > inx) // something was added
shapes.add(0, shapes.remove(inx));
}
calcLevels();
writeOverviewMap();
bounds = overviewSource.getBounds();
overviewSource = null; // release memory
}
@Override
public String getFilename() {
return Utils.joinPath(outputDir, overviewMapname, "img");
}
private void calcLevels() {
if (wantedLevels == null)
setRes(maxRes);
else {
// make sure that the wanted levels for the overview map
// can store the largest 0x4a polygon at level 0
int n = wantedLevels.length-1;
while (n > 0 && wantedLevels[n].getBits() > maxRes)
n--;
if (n > 0){
int l = 0;
while (n >= 0){
wantedLevels[n] = new LevelInfo(l++, wantedLevels[n].getBits());
n--;
}
wantedLevels = Arrays.copyOfRange(wantedLevels, 0, l);
overviewSource.setMapLevels(wantedLevels);
} else {
setRes(maxRes);
}
}
}
/**
* Adjust {@code maxRes} value.
* @param detailTileBounds tile bounds (of ovm_ file)
* @param tileName tile name
*/
private int checkFixRes(Area detailTileBounds, String tileName) {
int newMaxRes = maxRes;
int maxSize = MapSplitter.MAX_DIVISION_SIZE << (24 - newMaxRes);
int maxDimPoly = detailTileBounds.getMaxDimension();
if (maxDimPoly > maxSize) {
int oldMaxRes = newMaxRes;
while (maxDimPoly > maxSize) {
newMaxRes--;
maxSize = 0xffff << (24 - newMaxRes);
}
final String msg = "Tile selection (0x4a) polygon for";
final String msg2;
if (tileName != null)
msg2 = "tile " + tileName;
else
msg2 = detailTileBounds.toString();
log.error(msg, msg2, "cannot be written in level 0 resolution", oldMaxRes + ", using", newMaxRes, "instead");
}
return newMaxRes;
}
/**
* Write out the overview map.
*/
private void writeOverviewMap() {
if (overviewSource.mapLevels() == null)
return;
MapBuilder mb = new MapBuilder(false, true);
mb.setEnableLineCleanFilters(false);
FileSystemParam params = new FileSystemParam();
params.setMapDescription(areaName);
mb.setCopyrights(creMsgList(copyrightMsgs));
mb.setMapInfo(creMsgList(licenseInfos));
try {
if (codepage == null){
codepage = 0; // should not happen
}
Sort sort = SrtTextReader.sortForCodepage(codepage);
Map map = Map.createMap(overviewMapname, outputDir, params, overviewMapnumber, sort, true);
map.config(overviewProps);
mb.config(overviewProps);
if (encodingType != null){
map.getLblFile().setEncoder(encodingType, codepage);
}
mb.makeMap(map, overviewSource);
map.close();
} catch (FileExistsException e) {
throw new ExitException("Could not create overview map", e);
} catch (FileNotWritableException e) {
throw new ExitException("Could not write to overview map", e);
} catch (MapTooBigException e) {
throw new MapTooBigException(e.getMaxAllowedSize(),
"The overview map is too big.",
"Try reducing the highest overview resolution or reducing the amount of information included in the overview.");
}
}
/**
* Add an individual .img file to the overview map.
*
* @param finfo Information about an individual map.
*/
private void readFileIntoOverview(FileInfo finfo) throws FileNotFoundException {
MapReader mapReader = null;
String filename = finfo.getFilename();
if (codepage == null){
codepage = finfo.getCodePage();
}
if (codepage != finfo.getCodePage()){
Logger.defaultLogger.warn("Input file " + filename + " has different code page " + finfo.getCodePage());
}
try {
mapReader = new MapReader(filename);
if (encodingType == null){
encodingType = mapReader.getEncodingType();
}
if (encodingType != mapReader.getEncodingType()){
Logger.defaultLogger.warn("Input file " + filename + " has different charset type " + encodingType);
}
String[] msgs = mapReader.getCopyrights();
boolean found = false;
for (String[] block : copyrightMsgs) {
if (Arrays.deepEquals(block, msgs)){
found = true;
break;
}
}
if (!found )
copyrightMsgs.add(msgs);
msgs = finfo.getLicenseInfo();
found = false;
for (String[] block : licenseInfos) {
if (Arrays.deepEquals(block, msgs)){
found = true;
break;
}
}
if (!found )
licenseInfos.add(msgs);
Zoom[] levels = mapReader.getLevels();
if (wantedLevels == null){
LevelInfo[] mapLevels;
if (isOverviewImg(filename)){
mapLevels = new LevelInfo[levels.length-1];
for (int i = 1; i < levels.length; i++){
mapLevels[i-1] = new LevelInfo(levels[i].getLevel(), levels[i].getResolution());
}
} else {
mapLevels = new LevelInfo[1];
mapLevels[0] = new LevelInfo(levels[1].getLevel(), levels[1].getResolution());
}
wantedLevels = mapLevels;
maxRes = wantedLevels[wantedLevels.length-1].getBits();
}
addMapCoverageArea(finfo);
if (isOverviewImg(filename)){
readPoints(mapReader);
readLines(mapReader);
readShapes(mapReader);
}
} catch (FileNotFoundException e) {
throw new ExitException("Could not open " + filename + " when creating overview file");
} finally {
Utils.closeFile(mapReader);
}
}
/**
* Read the points from the .img file and add them to the overview map.
*
* @param mapReader Map reader on the detailed .img file.
*/
private void readPoints(MapReader mapReader) {
Area sourceBounds = overviewSource.getBounds();
Zoom[] levels = mapReader.getLevels();
for (int l = 1; l < levels.length; l++){
int min = levels[l].getLevel();
int res = levels[l].getResolution();
List<Point> pointList = mapReader.pointsForLevel(min, MapReader.WITH_EXT_TYPE_DATA);
for (Point point: pointList) {
if (log.isDebugEnabled())
log.debug("got point", point);
if (!sourceBounds.contains(point.getLocation())){
if (log.isDebugEnabled())
log.debug(point, "dropped, is outside of tile boundary");
continue;
}
MapPoint mp = new MapPoint();
mp.setType(point.getType());
if (point.getLabel() != null) {
mp.setName(point.getLabel().getText());
}
mp.setMaxResolution(res);
mp.setMinResolution(res);
mp.setLocation(point.getLocation());
overviewSource.addPoint(mp);
}
}
}
/**
* Read the lines from the .img file and add them to the overview map.
*
* @param mapReader Map reader on the detailed .img file.
*/
private void readLines(MapReader mapReader) {
Zoom[] levels = mapReader.getLevels();
for (int l = 1; l < levels.length; l++){
int min = levels[l].getLevel();
int res = levels[l].getResolution();
List<Polyline> lineList = mapReader.linesForLevel(min);
for (Polyline line : lineList) {
if (log.isDebugEnabled())
log.debug("got line", line);
MapLine ml = new MapLine();
List<Coord> points = line.getPoints();
if (log.isDebugEnabled())
log.debug("line point list", points);
if (points.size() < 2)
continue;
ml.setType(line.getType());
if ((line.getType() & 0x40) != 0)
ml.setDirection(true);
if (line.getLabel() != null)
ml.setName(line.getLabel().getText());
ml.setMaxResolution(res);
ml.setMinResolution(res);
ml.setPoints(points);
overviewSource.addLine(ml);
}
}
}
/**
* Read the polygons from the .img file and add them to the overview map.
*
* @param mapReader Map reader on the detailed .img file.
*/
private void readShapes(MapReader mapReader) {
Zoom[] levels = mapReader.getLevels();
for (int l = 1; l < levels.length; l++){
int min = levels[l].getLevel();
int res = levels[l].getResolution();
List<Polygon> list = mapReader.shapesForLevel(min, MapReader.WITH_EXT_TYPE_DATA);
for (Polygon shape : list) {
if (log.isDebugEnabled())
log.debug("got polygon", shape);
if (shape.getType() == 0x4b){
hasBackground = true;
}
MapShape ms = new MapShape();
List<Coord> points = shape.getPoints();
if (log.isDebugEnabled())
log.debug("polygon point list", points);
if (points.size() < 3)
continue;
ms.setType(shape.getType());
if (shape.getLabel() != null)
ms.setName(shape.getLabel().getText());
ms.setMaxResolution(res);
ms.setMinResolution(res);
ms.setPoints(points);
overviewSource.addShape(ms);
}
}
}
/**
* Add an area that shows the area covered by a detailed map. This can
* be an arbitary shape, although at the current time we only support
* rectangles.
* Also build up full overview path (not ness. rectangle) for DEM
* and check/decrease resolution if necessary
*
* @param finfo Information about a detail map.
*/
private void addMapCoverageArea(FileInfo finfo) {
Area bounds = finfo.getBounds();
List<Coord> points = bounds.toCoords();
points.forEach(overviewSource::addToBounds);
overviewSource.addToTileAreaPath(points);
maxRes = checkFixRes(bounds, finfo.getMapname());
// Create the tile coverage rectangle
MapShape bg = new MapShape();
bg.setType(0x4a);
bg.setPoints(points);
bg.setMinResolution(0);
bg.setName(finfo.getDescription() + '\u001d' + finfo.getMapname());
overviewSource.addShape(bg);
}
public Area getBounds() {
if (bounds != null)
return bounds;
if (overviewSource != null)
return overviewSource.getBounds();
return new Area(1, 1, -1, -1); // return invalid bbox
}
/**
* Check if the the file name points to a partly overview img file
* @param name full path or just a name
* @return true if the name points to a partly overview img file
*/
public static boolean isOverviewImg (String name){
return new File(name).getName().startsWith(OVERVIEW_PREFIX);
}
/**
* Add the prefix to the file name.
* @param name filename
* @return filename of the corresponding overview img file (without a path)
*/
public static String getOverviewImgName (String name){
File f = new File(name);
return OverviewBuilder.OVERVIEW_PREFIX + f.getName();
}
public static String getMapName(String name) {
String fname = new File(name).getName();
if (fname.startsWith(OVERVIEW_PREFIX))
return fname.substring(OVERVIEW_PREFIX.length());
return name;
}
private static List<String> creMsgList(List<String[]> msgs){
ArrayList< String> list = new ArrayList<>();
for (int i = 0; i < msgs.size(); i++){
String[] block = msgs.get(i);
list.addAll(Arrays.asList(block));
if (i < msgs.size() - 1) {
// separate blocks
list.add("");
}
}
return list;
}
/**
* Set the highest resolution
* @param resolution
*/
private void setRes(int resolution) {
LevelInfo[] mapLevels = new LevelInfo[1];
mapLevels[0] = new LevelInfo(0, resolution);
overviewSource.setMapLevels(mapLevels);
}
}