package sg.atom2d.tools.map; import java.awt.*; import javax.swing.*; import java.awt.event.*; import javax.swing.event.*; import java.util.*; /** * a JComponent view of the map. Supports setting tiles on a map. */ public class MapComponent extends JComponent implements MouseListener, MouseMotionListener, MapChangeListener { Map map; MapEdit mapEdit; private int width = 0; private int height = 0; private int tileWidth = 0; private int tileHeight = 0; private int activeLayer = 0; boolean hideLayers = false; boolean showGrid = true; boolean stateChanged = false; Stack undoStack; Stack redoStack; int grabX = 0; int grabY = 0; boolean dragged = false; int offsetX = 0; int offsetY = 0; JViewport viewport; public MapComponent(Map m, MapEdit me) { this.map = m; this.mapEdit = me; width = m.getWidth(); height = m.getHeight(); tileWidth = m.getZoomWidth(); tileHeight = m.getZoomHeight(); setPreferredSize(new Dimension(tileWidth*width, tileHeight*height)); this.addMouseListener(this); this.addMouseMotionListener(this); undoStack = new Stack(); redoStack = new Stack(); stateChanged = true; } public void setViewport(JViewport vp) { this.viewport = vp; } synchronized public void setMap(Map m) { this.map = m; width = m.getWidth(); height = m.getHeight(); tileWidth = m.getZoomWidth(); tileHeight = m.getZoomHeight(); setPreferredSize(new Dimension(tileWidth*width, tileHeight*height)); revalidate(); undoStack.clear(); redoStack.clear(); stateChanged = true; } void refreshZoom() { tileWidth = map.getZoomWidth(); tileHeight = map.getZoomHeight(); setPreferredSize(new Dimension(tileWidth*width, tileHeight*height)); revalidate(); repaint(); } /** * paints this component... basically renders the visible portion of the map * onto the given graphics context. * also draws a grid... */ synchronized public void paintComponent(Graphics g) { g.setColor(Color.white); g.fillRect(0,0,width*tileWidth, height*tileHeight); //as the tiles are drawn with the origin at the //bottom right, but the component's origin is the top left, //we need to set the offset so we can see the tiles if(hideLayers) { map.render(g, viewport.getViewPosition(), viewport.getSize(), activeLayer); } else { map.render(g, viewport.getViewPosition(), viewport.getSize()); } //map.render(g, -tileWidth, -tileHeight); if(showGrid) { g.setColor(Color.gray); for(int i=0; i<width; i++) { g.drawLine(i*tileWidth, 0, i*tileWidth, height*tileHeight); } for(int j=0; j<height; j++) { g.drawLine(0,j*tileHeight, width*tileWidth, j*tileHeight); } } ((Graphics2D)g).setStroke(new BasicStroke(2)); g.setColor(Color.black); g.drawLine(0, 0, width * tileWidth, 0); g.drawLine(0, 0, 0, height * tileHeight); g.drawLine(width * tileWidth, 0, width * tileWidth, height * tileHeight); g.drawLine(0, height * tileHeight, width * tileWidth, height * tileHeight); } /** * change the given tile to the one selected in the map editor. */ public void mapClicked(int x, int y) { x = x/tileWidth; y = y/tileHeight; if(x < map.getWidth() && x >= 0 && y < map.getHeight() && y >= 0) { if(mapEdit.getPaintMode() == MapEdit.PAINT_NORMAL) { map.setTile(x, y, activeLayer, mapEdit.getSelectedTile()); stateChanged = true; } else if(mapEdit.getPaintMode() == MapEdit.PAINT_FILL) { recursiveFlood(x, y, activeLayer, map.getTile(x, y, activeLayer), mapEdit.getSelectedTile()); } else { System.out.println("Invalid paint mode"); } } } /* Flood fill operation from http://en.wikipedia.org/wiki/Flood_fill * The section used is as follows: Most practical implementations use a loop for the west and east directions as an optimization to avoid the overhead of stack or queue management: 1. Set Q to the empty queue. 2. If the color of node is not equal to target-color, return. 3. Add node to Q. 4. For each element n of Q: 5. If the color of n is equal to target-color: 6. Set w and e equal to n. 7. Move w to the west until the color of the node to the west of w no longer matches target-color. 8. Move e to the east until the color of the node to the east of e no longer matches target-color. 9. Set the color of nodes between w and e to replacement-color. 10. For each node n between w and e: 11. If the color of the node to the north of n is target-color, add that node to Q. If the color of the node to the south of n is target-color, add that node to Q. 12. Continue looping until Q is exhausted. 13. Return. */ public void recursiveFlood(int x, int y, int layer, Tile target, Tile replacement) { if(x < 0 || x > map.getWidth() - 1 || y < 0 || y > map.getHeight() - 1) { return; } Tile node = map.getTile(x, y, layer); //1. If the color of node is not equal to target-color, return. if(Tile.areEqual(node, replacement) || !Tile.areEqual(node, target)) { return; } stateChanged = true; map.setTile(x, y, layer, replacement); /* new version. see method comment for description */ int left = x - 1;; int right = x + 1;; while(left >= 0 && Tile.areEqual(map.getTile(left, y, layer), target)) { map.setTile(left, y, layer, replacement); left -= 1; } while(right < map.getWidth() && Tile.areEqual(map.getTile(right, y, layer), target)) { map.setTile(right, y, layer, replacement); right += 1; } /* step back off the walls we have hit */ left++; right--; /* recursively do this operation above and below. Not so bad as the other one. * the deepest recursion level reachable is the map height. not great either. */ for(int i = left; i <= right; i++) { recursiveFlood(i, y-1, layer, target, replacement); recursiveFlood(i, y+1, layer, target, replacement); } } public void setActiveLayer(int layer) { if (layer >= 0 && layer <3) { activeLayer = layer; } } public int getActiveLayer() { return activeLayer; } boolean btn1Pressed = false; boolean btn2Pressed = false; int oldX = 0; int oldY = 0; public void mousePressed(MouseEvent e) { if(stateChanged) { saveUndoState(); stateChanged = false; } switch(e.getButton()) { case MouseEvent.BUTTON1: btn1Pressed = true; mapClicked(e.getX(), e.getY()); this.repaint(); break; /* default: btn2Pressed = true; Dimension d = viewport.getSize(); Point newPoint = new Point((int)(e.getX() - d.getWidth()/2), (int)(e.getY() - d.getHeight()/2)); viewport.setViewPosition(newPoint); viewport.repaint(); break; */ default: btn2Pressed = true; grabX = e.getX(); grabY = e.getY(); System.out.println("Grab at "+grabX+", "+grabY); break; } } public void mouseReleased(MouseEvent e) { switch(e.getButton()) { case MouseEvent.BUTTON1: btn1Pressed = false; oldX = e.getX(); oldY = e.getY(); break; default: btn2Pressed = false; oldX = e.getX(); oldY = e.getY(); if(!dragged) { Dimension d = viewport.getSize(); Point newPoint = new Point((int)(e.getX() - d.getWidth()/2), (int)(e.getY() - d.getHeight()/2)); viewport.setViewPosition(newPoint); } dragged = false; break; } } public void mouseEntered(MouseEvent e) { } public void mouseDragged(MouseEvent e) { if(btn1Pressed && mapEdit.getPaintMode() != MapEdit.PAINT_FILL) { mapClicked(e.getX(), e.getY()); //System.out.println(mapEdit.getSelectedTile()); this.repaint(); } else if(btn2Pressed) { int offX = e.getX() - grabX; int offY = e.getY() - grabY; Dimension d = viewport.getSize(); Point p = viewport.getViewPosition(); Point newPoint = new Point(p.x - offX, p.y - offY); viewport.setViewPosition(newPoint); dragged = true; } } public void mouseExited(MouseEvent e) { } public void mouseClicked(MouseEvent e) { } public void mouseMoved(MouseEvent e) { } public void setGrid(boolean grid) { showGrid = grid; } public void setHideLayers(boolean hl) { hideLayers = hl; } public void mapChanging(boolean major) { if(!major) { saveUndoState(); } else { clearUndoInfo(); } } public void mapChanged(boolean major) { repaint(); } /******** * Undo * ********/ public void clearUndoInfo() { redoStack.clear(); undoStack.clear(); } void saveUndoState() { redoStack.clear(); undoStack.push(map.toIntArray()); } void undo() { if(!undoStack.empty()) { redoStack.push(map.toIntArray()); int[][][] i = (int[][][])undoStack.pop(); map.setAllTiles(i, mapEdit.scene.tileset); } } void redo() { if(!redoStack.empty()) { undoStack.push(map.toIntArray()); int[][][] i = (int[][][])redoStack.pop(); map.setAllTiles(i, mapEdit.scene.tileset); } } }