package com.opendoorlogistics.studio.components.map;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.JPanel;
import com.opendoorlogistics.api.geometry.LatLong;
import com.opendoorlogistics.api.geometry.LatLongToScreen;
import com.opendoorlogistics.api.standardcomponents.map.MapApi;
import com.opendoorlogistics.api.standardcomponents.map.MapTileProvider;
import com.opendoorlogistics.api.standardcomponents.map.MapTileProvider.MapTile;
import com.opendoorlogistics.api.standardcomponents.map.MapTileProvider.MapTileLoadedListener;
import com.opendoorlogistics.api.tables.ODLTableReadOnly;
import com.opendoorlogistics.api.ui.Disposable;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.GeoPosition;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.Tile;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileFactory;
import com.opendoorlogistics.codefromweb.jxmapviewer2.fork.swingx.mapviewer.TileListener;
import com.opendoorlogistics.core.gis.map.RenderProperties;
import com.opendoorlogistics.core.gis.map.background.BackgroundTileFactorySingleton;
import com.opendoorlogistics.core.gis.map.background.ODLTileFactory;
import com.opendoorlogistics.core.gis.map.data.BackgroundImage;
import com.opendoorlogistics.core.gis.map.data.LatLongImpl;
import com.opendoorlogistics.core.gis.map.transforms.LatLongToScreenImpl;
public class MapViewPanel extends JPanel implements Disposable, MapTileLoadedListener {
private final ODLTileFactory factory;
private final MapApi mapi;
private BufferedImage mapImage;
// private boolean repaintPluginOverlapOnly;
private boolean disablePaint;
private boolean pendingFullRepaint;
private boolean pendingRepaintPluginOverlapOnly;
// private boolean skipMapDraw;
// private final MapObjectsRenderer renderer;
public MapViewPanel(MapApi mapi) {
this.factory = BackgroundTileFactorySingleton.getFactory();
this.mapi = mapi;
// listen for map tiles being loaded
BackgroundTileFactorySingleton.getFactory().addLoadedListener(this);
// zoom is controlled by the map view panel (as its always valid)
addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent evt) {
Point current = evt.getPoint();
Rectangle bound = getViewportBounds();
int zoom = mapi.getZoom();
int newZoom = zoom + evt.getWheelRotation();
Point zoomCentreWorldBitmap = new Point(current.x + bound.x, current.y + bound.y);
doZoom(newZoom, zoomCentreWorldBitmap);
}
});
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyPressed(KeyEvent e) {
if ((e.getModifiers() & KeyEvent.CTRL_MASK) != 0) {
int code = e.getKeyCode();
Point centre = new Point((int) mapi.getWorldBitmapMapCentre().getX(), (int) mapi.getWorldBitmapMapCentre().getY());
// remember that equals shares the key with +
if (code == KeyEvent.VK_PLUS || code == KeyEvent.VK_ADD || code == KeyEvent.VK_EQUALS) {
doZoom(mapi.getZoom() - 1, centre);
}
if (code == KeyEvent.VK_MINUS || code == KeyEvent.VK_SUBTRACT) {
doZoom(mapi.getZoom() + 1, centre);
}
}
}
});
}
public LatLongToScreen createImmutableConverter() {
final int currentZoom = mapi.getZoom();
final Rectangle currentViewport = getViewportBounds();
return new LatLongToScreenImpl() {
@Override
public Rectangle2D getViewportWorldBitmapScreenPosition() {
return currentViewport;
}
@Override
public Point2D getWorldBitmapPixelPosition(LatLong latLong) {
GeoPosition pos = new GeoPosition(latLong.getLatitude(), latLong.getLongitude());
Point2D point = factory.geoToPixel(pos, currentZoom);
return point;
}
@Override
public LatLong getLongLat(double pixelX, double pixelY) {
GeoPosition pos = factory.pixelToGeo(new Point2D.Double(pixelX + currentViewport.x, pixelY + currentViewport.y), currentZoom);
return new LatLongImpl(pos.getLatitude(), pos.getLongitude());
}
@Override
public Object getZoomHashmapKey() {
return currentZoom;
}
@Override
public int getZoomForObjectFiltering() {
return currentZoom;
}
};
}
public Rectangle getViewportBounds() {
Insets insets = getInsets();
// calculate the "visible" viewport area in pixels
int viewportWidth = getWidth() - insets.left - insets.right;
int viewportHeight = getHeight() - insets.top - insets.bottom;
Point2D centre = mapi.getWorldBitmapMapCentre();
double viewportX = (centre.getX() - viewportWidth / 2);
double viewportY = (centre.getY() - viewportHeight / 2);
return new Rectangle((int) viewportX, (int) viewportY, viewportWidth, viewportHeight);
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (isDisablePaint()) {
return;
}
// draw to an image first. The image can be reused to allow the selection box to redrawn but not anything else
boolean pluginOverlayOnlyRepaint = !pendingFullRepaint && pendingRepaintPluginOverlapOnly;
if (mapImage == null || mapImage.getWidth() != getWidth() || mapImage.getHeight() != getHeight() || pluginOverlayOnlyRepaint == false) {
// System.out.println("Full repaint " + new Date());
mapImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = mapImage.createGraphics();
try {
g2.setClip(0, 0, getWidth(), getHeight());
Rectangle viewportBounds = getViewportBounds();
if ((mapi.getRenderFlags() & RenderProperties.SHOW_BACKGROUND) == RenderProperties.SHOW_BACKGROUND) {
drawMapTiles(g2, viewportBounds);
} else {
// fill white
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, getWidth(), getHeight());
}
paintMapObjects(g2);
} finally {
g2.dispose();
}
}
pendingFullRepaint = false;
pendingRepaintPluginOverlapOnly = false;
BufferedImage modifiedImage = ((MapApiImpl) mapi).fireModifyMapImageListeners(mapi, mapImage);
g.drawImage(modifiedImage, 0, 0, getWidth(), getHeight(), null, null);
paintPluginOverlay((Graphics2D) g);
}
protected void paintMapObjects(Graphics2D g) {
}
protected void paintPluginOverlay(Graphics2D g) {
}
/**
* Adapted From jxmapviewer2
*
* @param g
* @param zoom
* @param viewportBounds
*/
private void drawMapTiles(final Graphics g, Rectangle viewportBounds) {
// get the tile factories...
ODLTableReadOnly imagesTable = mapi.getMapDataApi().getBackgroundImagesTable();
ArrayList<MapTileProvider> factories = new ArrayList<MapTileProvider>();
if (imagesTable != null) {
for (Object provider : BackgroundImage.BEAN_MAPPING.readObjectsFromTable(imagesTable)) {
BackgroundImage mtp = (BackgroundImage) provider;
if (mtp != null && mtp.getTileProvider()!=null) {
factories.add(mtp.getTileProvider());
}
}
}
// add default factory if we haven't got any
if (factories.size() == 0) {
factories.add(BackgroundTileFactorySingleton.getFactory());
}
int zoomLevel = mapi.getZoom();
int tileSize = factory.getTileSize(zoomLevel);
Dimension mapSize = factory.getMapSize(zoomLevel);
// calculate the "visible" viewport area in tiles
int numWide = viewportBounds.width / tileSize + 2;
int numHigh = viewportBounds.height / tileSize + 2;
int tpx = (int) Math.floor(viewportBounds.getX() / tileSize);
int tpy = (int) Math.floor(viewportBounds.getY() / tileSize);
for (int x = 0; x <= numWide; x++) {
for (int y = 0; y <= numHigh; y++) {
int itpx = x + tpx;
int itpy = y + tpy;
if (g.getClipBounds().intersects(new Rectangle(itpx * tileSize - viewportBounds.x, itpy * tileSize - viewportBounds.y, tileSize, tileSize))) {
for (MapTileProvider currentFactory : factories) {
MapTile tile = currentFactory.getMapTile(itpx, itpy, zoomLevel);
if (tile == null) {
continue;
}
int ox = ((itpx * tileSize) - viewportBounds.x);
int oy = ((itpy * tileSize) - viewportBounds.y);
// if the tile is off the map to the north/south, then just
// don't paint anything
if (itpx < 0 || itpy < 0 || itpx > mapSize.width || itpy > mapSize.height) {
if (isOpaque()) {
g.setColor(Color.WHITE);
g.fillRect(ox, oy, tileSize, tileSize);
}
// assuming tile size is 256, draw grid
g.setColor(Color.GRAY);
int xy = 0;
while (xy <= tileSize) {
g.drawLine(ox + xy, oy, ox + xy, oy + tileSize);
g.drawLine(ox, oy + xy, ox + tileSize, oy + xy);
xy += 32;
}
} else if (tile.isLoaded()) {
g.drawImage(tile.getImage(), ox, oy, null);
} else {
// Use tile at higher zoom level with 200% magnification
MapTile superTile = factory.getMapTile(itpx / 2, itpy / 2, zoomLevel + 1);
if (superTile.isLoaded()) {
int offX = (itpx % 2) * tileSize / 2;
int offY = (itpy % 2) * tileSize / 2;
g.drawImage(superTile.getImage(), ox, oy, ox + tileSize, oy + tileSize, offX, offY, offX + tileSize / 2, offY + tileSize / 2,
null);
} else {
g.setColor(Color.GRAY);
g.fillRect(ox, oy, tileSize, tileSize);
}
}
}
}
}
}
}
private void doZoom(int newZoom, Point zoomCentreWorldBitmap) {
// get pixel offset between zoom position and control centre
Point2D center = mapi.getWorldBitmapMapCentre();
Point2D offset = new Point2D.Double(zoomCentreWorldBitmap.x - center.getX(), zoomCentreWorldBitmap.y - center.getY());
// convert position to fraction of map size
int zoom = mapi.getZoom();
int tileSize = factory.getTileSize(zoom);
Dimension mapSize = factory.getMapSize(zoom);
long mapWidthPixels = mapSize.width * tileSize;
long mapHeightPixels = mapSize.height * tileSize;
double x = zoomCentreWorldBitmap.getX() / mapWidthPixels;
double y = zoomCentreWorldBitmap.getY() / mapHeightPixels;
// get new zoom
newZoom = Math.max(newZoom, mapi.getMinZoom());
newZoom = Math.min(newZoom, mapi.getMaxZoom());
// get new map dimensions
tileSize = factory.getTileSize(newZoom);
mapSize = factory.getMapSize(newZoom);
mapWidthPixels = mapSize.width * tileSize;
mapHeightPixels = mapSize.height * tileSize;
// get zoom position in the new map world bitmap dimensions
Point newPosition = new Point((int) Math.abs(x * mapWidthPixels), (int) Math.abs(y * mapHeightPixels));
// get the new map centre
Point2D newCentre = new Point2D.Double(newPosition.getX() - offset.getX(), newPosition.getY() - offset.getY());
mapi.setView(newZoom, newCentre);
}
@Override
public void dispose() {
BackgroundTileFactorySingleton.getFactory().removeLoadedListener(this);
}
@Override
public void tileLoaded(MapTile tile) {
if (tile.getZoom() == mapi.getZoom()) {
repaint();
}
}
public void repaint(boolean repaintPluginOverlapOnly) {
// this.repaintPluginOverlapOnly = repaintPluginOverlapOnly;
if (repaintPluginOverlapOnly) {
pendingRepaintPluginOverlapOnly = true;
} else {
pendingFullRepaint = true;
}
super.repaint();
}
@Override
public void repaint() {
pendingFullRepaint = true;
super.repaint();
}
public boolean isDisablePaint() {
return disablePaint;
}
public void setDisablePaint(boolean disablePaint) {
this.disablePaint = disablePaint;
}
}