/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.eas.client.controls.geopane;
import com.eas.client.controls.geopane.cache.TilesCache;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.concurrent.locks.ReadWriteLock;
import javax.swing.JPanel;
/**
* Component designed form fast multithreaded rendering of maps and schemes.
* The idea is to paint series of tiles across component's area.
* Tiles will be made by multitreaded renderer from geotools or netbeans visual libraries.
* Tiles will be cached by this component for reuse while painting.
* The cache will be cleared by any non-translating transformations.
* The approach is to translate screen grid instead of translating user's point of view
* whenever such behavour remains correct, i.e. while scale, rotate and shear transformations are absent
* and only translating is present.
* @author mg
*/
public class JTiledPane extends JPanel {
public static final String PLACE_HOLDER_LABEL_TEXT = "loadingLabelText";
public static final int WAIT_4_RENDERING_TIME = 4;
// tiles screen grid offset
protected Point offset = new Point(0, 0);
// map or schema background images, rendered vector figures and other heavyweight layers group.
protected TilesCache cache;
protected ReadWriteLock cacheLock;
// selection, signalig and other lightweight layers group tiles cache
protected TilesCache lightweightCache;
protected ReadWriteLock lightweightCacheLock;
// image placeholder of the absent tile
protected Image placeholderImage = null;
protected static final int SCREEN_INSET_SIZE = 1;
protected Rectangle selectionRectangle;
protected JTiledPane() {
super();
}
public JTiledPane(TilesCache aCache) {
this();
cache = aCache;
cache.setBackground(getBackground());
}
public JTiledPane(TilesCache aCache, TilesCache aLightweightCache) {
this(aCache);
lightweightCache = aLightweightCache;
lightweightCache.setImageType(BufferedImage.TYPE_INT_ARGB);
}
@Override
public void setBackground(Color bg) {
super.setBackground(bg);
if (cache != null) {
cache.setBackground(getBackground());
}
}
public TilesCache getCache() {
return cache;
}
public TilesCache getLightweightCache() {
return lightweightCache;
}
public void clearGridTranslating() {
offset.x = 0;
offset.y = 0;
}
public void translateGrid(int x, int y) throws Exception {
translateGrid(x, y, true);
}
public void translateGrid(int x, int y, boolean isLast) throws Exception {
offset.x -= x;
offset.y -= y;
invalidate();
}
public int getGridOffsetX() {
return offset.x;
}
public int getGridOffsetY() {
return offset.y;
}
public Rectangle getSelectionRectangle() {
return selectionRectangle;
}
public void setSelectionRectangle(Rectangle aRectangle) {
assert aRectangle == null || aRectangle.width >= 0;
assert aRectangle == null || aRectangle.height >= 0;
selectionRectangle = aRectangle;
}
@Override
public void paint(Graphics g) {
validate();
super.paint(g);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics tiledGraphics = g.create();
try {
if (placeholderImage == null) {
placeholderImage = generatePlaceholder(cache);
}
Dimension size = getSize();
// place the begining of coordinate system into center of the window and than
// translate it to last grid anchor
tiledGraphics.translate(size.width / 2 - offset.x, size.height / 2 - offset.y);
Rectangle clip = tiledGraphics.getClipBounds();
if (!clip.isEmpty()) {
Rectangle screen = new Rectangle(-size.width / 2, -size.height / 2, size.width, size.height);
screen.translate(offset.x, offset.y);
TilesBoundaries clipTiles = calcTilesBoundaries(clip);
TilesBoundaries screenTiles = calcTilesBoundaries(screen);
cache.setActualArea(screenTiles.expanded(SCREEN_INSET_SIZE));
lightweightCache.setActualArea(screenTiles.expanded(SCREEN_INSET_SIZE));
int cX = (clipTiles.minX + clipTiles.maxX) / 2;
int cY = (clipTiles.minY + clipTiles.maxY) / 2;
paintTile(cX, cY, clip, tiledGraphics);
paintTile(cX - 1, cY - 1, clip, tiledGraphics);
paintTile(cX - 1, cY, clip, tiledGraphics);
paintTile(cX, cY - 1, clip, tiledGraphics);
for (int x = clipTiles.minX; x <= clipTiles.maxX; x++) {
for (int y = clipTiles.minY; y <= clipTiles.maxY; y++) {
if (x == cX && y == cY || x == cX - 1 && y == cY - 1 || x == cX - 1 && y == cY || x == cX && y == cY - 1) {
} else {
paintTile(x, y, clip, tiledGraphics);
}
}
}
if (tiledGraphics instanceof Graphics2D) {
paintScreenContext((Graphics2D) tiledGraphics);
}
}
} finally {
tiledGraphics.dispose();
}
}
protected void paintScreenContext(Graphics2D g) {
if (selectionRectangle != null) {
g.translate(offset.x, offset.y);
g.setColor(Color.green.darker());
g.drawRect(selectionRectangle.x, selectionRectangle.y, selectionRectangle.width, selectionRectangle.height);
}
}
private void paintTile(int x, int y, Rectangle clip, Graphics g) {
Point ptKey = new Point(x, y);
Image image = cache.get(ptKey);
Image lightweightImage = lightweightCache.get(ptKey);
Rectangle controlRect = TileUtils.calcControlRect(x, y, clip, cache.getTileSize());
Rectangle imageRect = TileUtils.calcImageRect(x, y, clip, cache.getTileSize());
if (image == null) {
image = placeholderImage;
}
if (image != null) {
g.drawImage(image, controlRect.x, controlRect.y, controlRect.x + controlRect.width, controlRect.y + controlRect.height, imageRect.x, imageRect.y, imageRect.x + imageRect.width, imageRect.y + imageRect.height, null);
}
if (lightweightImage != null) {
g.drawImage(lightweightImage, controlRect.x, controlRect.y, controlRect.x + controlRect.width, controlRect.y + controlRect.height, imageRect.x, imageRect.y, imageRect.x + imageRect.width, imageRect.y + imageRect.height, null);
}
}
/**
* Calculates tiles numbers along corresponding axes.
* Let's assume tile size is 128 pixels.
* screen point (95, 10) will lead to tile point (0, 0).
* screen point (-5, -1) will lead to tile point (-1, -1).
* screen point (129, 10) will lead to tile point (1, 0).
* @param areaOfInterest Area, tile numbers will be calculated for.
* @return TilesBoundaries instance with calculated tiles numbers.
*/
protected TilesBoundaries calcTilesBoundaries(Rectangle areaOfInterest) {
TilesBoundaries bounds = new TilesBoundaries();
bounds.minX = Double.valueOf(Math.floor((double) areaOfInterest.x / (double) cache.getTileSize())).intValue();
bounds.maxX = Double.valueOf(Math.floor((double) (areaOfInterest.x + areaOfInterest.width) / (double) cache.getTileSize())).intValue();
bounds.minY = Double.valueOf(Math.floor((double) areaOfInterest.y / (double) cache.getTileSize())).intValue();
bounds.maxY = Double.valueOf(Math.floor((double) (areaOfInterest.y + areaOfInterest.height) / (double) cache.getTileSize())).intValue();
return bounds;
}
private Image generatePlaceholder(TilesCache aCache) {
String labelText = GeoPaneUtils.getString(PLACE_HOLDER_LABEL_TEXT);
Image img = new BufferedImage(aCache.getTileSize(), aCache.getTileSize(), aCache.getImageType());
Graphics g = img.getGraphics();
g.setColor(aCache.getBackground());
g.fillRect(0, 0, img.getWidth(null), img.getHeight(null));
g.setColor(Color.gray);
g.drawString(labelText, 0, aCache.getTileSize() / 2 - 5);
return img;
}
}