+ *
+ */
+public class FieldPapersKey {
+ private final int x;
+ private final int y;
+ private final int level;
+
+ /**
+ *
+ * Constructs key for hashmaps for some tile describedy by X and Y position. X and Y are tiles
+ * positions on discrete map.
+ *
+ *
+ * @param x x position in tiles table
+ * @param y y position in tiles table
+ */
+ public final boolean valid;
+ public FieldPapersKey(int level, int x, int y) {
+ this.x = x;
+ this.y = y;
+ this.level = level;
+ if (level <= 0 || x < 0 || y < 0) {
+ this.valid = false;
+ System.err.println("invalid WalkingPapersKey("+level+", "+x+", "+y+")");
+ } else {
+ this.valid = true;
+ }
+ }
+
+ /**
+ *
+ * Returns true ONLY if x and y are equals.
+ *
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FieldPapersKey) {
+ FieldPapersKey smk = (FieldPapersKey) obj;
+ if((smk.x == this.x) && (smk.y == this.y) && (smk.level == this.level)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return return new Integer(this.x + this.y * 10000).hashCode();
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return new Integer(this.x + this.y * 10000 + this.level * 100000).hashCode();
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "WalkingPapersKey(x=" + this.x + ",y=" + this.y + ",level=" + level + ")";
+ }
+
+}
diff --git a/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersLayer.java b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersLayer.java
new file mode 100644
index 0000000..421dec5
--- /dev/null
+++ b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersLayer.java
@@ -0,0 +1,427 @@
+package org.openstreetmap.josm.plugins.fieldpapers;
+
+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.MapView.LayerChangeListener;
+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.tools.ImageProvider;
+
+/**
+ * Class that displays a slippy map layer. Adapted from SlippyMap plugin for Walking Papers use.
+ *
+ * @author Frederik Ramm
+ * @author LuVar
+ * @author Dave Hansen
+ *
+ */
+public class FieldPapersLayer extends Layer implements ImageObserver {
+ /**
+ * Actual zoom lvl. Initial zoom lvl is set to
+ * {@link WalkingPapersPreferences#getMinZoomLvl()}.
+ */
+ private int currentZoomLevel;
+ private HashMap 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;
+
+ @SuppressWarnings("serial")
+ public FieldPapersLayer(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();
+
+ MapView.addLayerChangeListener(new LayerChangeListener() {
+ public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+ // if user changes to a walking papers layer, zoom there just as if
+ // it was newly added
+ layerAdded(newLayer);
+ }
+
+ public void layerAdded(Layer newLayer) {
+ // only do something if we are affected
+ if (newLayer != FieldPapersLayer.this) return;
+ BoundingXYVisitor bbox = new BoundingXYVisitor();
+ bbox.visit(printBounds);
+ Main.map.mapView.recalculateCenterScale(bbox);
+ needRedraw = true;
+ }
+
+ public void layerRemoved(Layer oldLayer) {
+ if (oldLayer == FieldPapersLayer.this) {
+ MapView.removeLayerChangeListener(this);
+ }
+ }
+ });
+ }
+
+ /**
+ * 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 {
+ public int compare(FieldPapersTile s1, FieldPapersTile 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;
+ /**
+ *
+ * Check if tiles.size() is not more than max_nr_tiles. If yes, oldest tiles by timestamp
+ * are fired out from cache.
+ *
+ */
+ public void checkTileStorage() {
+ long now = System.currentTimeMillis();
+ if (now - lastCheck < 1000) return;
+ lastCheck = now;
+ TreeSet 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 (FieldPapersTile t : tiles) {
+ if (dropCount <= 0)
+ break;
+ t.dropImage();
+ dropCount--;
+ }
+ }
+
+ void loadSingleTile(FieldPapersTile 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++) {
+ FieldPapersKey key = new FieldPapersKey(currentZoomLevel, x, y);
+ FieldPapersTile 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 FieldPapersTile(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
+
+ FieldPapersTile 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;
+
+ FieldPapersKey key = new FieldPapersKey(currentZoomLevel, tilex, tiley);
+ if (!key.valid) {
+ System.err.println("getTileForPixelpos("+px+","+py+") made invalid key");
+ return null;
+ }
+ FieldPapersTile tile = tileStorage.get(key);
+ if (tile == null)
+ tileStorage.put(key, tile = new FieldPapersTile(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;
+ }
+
+ 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;
+ }
+ }
+
+}
diff --git a/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersPlugin.java b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersPlugin.java
new file mode 100644
index 0000000..bfbf7b9
--- /dev/null
+++ b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersPlugin.java
@@ -0,0 +1,33 @@
+package org.openstreetmap.josm.plugins.fieldpapers;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+
+import java.awt.event.KeyEvent;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginInformation;
+
+/**
+ * Main class for the walking papers plugin.
+ *
+ * @author Frederik Ramm
+ *
+ */
+public class FieldPapersPlugin extends Plugin
+{
+ static JMenu walkingPapersMenu;
+
+ public FieldPapersPlugin(PluginInformation info)
+ {
+ super(info);
+ MainMenu menu = Main.main.menu;
+ walkingPapersMenu = menu.addMenu(marktr("Walking Papers"), KeyEvent.VK_K, menu.defaultMenuPos, ht("/Plugin/WalkingPapers"));
+ walkingPapersMenu.add(new JMenuItem(new FieldPapersAddLayerAction()));
+ }
+}
diff --git a/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersTile.java b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersTile.java
new file mode 100644
index 0000000..9679e03
--- /dev/null
+++ b/src/org/openstreetmap/josm/plugins/fieldpapers/FieldPapersTile.java
@@ -0,0 +1,67 @@
+package org.openstreetmap.josm.plugins.fieldpapers;
+
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.net.URL;
+
+/**
+ * Class that contains information about one single slippy map tile.
+ *
+ * @author Frederik Ramm
+ * @author LuVar
+ * @author Dave Hansen
+ *
+ */
+public class FieldPapersTile {
+ private Image tileImage;
+ long timestamp;
+
+ int x;
+ int y;
+ int z;
+
+ FieldPapersLayer parentLayer;
+
+
+ public FieldPapersTile(int x, int y, int z, FieldPapersLayer parent) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ parentLayer = parent;
+ timestamp = System.currentTimeMillis();
+ }
+
+ public URL getImageUrl() {
+ return parentLayer.formatImageUrl(x, y, z);
+ }
+
+ public void loadImage() {
+ URL imageUrl = this.getImageUrl();
+ tileImage = Toolkit.getDefaultToolkit().createImage(imageUrl);
+ Toolkit.getDefaultToolkit().sync();
+ timestamp = System.currentTimeMillis();
+ }
+
+ public Image getImage() {
+ timestamp = System.currentTimeMillis();
+ return tileImage;
+ }
+
+ public void dropImage() {
+ tileImage = null;
+ // This should work in theory but doesn't seem to actually
+ // reduce the X server memory usage
+ //tileImage.flush();
+ }
+
+ public long access_time() {
+ return timestamp;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof FieldPapersTile))
+ return false;
+ FieldPapersTile other = (FieldPapersTile) o;
+ return (this.x == other.x && this.y == other.y && this.z == other.z);
+ }
+}