/*******************************************************************************
* Copyright (c) MOBAC developers
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package mobac.program.model;
import java.awt.Dimension;
import java.awt.Point;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import javax.swing.tree.TreeNode;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElements;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import mobac.exceptions.InvalidNameException;
import mobac.program.interfaces.AtlasInterface;
import mobac.program.interfaces.CapabilityDeletable;
import mobac.program.interfaces.LayerInterface;
import mobac.program.interfaces.MapInterface;
import mobac.program.interfaces.MapSource;
import mobac.program.interfaces.MapSpace;
import mobac.program.interfaces.ToolTipProvider;
import mobac.utilities.I18nUtils;
import mobac.utilities.MyMath;
import mobac.utilities.Utilities;
import org.apache.log4j.Logger;
/**
* A layer holding one or multiple maps of the same map source and the same zoom level. The number of maps depends on
* the size of the covered area - if it is smaller than the specified <code>maxMapSize</code> then there will be only
* one map.
*
*/
@XmlRootElement
public class Layer implements LayerInterface, TreeNode, ToolTipProvider, CapabilityDeletable {
private static Logger log = Logger.getLogger(Layer.class);
@XmlTransient
private AtlasInterface atlasInterface;
private String name;
@XmlElements({ @XmlElement(name = "PolygonMap", type = MapPolygon.class),
@XmlElement(name = "Map", type = Map.class) })
private LinkedList<MapInterface> maps = new LinkedList<MapInterface>();
protected Layer() {
}
public Layer(AtlasInterface atlasInterface, String name) throws InvalidNameException {
this.atlasInterface = atlasInterface;
setName(name);
}
public void addMapsAutocut(String mapNameBase, MapSource mapSource, EastNorthCoordinate minCoordinate,
EastNorthCoordinate maxCoordinate, int zoom, TileImageParameters parameters, int maxMapSize)
throws InvalidNameException {
MapSpace mapSpace = mapSource.getMapSpace();
addMapsAutocut(mapNameBase, mapSource, minCoordinate.toTileCoordinate(mapSpace, zoom),
maxCoordinate.toTileCoordinate(mapSpace, zoom), zoom, parameters, maxMapSize, 0);
}
public void addMapsAutocut(String mapNameBase, MapSource mapSource, Point minTileCoordinate,
Point maxTileCoordinate, int zoom, TileImageParameters parameters, int maxMapSize, int overlapTiles)
throws InvalidNameException {
log.trace("Adding new map(s): \"" + mapNameBase + "\" " + mapSource + " zoom=" + zoom + " min="
+ minTileCoordinate.x + "/" + minTileCoordinate.y + " max=" + maxTileCoordinate.x + "/"
+ maxTileCoordinate.y);
int tileSize = mapSource.getMapSpace().getTileSize();
minTileCoordinate.x -= minTileCoordinate.x % tileSize;
minTileCoordinate.y -= minTileCoordinate.y % tileSize;
maxTileCoordinate.x += tileSize - 1 - (maxTileCoordinate.x % tileSize);
maxTileCoordinate.y += tileSize - 1 - (maxTileCoordinate.y % tileSize);
Dimension tileDimension;
if (parameters == null)
tileDimension = new Dimension(tileSize, tileSize);
else
tileDimension = parameters.getDimension();
// We adapt the max map size to the tile size so that we do
// not get ugly cutted/incomplete tiles at the borders
Dimension maxMapDimension = new Dimension(maxMapSize, maxMapSize);
maxMapDimension.width -= maxMapSize % tileDimension.width;
maxMapDimension.height -= maxMapSize % tileDimension.height;
int mapWidth = maxTileCoordinate.x - minTileCoordinate.x;
int mapHeight = maxTileCoordinate.y - minTileCoordinate.y;
if (mapWidth < maxMapDimension.width && mapHeight < maxMapDimension.height) {
Map s = new Map(this, mapNameBase, mapSource, zoom, minTileCoordinate, maxTileCoordinate, parameters);
maps.add(s);
return;
}
Dimension nextMapStep = new Dimension(maxMapDimension.width - (tileDimension.width * overlapTiles),
maxMapDimension.height - (tileDimension.height * overlapTiles));
int maxXCounter = MyMath.divCeil(mapWidth, nextMapStep.width);
int maxYCounter = MyMath.divCeil(mapHeight, nextMapStep.height);
int maxMapCounter = maxXCounter * maxYCounter;
int maxMapCountDigits = (int) Math.ceil(Math.log10(maxMapCounter));
String mapNameFormat = "%s (%0" + maxMapCountDigits + "d)";
int mapCounter = 0;
for (int mapX = minTileCoordinate.x; mapX < maxTileCoordinate.x; mapX += nextMapStep.width) {
for (int mapY = minTileCoordinate.y; mapY < maxTileCoordinate.y; mapY += nextMapStep.height) {
int maxX = Math.min(mapX + maxMapDimension.width, maxTileCoordinate.x);
int maxY = Math.min(mapY + maxMapDimension.height, maxTileCoordinate.y);
Point min = new Point(mapX, mapY);
Point max = new Point(maxX - 1, maxY - 1);
String mapName = String.format(mapNameFormat, new Object[] { mapNameBase, mapCounter++ });
Map s = new Map(this, mapName, mapSource, zoom, min, max, parameters);
maps.add(s);
}
}
}
public void delete() {
maps.clear();
atlasInterface.deleteLayer(this);
}
public AtlasInterface getAtlas() {
return atlasInterface;
}
public void addMap(MapInterface map) {
// TODO: Add name collision check
maps.add(map);
map.setLayer(this);
}
public MapInterface getMap(int index) {
return maps.get(index);
}
public int getMapCount() {
return maps.size();
}
@XmlAttribute
public String getName() {
return name;
}
public void setName(String newName) throws InvalidNameException {
if (atlasInterface != null) {
for (LayerInterface layer : atlasInterface) {
if ((layer != this) && newName.equals(layer.getName()))
throw new InvalidNameException("There is already a layer named \"" + newName
+ "\" in this atlas.\nLayer names have to unique within an atlas.");
}
}
this.name = newName;
}
@Override
public String toString() {
return name;
}
public long calculateTilesToDownload() {
long result = 0;
for (MapInterface map : maps)
result += map.calculateTilesToDownload();
return result;
}
public double getMinLat() {
double lat = 90d;
for (MapInterface m : maps) {
lat = Math.min(lat, m.getMinLat());
}
return lat;
}
public double getMaxLat() {
double lat = -90d;
for (MapInterface m : maps) {
lat = Math.max(lat, m.getMaxLat());
}
return lat;
}
public double getMinLon() {
double lon = 180d;
for (MapInterface m : maps) {
lon = Math.min(lon, m.getMinLon());
}
return lon;
}
public double getMaxLon() {
double lon = -180d;
for (MapInterface m : maps) {
lon = Math.max(lon, m.getMaxLon());
}
return lon;
}
public String getToolTip() {
StringWriter sw = new StringWriter(1024);
sw.write("<html>");
sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_layer_title"));
sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_layer_map_count", maps.size()));
sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_max_tile", calculateTilesToDownload()));
sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_start",
Utilities.prettyPrintLatLon(getMaxLat(), true), Utilities.prettyPrintLatLon(getMinLon(), false)));
sw.write(I18nUtils.localizedStringForKey("lp_atlas_info_area_end",
Utilities.prettyPrintLatLon(getMinLat(), true), Utilities.prettyPrintLatLon(getMaxLon(), false)));
sw.write("</html>");
return sw.toString();
}
public Iterator<MapInterface> iterator() {
return maps.iterator();
}
public Enumeration<?> children() {
return Collections.enumeration(maps);
}
public boolean getAllowsChildren() {
return true;
}
public TreeNode getChildAt(int childIndex) {
return (TreeNode) maps.get(childIndex);
}
public int getChildCount() {
return maps.size();
}
public int getIndex(TreeNode node) {
return maps.indexOf(node);
}
public TreeNode getParent() {
return (TreeNode) atlasInterface;
}
public boolean isLeaf() {
return false;
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
this.atlasInterface = (Atlas) parent;
}
public boolean checkData() {
if (atlasInterface == null)
return true;
if (name == null)
return true;
// Check for duplicate map names
HashSet<String> names = new HashSet<String>(maps.size());
for (MapInterface map : maps)
names.add(map.getName());
if (names.size() < maps.size())
return true; // at least one duplicate name found
return false;
}
public void deleteMap(Map map) {
maps.remove(map);
}
public LayerInterface deepClone(AtlasInterface atlas) {
Layer layer = new Layer();
layer.atlasInterface = atlas;
layer.name = name;
for (MapInterface map : maps)
layer.maps.add(map.deepClone(layer));
return layer;
}
}