www.pudn.com > GMapViewer-src.zip > GMapCanvas.java



package org.sreid.j2me.gmapviewer;

import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import org.sreid.j2me.util.*;

class GMapCanvas extends Canvas implements CommandListener {

	private static final String MAIN_MENU = "Back";
	private static final String ZOOM_IN = "Zoom in";
	private static final String ZOOM_OUT = "Zoom out";
	private static final String CREATE_MAP_PIN = "New map pin";
	private static final String MAP_PIN_MENU = "Map pin menu";
	private static final String SEARCH_MENU = "Search menu";

	private final GMapViewer app;

	// Font for map pin text
	private final Font font = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_PLAIN, Font.SIZE_SMALL);

	// Absolute position.
	private int x, y;

	// Zoom level. 0-14. Smaller is more zoomed in.
	private int zoom;

	private long lastMoveTime = 0;

	int requestID = 0; // used by MapTileManager.getMapTile

	GMapCanvas(GMapViewer app) {
		this.app = app;
		setCommandListener(this);
		addCommand(new Command(MAIN_MENU, Command.BACK, 1));
		addCommand(new Command(ZOOM_IN, Command.SCREEN, 1));
		addCommand(new Command(ZOOM_OUT, Command.SCREEN, 1));
		addCommand(new Command(CREATE_MAP_PIN, Command.SCREEN, 2));
		addCommand(new Command(MAP_PIN_MENU, Command.SCREEN, 2));
		addCommand(new Command(SEARCH_MENU, Command.SCREEN, 2));
	}

	void setXY(int x, int y) {
		this.x = x;
		this.y = y;
		app.mpm.sortPinsNearestFirst();
		repaint();
	}

	int getX() { return x; }
	int getY() { return y; }
	int getZ() { return zoom; }

	// Move view position (relative, 0 = no change)
	private void moveView(int movx, int movy) {
		long t = System.currentTimeMillis();
		boolean fast = (t < (lastMoveTime + app.prefs.getInt("moveSpeedTimeout", 800)));
		int speed = (fast ? app.prefs.getInt("moveSpeed2", 20) : app.prefs.getInt("moveSpeed1", 8));
		movx *= speed;
		movy *= speed;
		setXY(x + (movx << zoom), y + (movy << zoom));
		lastMoveTime = t;
	}


	private void zoomIn() {
		if (zoom > 0) zoom--;
		repaint();
	}

	private void zoomOut() {
		if (zoom < 14) zoom++;
		repaint();
	}

	public void paint(Graphics g) {
		// Convert x,y to screen coordinates
		final int w = getWidth();
		final int h = getHeight();
		final int sx = (x >> zoom) - (w / 2); // screen x position
		final int sy = (y >> zoom) - (h / 2); // screen y position
		final int xpos = sx >> 7;
		final int xoff = sx & 0x7f;
		final int ypos = sy >> 7;
		final int yoff = sy & 0x7f;
		//if (app.debug) app.debug("Position: " + x + " " + y + " -> " + xpos + " " + xoff + " " + ypos + " " + yoff + " " + zoom);

		// Number of tiles wide/high. Round up, and add 1 to handle overlap.
		final int tw = ((w + MapTile.WIDTH - 1) / MapTile.WIDTH) + 1;
		final int th = ((h + MapTile.HEIGHT - 1) / MapTile.HEIGHT) + 1;

		// Make sure image cache size is large enough that we don't
		// have to re-decode tiles on every refresh.
		app.cache.imageCacheSize = tw * th + 1;

		// Identify visible tiles
		XYZ[] visible = new XYZ[tw * th];
		int nVisible = 0;
		for (int ty = 0; ty < th; ty++) {
			for (int tx = 0; tx < tw; tx++) {
				XYZ xyz = new XYZ(xpos + tx, ypos + ty, zoom);
				if (computeTileVisiblePixels(xyz, sx, sy, w, h) > 0) {
					visible[nVisible++] = xyz;
				}
			}
		}

		// Sort tiles by how much space in the on-screen area it affects, 
		// so that we will request the most visible tiles first.
		Sort.sort(visible, 0, nVisible, new Comparator() {
			public int compare(Object o1, Object o2) {
				int v1, v2;
				// First, compare by visible pixels
				v1 = computeTileVisiblePixels((XYZ)o1, sx, sy, w, h);
				v2 = computeTileVisiblePixels((XYZ)o2, sx, sy, w, h);
				if (v1 != v2) return v2 - v1;
				// If visible pixels the same (probably fully visible), then compare by distance from center
				v1 = computeTileDistance((XYZ)o1, sx, sy, w, h);
				v2 = computeTileDistance((XYZ)o2, sx, sy, w, h);
				return v1 - v2;
			}
		});

		// Request/draw the tiles
		requestID++;
		for (int i = 0; i < nVisible; i++) {
			XYZ xyz = visible[i];
			MapTile mt = app.mtm.getMapTile(xyz);
			int tx = (xyz.x * MapTile.WIDTH) - ((xpos * MapTile.WIDTH) + xoff);
			int ty =  (xyz.y * MapTile.HEIGHT) - ((ypos * MapTile.HEIGHT) + yoff);
			Image image = mt.image; // take a reference now, in case background thread nulls out the field
			if (image != null) {
				// Got the image. Draw it.
				g.drawImage(image, tx, ty, Graphics.TOP|Graphics.LEFT);
			}
			else if (mt.bytes == null && mt.rmsID == -1) {
				if (mt.downloading) {
					// yellow to indicate downloading
					drawEmptyTile(g, 0xffff00, tx, ty);
				}
				else  {
					// orange to indicate waiting for download
					drawEmptyTile(g, 0xff8000, tx, ty);
				}
			}
			else { // Image is null, but does not need to be downloaded. That means we're waiting for decode.
				if (mt.decoding) {
					// green to indicate decoding
					drawEmptyTile(g, 0x00ff00, tx, ty);
				}
				else {
					// blue to indicate waiting for decode
					drawEmptyTile(g, 0x0000ff, tx, ty);
				}
			}
		}

		// Draw the map pins over top of the tiles.
		g.setFont(font);
		final int fudge = 100;
		final int PW = 16, PH = 16; // pin size
		final long mapPinTextRadius = app.prefs.getInt("mapPinTextRadius", 600);
		final long mapPinTextRadiusSquared = mapPinTextRadius * mapPinTextRadius;
		for (int i = app.mpm.getMapPinCount() - 1; i >= 0; i--) { // reverse order, so closer map pins will overwrite more distant ones
			MapPin pin = app.mpm.getMapPin(i);
			if (pin == null) continue;
			int px = (pin.x >> zoom) - sx;
			int py = (pin.y >> zoom) - sy;
			if (px >= (-fudge) && px <= (w + fudge) && py >= (-fudge) && py <= (h + fudge)) {
				// The pin is on-screen, or close enough. Draw it.
				g.setColor(pin.isPermanent ? 0xff0000 : 0x0000ff);
				g.fillArc(px - PW, py - PH, PW*2, PH*2, 90, 15);
				g.drawLine(px, py - PH, px, py); // draw a line too, so it always goes right to the point.
				// Draw text? (pythagorean theorem)
				int hdist = Math.abs(x - pin.x);
				int vdist = Math.abs(y - pin.y);
				if ( (hdist * hdist) + (vdist * vdist) < mapPinTextRadiusSquared) {
					g.setColor(0xffffff);
					g.fillRect(px + 1, py - PH, font.stringWidth(pin.text), font.getHeight());
					g.setColor(0x000000);
					g.drawString(pin.text, px + 1, py - PH, Graphics.TOP|Graphics.LEFT);
				}
			}
		}

		// Draw crosshairs
		g.setColor(0x808080);
		final int cs = 3; // crosshair size
		g.drawLine(w/2 - cs, h/2, w/2 + cs, h/2);
		g.drawLine(w/2, h/2 - cs, w/2, h/2 + cs);

		if (app.prefs.getInt("callgc", 0) >= 3) System.gc();
	}

	// Returns the number of pixels currently visible from the specified tile.
	// Return value ranges from 0 (completely off-screen) to 
	// MapTile.WIDTH*MapTile.HEIGHT (completely on-screen).
	private int computeTileVisiblePixels(XYZ tile, int sx, int sy, int w, int h) {
		// convert to pixels
		int tx = tile.x * MapTile.WIDTH;
		int ty = tile.y * MapTile.HEIGHT;

		// figure visible width, visible height
		int vw = Math.max(0, (
		 Math.min(tx + MapTile.WIDTH, sx + w) -
		 Math.max(tx, sx) ));
		int vh = Math.max(0, (
		  Math.min(ty + MapTile.HEIGHT, sy + h) -
		  Math.max(ty, sy) ));

		// area
		return vw * vh;
	}

	// Returns the distance (squared actually) of the center of the specified
	// tile from the center of the screen.
	private int computeTileDistance(XYZ tile, int sx, int sy, int w, int h) {
		// convert to pixels
		int tx = tile.x * MapTile.WIDTH;
		int ty = tile.y * MapTile.HEIGHT;

		// Compute horizontal and vertical distances
		int hdist = Math.abs( (sx + (w / 2)) - (tx + (MapTile.WIDTH  / 2)) );
		int vdist = Math.abs( (sy + (h / 2)) - (ty + (MapTile.HEIGHT / 2)) );

		// Pythagorean theorem
		return (hdist * hdist) + (vdist * vdist); // don't need longs here, as distance will always be small
	}

	private void drawEmptyTile(Graphics g, int color, int x, int y) {
		// Solid color
		g.setColor(color);
		g.fillRect(x, y, MapTile.WIDTH, MapTile.HEIGHT);
		// ...with a thin black border
		g.setColor(0x000000);
		g.drawRect(x, y, MapTile.WIDTH, MapTile.HEIGHT);
	}

	protected void keyPressed(int keyCode) {
		doKey(keyCode);
	}

	protected void keyRepeated(int keyCode) {
		doKey(keyCode);
	}

	private void doKey(int keyCode) {
		//app.debug("Key pressed: " + keyCode + "/" + getGameAction(keyCode) + "/" + getKeyName(keyCode));
		switch (getGameAction(keyCode)) {
			case UP:
				moveView(0, -1);
				break;
			case DOWN:
				moveView(0, 1);
				break;
			case LEFT:
				moveView(-1, 0);
				break;
			case RIGHT:
				moveView(1, 0);
				break;
		}
	}

	public void commandAction(Command c, Displayable d) {
		String cmd = c.getLabel();
		if (cmd.equals(ZOOM_IN)) {
			zoomIn();
		}
		if (cmd.equals(ZOOM_OUT)) {
			zoomOut();
		}
		if (cmd.equals(CREATE_MAP_PIN)) {
			createMapPin();
		}
		if (cmd.equals(MAP_PIN_MENU)) {
			app.mapPinMenu.cameFrom = this;
			app.mapPinMenu.loadList();
			app.display.setCurrent(app.mapPinMenu);
		}
		if (cmd.equals(SEARCH_MENU)) {
			app.searchMenu.cameFrom = this;
			app.searchMenu.loadList();
			app.display.setCurrent(app.searchMenu);
		}
		if (cmd.equals(MAIN_MENU)) {
			app.display.setCurrent(app.mainMenu);
		}
	}

	private void createMapPin() {
		final Dialog dlg = Dialog.createTextDialog("New map pin", "Map pin text", "", 40, TextField.ANY);
		dlg.setCallback(new Runnable() { public void run() {
			String userResponse = (String)dlg.getUserResponse();
			if (userResponse != null) {
				MapPin pin = new MapPin(x, y);
				pin.text = (String)userResponse;
				app.mpm.keepPin(pin);
			}
		}});
		dlg.showOn(app);
	}

	/** Loads position from preferences. */
	void loadPosition() {
		x = app.prefs.getInt("canvas.x", -2505728);
		y = app.prefs.getInt("canvas.y", -1282048);
		zoom = app.prefs.getInt("canvas.zoom", 6);
	}

	/** Saves position to preferences. */
	void savePosition() {
		app.prefs.put("canvas.x", Integer.toString(x));
		app.prefs.put("canvas.y", Integer.toString(y));
		app.prefs.put("canvas.zoom", Integer.toString(zoom));
	}		
}