package org.openstreetmap.josm.plugins.walkingpapers;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.ImageObserver;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.TreeSet;
import javax.swing.Action;
import javax.swing.Icon;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Class that displays a slippy map layer. Adapted from SlippyMap plugin for Walking Papers use.
*
* @author Frederik Ramm <frederik@remote.org>
* @author LuVar <lubomir.varga@freemap.sk>
* @author Dave Hansen <dave@sr71.net>
*
*/
public class WalkingPapersLayer extends Layer implements ImageObserver {
/**
* Actual zoom lvl. Initial zoom lvl is set to
* {@link WalkingPapersPreferences#getMinZoomLvl()}.
*/
private int currentZoomLevel;
private HashMap<WalkingPapersKey, WalkingPapersTile> tileStorage = null;
private Point[][] pixelpos = new Point[21][21];
private LatLon lastTopLeft;
private LatLon lastBotRight;
private int viewportMinX, viewportMaxX, viewportMinY, viewportMaxY;
private Image bufferImage;
private boolean needRedraw;
private int minzoom, maxzoom;
private Bounds printBounds;
private String tileUrlTemplate;
private String walkingPapersId;
public WalkingPapersLayer(String id, String tile, Bounds b, int minz, int maxz) {
super(tr("Walking Papers: {0}", id));
setBackgroundLayer(true);
walkingPapersId = id;
tileUrlTemplate = tile;
this.printBounds = b;
this.minzoom = minz; this.maxzoom = maxz;
currentZoomLevel = minz;
clearTileStorage();
final ActiveLayerChangeListener activeListener = new ActiveLayerChangeListener() {
@Override
public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
// if user changes to a walking papers layer, zoom there just as if it was newly added
handleNewLayer(Main.getLayerManager().getActiveLayer());
}
};
Main.getLayerManager().addActiveLayerChangeListener(activeListener);
Main.getLayerManager().addLayerChangeListener(new LayerChangeListener() {
@Override
public void layerAdded(LayerAddEvent e) {
handleNewLayer(e.getAddedLayer());
}
@Override
public void layerRemoving(LayerRemoveEvent e) {
if (e.getRemovedLayer() == WalkingPapersLayer.this) {
Main.getLayerManager().removeLayerChangeListener(this);
Main.getLayerManager().removeActiveLayerChangeListener(activeListener);
}
}
@Override
public void layerOrderChanged(LayerOrderChangeEvent e) {
}
});
}
private void handleNewLayer(Layer newLayer) {
// only do something if we are affected
if (newLayer != WalkingPapersLayer.this) return;
BoundingXYVisitor bbox = new BoundingXYVisitor();
bbox.visit(printBounds);
Main.map.mapView.zoomTo(bbox);
needRedraw = true;
}
/**
* Zoom in, go closer to map.
*/
public void increaseZoomLevel() {
if (currentZoomLevel < maxzoom) {
currentZoomLevel++;
needRedraw = true;
}
}
/**
* Zoom out from map.
*/
public void decreaseZoomLevel() {
if (currentZoomLevel > minzoom) {
currentZoomLevel--;
needRedraw = true;
}
}
public void clearTileStorage() {
tileStorage = new HashMap<>();
checkTileStorage();
}
static class TileTimeComp implements Comparator<WalkingPapersTile> {
@Override
public int compare(WalkingPapersTile s1, WalkingPapersTile s2) {
long t1 = s1.access_time();
long t2 = s2.access_time();
if (s1 == s2) return 0;
if (t1 == t2) {
t1 = s1.hashCode();
t2 = s2.hashCode();
}
if (t1 < t2) return -1;
return 1;
}
}
long lastCheck = 0;
/**
* <p>
* Check if tiles.size() is not more than max_nr_tiles. If yes, oldest tiles by timestamp
* are fired out from cache.
* </p>
*/
public void checkTileStorage() {
long now = System.currentTimeMillis();
if (now - lastCheck < 1000) return;
lastCheck = now;
TreeSet<WalkingPapersTile> tiles = new TreeSet<>(new TileTimeComp());
tiles.addAll(tileStorage.values());
int max_nr_tiles = 100;
if (tiles.size() < max_nr_tiles) {
return;
}
int dropCount = tiles.size() - max_nr_tiles;;
for (WalkingPapersTile t : tiles) {
if (dropCount <= 0)
break;
t.dropImage();
dropCount--;
}
}
void loadSingleTile(WalkingPapersTile tile) {
tile.loadImage();
this.checkTileStorage();
}
/*
* Attempt to approximate how much the image is
* being scaled. For instance, a 100x100 image
* being scaled to 50x50 would return 0.25.
*/
Double getImageScaling(Image img, Point p0, Point p1) {
int realWidth = img.getWidth(this);
int realHeight = img.getHeight(this);
if (realWidth == -1 || realHeight == -1)
return null;
int drawWidth = p1.x - p0.x;
int drawHeight = p1.x - p0.x;
double drawArea = drawWidth * drawHeight;
double realArea = realWidth * realHeight;
return drawArea / realArea;
}
/**
*/
@Override
public void paint(Graphics2D g, MapView mv, Bounds bounds) {
LatLon topLeft = mv.getLatLon(0, 0);
LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
Graphics2D oldg = g;
if (botRight.lon() == 0.0 || botRight.lat() == 0) {
// probably still initializing
return;
}
if (lastTopLeft != null && lastBotRight != null
&& topLeft.equalsEpsilon(lastTopLeft)
&& botRight.equalsEpsilon(lastBotRight) && bufferImage != null
&& mv.getWidth() == bufferImage.getWidth(null)
&& mv.getHeight() == bufferImage.getHeight(null) && !needRedraw) {
g.drawImage(bufferImage, 0, 0, null);
return;
}
needRedraw = false;
lastTopLeft = topLeft;
lastBotRight = botRight;
bufferImage = mv.createImage(mv.getWidth(), mv.getHeight());
g = (Graphics2D) bufferImage.getGraphics();
if (!LatLon.isValidLat(topLeft.lat()) ||
!LatLon.isValidLat(botRight.lat()) ||
!LatLon.isValidLon(topLeft.lon()) ||
!LatLon.isValidLon(botRight.lon()))
return;
viewportMinX = lonToTileX(topLeft.lon());
viewportMaxX = lonToTileX(botRight.lon());
viewportMinY = latToTileY(topLeft.lat());
viewportMaxY = latToTileY(botRight.lat());
if (viewportMinX > viewportMaxX) {
int tmp = viewportMinX;
viewportMinX = viewportMaxX;
viewportMaxX = tmp;
}
if (viewportMinY > viewportMaxY) {
int tmp = viewportMinY;
viewportMinY = viewportMaxY;
viewportMaxY = tmp;
}
if (viewportMaxX-viewportMinX > 18) return;
if (viewportMaxY-viewportMinY > 18) return;
for (int x = viewportMinX - 1; x <= viewportMaxX + 1; x++) {
double lon = tileXToLon(x);
for (int y = viewportMinY - 1; y <= viewportMaxY + 1; y++) {
LatLon tmpLL = new LatLon(tileYToLat(y), lon);
pixelpos[x - viewportMinX + 1][y - viewportMinY + 1] = mv.getPoint(Main.getProjection()
.latlon2eastNorth(tmpLL));
}
}
g.setColor(Color.DARK_GRAY);
Double imageScale = null;
int count = 0;
for (int x = viewportMinX-1; x <= viewportMaxX; x++) {
for (int y = viewportMinY-1; y <= viewportMaxY; y++) {
WalkingPapersKey key = new WalkingPapersKey(currentZoomLevel, x, y);
WalkingPapersTile tile;
tile = tileStorage.get(key);
if (!key.valid) continue;
if (tile == null) {
// check if tile is in range
Bounds tileBounds = new Bounds(new LatLon(tileYToLat(y+1), tileXToLon(x)),
new LatLon(tileYToLat(y), tileXToLon(x+1)));
if (!tileBounds.asRect().intersects(printBounds.asRect())) continue;
tile = new WalkingPapersTile(x, y, currentZoomLevel, this);
tileStorage.put(key, tile);
loadSingleTile(tile);
checkTileStorage();
}
Image img = tile.getImage();
if (img != null) {
Point p = pixelpos[x - viewportMinX + 1][y - viewportMinY + 1];
Point p2 = pixelpos[x - viewportMinX + 2][y - viewportMinY + 2];
g.drawImage(img, p.x, p.y, p2.x - p.x, p2.y - p.y, this);
if (imageScale == null)
imageScale = getImageScaling(img, p, p2);
count++;
}
}
}
if (count == 0)
{
//System.out.println("no images on " + walkingPapersId + ", return");
return;
}
oldg.drawImage(bufferImage, 0, 0, null);
if (imageScale != null) {
// If each source image pixel is being stretched into > 3
// drawn pixels, zoom in... getting too pixelated
if (imageScale > 3) {
increaseZoomLevel();
this.paint(oldg, mv, bounds);
}
// If each source image pixel is being squished into > 0.32
// of a drawn pixels, zoom out.
else if (imageScale < 0.32) {
decreaseZoomLevel();
this.paint(oldg, mv, bounds);
}
}
}// end of paint metod
WalkingPapersTile getTileForPixelpos(int px, int py) {
int tilex = viewportMaxX;
int tiley = viewportMaxY;
for (int x = viewportMinX; x <= viewportMaxX; x++) {
if (pixelpos[x - viewportMinX + 1][0].x > px) {
tilex = x - 1;
break;
}
}
if (tilex == -1) return null;
for (int y = viewportMinY; y <= viewportMaxY; y++) {
if (pixelpos[0][y - viewportMinY + 1].y > py) {
tiley = y - 1;
break;
}
}
if (tiley == -1) return null;
WalkingPapersKey key = new WalkingPapersKey(currentZoomLevel, tilex, tiley);
if (!key.valid) {
System.err.println("getTileForPixelpos("+px+","+py+") made invalid key");
return null;
}
WalkingPapersTile tile = tileStorage.get(key);
if (tile == null)
tileStorage.put(key, tile = new WalkingPapersTile(tilex, tiley, currentZoomLevel, this));
checkTileStorage();
return tile;
}
@Override
public Icon getIcon() {
return ImageProvider.get("walkingpapers");
}
@Override
public Object getInfoComponent() {
return getToolTipText();
}
@Override
public Action[] getMenuEntries() {
return new Action[] {
LayerListDialog.getInstance().createShowHideLayerAction(),
LayerListDialog.getInstance().createDeleteLayerAction(),
SeparatorLayerAction.INSTANCE,
// color,
// new JMenuItem(new RenameLayerAction(associatedFile, this)),
SeparatorLayerAction.INSTANCE,
new LayerListPopup.InfoAction(this) };
}
@Override
public String getToolTipText() {
return tr("Walking Papers layer ({0}) in zoom {1}", this.getWalkingPapersId(), currentZoomLevel);
}
@Override
public boolean isMergable(Layer other) {
return false;
}
@Override
public void mergeFrom(Layer from) {
}
@Override
public void visitBoundingBox(BoundingXYVisitor v) {
if (printBounds != null)
v.visit(printBounds);
}
private int latToTileY(double lat) {
double l = lat / 180 * Math.PI;
double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
return (int) (Math.pow(2.0, currentZoomLevel - 1) * (Math.PI - pf) / Math.PI);
}
private int lonToTileX(double lon) {
return (int) (Math.pow(2.0, currentZoomLevel - 3) * (lon + 180.0) / 45.0);
}
private double tileYToLat(int y) {
return Math.atan(Math.sinh(Math.PI
- (Math.PI * y / Math.pow(2.0, currentZoomLevel - 1))))
* 180 / Math.PI;
}
private double tileXToLon(int x) {
return x * 45.0 / Math.pow(2.0, currentZoomLevel - 3) - 180.0;
}
@Override
public boolean imageUpdate(Image img, int infoflags, int x, int y,
int width, int height) {
boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
if ((infoflags & ERROR) != 0) return false;
// Repaint immediately if we are done, otherwise batch up
// repaint requests every 100 milliseconds
needRedraw = true;
Main.map.repaint(done ? 0 : 100);
return !done;
}
public String getWalkingPapersId() {
return walkingPapersId;
}
public URL formatImageUrl(int x, int y, int z) {
String urlstr = tileUrlTemplate.
replace("{x}", String.valueOf(x)).
replace("{y}", String.valueOf(y)).
replace("{z}", String.valueOf(z));
try {
return new URL(urlstr);
} catch (Exception ex) {
return null;
}
}
}