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