/** * LandView.java * * Title: LandView * Description: Java version of the iPlan Australis Windows-IE-only LandView tool. * @author John Dalgliesh * @version v0.3 © John Dalgliesh 2004 */ package LandView; import java.awt.*; import java.awt.event.*; import java.applet.*; import java.awt.image.*; import java.util.*; import java.lang.Math; import java.net.*; import java.security.*; /** * Main applet class */ public class LandView extends java.applet.Applet { public LandView() { } public String getAppletInfo() { return "Applet Information"; } URLConnection keepAlive; public void init() { LandViewConfig lvc = new LandViewConfig( this ); // init lvc from applet parameters String val; if ((val = this.getParameter("longitude")) != null) lvc.longitude = Float.parseFloat(val); if ((val = this.getParameter("latitude")) != null) lvc.latitude = Float.parseFloat(val); if ((val = this.getParameter("scale")) != null) lvc.scale = Float.parseFloat(val); if ((val = this.getParameter("layers")) != null) { int valn = Integer.parseInt(val); for (int i = 0; i < lvc.layerCount; i++) lvc.layerVisible[i] = (((valn>>>i) & 1) == 1); } else lvc.layerVisible[0] = true; if ((val = this.getParameter("crosshairs")) != null) lvc.crosshairs = val.equals("true"); this.setLayout( new BorderLayout() ); LandCanvas can = new LandCanvas( this, lvc ); this.add( "Center", can ); LandControls con = new LandControls( can, lvc ); this.add( "West", con ); /* ... this doesn't work System.out.println( "Opening connection "+config.onlineRoot+"..." ); try { keepAlive = new URL( config.onlineRoot ).openConnection(); keepAlive.connect(); } catch (MalformedURLException e ) { } catch (java.io.IOException e ) { } System.out.println( "Connected." ); */ } // public void destroy() { this.remove( ... ); } public void setSize( int width, int height ) { super.setSize( width, height ); this.validate(); System.out.println( "w "+width+"h "+height ); } } /** * This class contains the configuration of */ class LandViewConfig { double longitude; double latitude; double scale; String onlineRoot; int layerCount; boolean layerVisible[]; class LayerSpec { String name; String sourceDir; char lastResChar; String imageFormat; LayerSpec( String n, String d, char lastRC, String f ) { name = n; sourceDir = d; lastResChar = lastRC; imageFormat = f; } } LayerSpec layerSpecs[]; static int MAX_LOADING_IMAGES = 2; boolean crosshairs; boolean doubleBuffer; LandViewConfig( Applet applet ) { this.resetClose(); onlineRoot = "http://iplan.australis.net.au/"; // should be static but I can't figure out how to get java to initialise it layerSpecs = new LayerSpec[] { new LayerSpec( "Satellite Photo", "db/lv/lic/aerial/", 'L', ".jpg" ), new LayerSpec( "Land Height", "db/lv/lic/elev/", 'G', ".jpg" ), new LayerSpec( "National Parks", "db/lv/lic/np/", 'H', ".png" ), new LayerSpec( "National Parks 2", "db/lv/np/np/", 'I', ".png" ), new LayerSpec( "Native Plants", "db/lv/np/natveg/", 'I', ".png" ), //*3 new LayerSpec( "State Forests", "db/lv/sf/sf/", 'I', ".png" ), new LayerSpec( "Mine Subsidence", "db/lv/lic/msd/", 'K', ".gif" ), new LayerSpec( "Soil Acidity", "db/lv/duap/acid/", 'H', ".png" ), new LayerSpec( "Rivers & Lakes", "db/lv/lic/drainage/", 'I', ".png" ), new LayerSpec( "Road, Rail, etc.", "db/lv/lic/transport/", 'J', ".png" ), new LayerSpec( "Suburbs", "db/lv/lic/suburb/", 'G', ".gif" ), new LayerSpec( "Local Councils", "db/lv/lic/lga/", 'J', ".gif" ), new LayerSpec( "Industrial Zones", "db/lv/duap/ozones/", 'L', ".png" ), new LayerSpec( "Ind+Bus Zones", "db/lv/duap/zones/", 'L', ".png" ), new LayerSpec( "Lots & Planning", "db/lv/lic/cad/", 'L', ".png" ), //*1 new LayerSpec( "Big Lot Numbers", "db/lv/lic/txt/", 'K', ".png" ), //*2 // Coastal Wetlands? }; // *1: half the images are broken, only showing bdrys plus not transparent // *2: L dirs exist but only 1/5th of them have files // *3: for the cumberland area of western sydney layerCount = layerSpecs.length; layerVisible = new boolean[layerCount]; for (int i = 0; i < layerCount; i++) layerVisible[i] = false; crosshairs = false; doubleBuffer = true; if (System.getProperty("os.name").equals( "Mac OS X" )) { // The JVM in Mac OS X has a big problem with name lookups when IPv6 // is enabled (which it is by default in Panther). try { String hostname = onlineRoot.substring(7,onlineRoot.length()-1); applet.showStatus( "Looking up '"+hostname+"'..." ); onlineRoot = "http://" + InetAddress.getByName( hostname ).getHostAddress() + "/"; } catch (Exception e) { } // Quartz always double-buffers for us so we needn't do so ourselves. doubleBuffer = false; } } void resetClose() { longitude = 633680.0; latitude = 68164.0; scale = 2.0; } void resetFar() { longitude = 633.7*2*500;//0; latitude = 32*32*500.0 - 442.8*2*500;//32*32*500.0; scale = 2048.0; } } /** * Stupid class to instantiate (not load) Images in another thread because * for some reason calling applet.getImage blocks for up to a few seconds * (even 'tho the spec says it returns immediately!) */ class ImageInstantiator extends Thread { LinkedList queue; ImageInstantiator() { queue = new LinkedList(); } public void run() { try { while (true) { ImageObserver look = null; synchronized (queue) { while (queue.isEmpty()) queue.wait(); look = (ImageObserver)queue.removeFirst(); } look.imageUpdate( null, 0, 4, 3, 2, 1 ); } } catch (InterruptedException e) { System.out.println( "image instantiator interrupted - stopping" ); } } public void add( ImageObserver ir ) { synchronized (queue) { queue.addLast( ir ); queue.notify(); } } public int size() { synchronized (queue) { return queue.size(); } } } /** * The canvas view */ class LandCanvas extends Canvas { Applet applet; LandViewConfig config; ImageInstantiator instantiator; class ImageRec implements ImageObserver { LandCanvas can; String name; // for debugging Image image; boolean ready; boolean touched; long lastUsed; ImageRec( LandCanvas canVal, String nameVal ) { can = canVal; name = nameVal; ready = false; touched = false; lastUsed = System.currentTimeMillis(); //this.imageInstantiate(); image = null; can.instantiator.add( this ); } boolean imageInstantiate() { if (name == null) return false; // already been destroyed System.out.println( "loading new image: " + name ); try { image = can.applet.getImage( /*can.applet.getDocumentBase(),*/ new URL(name) ); } catch (MalformedURLException e) { System.out.println( "unable to parse URL" ); } can.repaint(); return false; } public boolean/*callAgain*/ imageUpdate( Image inimg, int flags, int u0, int u1, int u2, int u3 ) { if (inimg == null && flags == 0 && u0 == 4 && u1 == 3 && u2 == 2 && u3 == 1) return this.imageInstantiate(); // we're being instantiated from the other thread if (inimg != image) return false; if ((flags & (ImageObserver.ABORT|ImageObserver.ERROR)) != 0) { System.out.println( "image '"+name+"' would not load" ); image = null; ready = true; can.repaint(); return false; } if ((flags & ImageObserver.ALLBITS) == 0) { // we need more, I tell you, MORE! return true; } System.out.println( "image '"+name+"' is now available" ); ready = true; can.repaint(); // ought to only repaint our portion of it return false; } Image getImage() { lastUsed = System.currentTimeMillis(); if (image != null) touched = true; return image; } boolean isLoading() { // touched false: // uninstantiated: image null, ready false // instantiated: image qqch, ready false // touched true: // loading: image qqch, ready false // loaded: image qqch, ready true // failed: image null, ready true return (image != null) && !ready && touched; } } class ImageKey implements Cloneable { int x; int y; public boolean equals( Object obj ) { ImageKey oth; try { oth = (ImageKey)obj; } catch (Exception e) { return false; } boolean ret = (oth.x == x && oth.y == y); return ret; } public int hashCode() { return ((y<<16) | (y>>16)) ^ x; } public Object clone() { ImageKey n = new ImageKey(); n.x = x; n.y = y; return n; } } class LayerQuilt { LandCanvas can; HashMap images; // hash from ImageKey to ImageRec double lonOrigin, latOrigin; double lonScale, latScale; char levelChar; String sourceDir; String imageFormat; float readyness; LayerQuilt( LandCanvas canVal, int scale, char levelCh, String dir, String format ) { can = canVal; images = new HashMap(); lonOrigin = 0; latOrigin = 32*32*500.0 - 500*(1< 32) { Object minKey = null; long minVal = ir.lastUsed; Iterator it = images.entrySet().iterator(); for (int i = 0; i < images.size(); i++) { Map.Entry me; try { me = (Map.Entry)it.next(); } catch (Exception e) { break; } if (((ImageRec)me.getValue()).lastUsed < minVal) { minVal = ((ImageRec)me.getValue()).lastUsed; minKey = me.getKey(); } } images.remove( minKey ); } } return ir; } String getImageName( ImageKey key ) // = 0; { String onlineRoot = can.config.onlineRoot; String onlineName = onlineRoot + sourceDir + levelChar+(key.x/32)+"x"+(key.y/32)+"/"+(key.x)+"x"+(key.y)+imageFormat; String localName = (key.x)+"x"+(key.y)+imageFormat; //System.out.println( "online "+onlineName+" offline "+localName ); //return localName; return onlineName; } void clear() { images.clear(); readyness = 0; System.out.println( "cleared quilt "+levelChar+" since it is no longer needed" ); } int imagesLoading() { int n = 0; Iterator it = images.values().iterator(); for (int i = 0; i < images.size(); i++) { ImageRec look; try { look = (ImageRec)it.next(); } catch (Exception e) { break; } if (look.isLoading()) n++; } return n; } } class Layer { LandCanvas can; LayerQuilt quilts[]; int nquilts; Layer( LandCanvas canVal, LandViewConfig.LayerSpec spec ) { can = canVal; int firstScale = 'L' - spec.lastResChar; nquilts = (spec.lastResChar - 'A') + 1; quilts = new LayerQuilt[nquilts]; for (int i = 0; i < nquilts; i++) quilts[i] = new LayerQuilt( can, firstScale+i, (char)(spec.lastResChar-i), spec.sourceDir, spec.imageFormat ); lastQuilt = null; lastQuiltStale = null; } LayerQuilt getQuilt( double scale ) { for (int i = 0; i < nquilts; i++) { if (scale < Math.abs(quilts[i].lonScale) / 250) { // min 250 pixels/block (don't annoy server too much) LayerQuilt q = quilts[i]; return q; } } System.out.println( "scale "+scale+" is too big, max "+ (Math.abs(quilts[nquilts-1].lonScale)/250) ); return null; } int imagesLoading() { int n = 0; for (int i = 0; i < nquilts; i++) n += quilts[i].imagesLoading(); return n; } // used by our caller to remember what it did last time LayerQuilt lastQuilt; LayerQuilt lastQuiltStale; } Layer layers[]; int imagesLoading; public LandCanvas( Applet a, LandViewConfig lvc ) { applet = a; config = lvc; instantiator = new ImageInstantiator(); instantiator.start(); this.setBackground( Color.white ); layers = new Layer[config.layerCount]; for (int i = 0; i < config.layerCount; i++) layers[i] = new Layer( this, config.layerSpecs[i] ); imagesLoading = 0; } Image bufferImage; Graphics bufferGraphics; Color bufferColour; boolean bufferImageValid() { return bufferImage != null && bufferImage.getWidth( null ) == this.getWidth() && bufferImage.getHeight( null ) == this.getHeight(); } public void update( Graphics g ) { // called from repaint if (!config.doubleBuffer) { super.update( g ); // clear whole rect then paint } else { if (!this.bufferImageValid()) { if (bufferGraphics != null) bufferGraphics.dispose(); if (bufferImage != null) bufferImage.flush(); bufferImage = this.createImage( this.getSize().width, this.getSize().height ); bufferGraphics = bufferImage.getGraphics(); bufferColour = new Color( 0.9f, 0.9f, 1.f ); } // clear it (slightly differently) bufferGraphics.setColor( bufferColour ); bufferGraphics.fillRect( 0, 0, this.getSize().width, this.getSize().height ); bufferGraphics.setColor( this.getForeground() ); // update offscreen image this.paintBuffer( bufferGraphics ); // blit offscreen image to screen g.drawImage( bufferImage, 0, 0, null ); } } public void paint( Graphics g ) { // called from exposed window if (!config.doubleBuffer) { this.paintBuffer( g ); } else { if (!this.bufferImageValid()) this.update( g ); // not valid, recreate image else g.drawImage( bufferImage, 0, 0, null ); // valid, reuse existing image } } public void paintBuffer( Graphics g ) { int numCurImagesReady = 0; int numCurImagesTotal = 0; boolean rerepaint = false; g.setColor( Color.blue ); Rectangle r = this.getBounds(); for (int i = 0; i < r.width + r.height; i+= 16) g.drawLine( 0, i, i, 0 ); // go through and find out how many images are loading, // as the necessary accounting is quite complicated imagesLoading = instantiator.size(); for (int i = 0; i < config.layerCount; i++) { if (layers[i] == null) continue; imagesLoading += layers[i].imagesLoading(); } boolean doneStale = false; for (int i = 0; i < config.layerCount; i++) { if (!config.layerVisible[i]) continue; LayerQuilt q = layers[i].getQuilt( config.scale ); // could blend with an adjacent quilts if it's close... if (q == null) continue; // if we haven't done the stale layer yet then do it first if (!doneStale) { // if the quilt has changed since last time, figure out a new stale quilt if (layers[i].lastQuilt != q) { if (layers[i].lastQuiltStale == null || layers[i].lastQuilt.readyness >= layers[i].lastQuiltStale.readyness) { if (layers[i].lastQuiltStale != null) layers[i].lastQuiltStale.clear(); layers[i].lastQuiltStale = layers[i].lastQuilt; } } // see if we have a stale quilt then if (layers[i].lastQuiltStale == null || layers[i].lastQuiltStale == q) { // nup, consider stale quilt done then doneStale = true; } else { // yup, do the stale one first then q = layers[i].lastQuiltStale; } } int numImagesReady = 0; int numImagesTotal = 0; // figure out the long and lat of the top left corner double corLon = config.longitude - (r.width/2)*config.scale; double corLat = config.latitude - (r.height/2)*config.scale; // get the key for the image overlapping the top left of the canvas ImageKey corKey = q.getImageKey( corLon, corLat ); // find out where that imag'es own top left is double imgLon = q.locImageKeyLon( corKey ); double imgLat = q.locImageKeyLat( corKey ); // turn that into pixels (should always be negative) int imgX = (int)Math.floor( (imgLon-corLon) / config.scale ); int imgY = (int)Math.floor( (imgLat-corLat) / config.scale ); // figure out which direction things go in int lonDir = (q.lonScale > 0) ? 1 : -1; int latDir = (q.latScale > 0) ? 1 : -1; // now go and draw all the images that overlap our canvas ImageKey curKey = new ImageKey(); curKey.y = corKey.y; int nowY = imgY; while (nowY < r.height) { // prepare for the next y line curKey.y += latDir; imgLat = q.locImageKeyLat( curKey ); int l8rY = (int)Math.floor( (imgLat-corLat) / config.scale ); //System.out.println( "l8rY "+l8rY+" imgLat "+imgLat ); curKey.y -= latDir; curKey.x = corKey.x; int nowX = imgX; while (nowX < r.width) { //System.out.println( "key x "+curKey.x+" key y "+curKey.y ); // prepare for the next x line curKey.x += lonDir; imgLon = q.locImageKeyLon( curKey ); int l8rX = (int)Math.floor( (imgLon-corLon) / config.scale ); //System.out.println( "l8rX "+l8rX ); curKey.x -= lonDir; // go and get the image at curKey then ImageRec ir = q.getImageRec( curKey, /*loadIfMissing:*/doneStale ); // and draw it if it is ready for that if (ir != null) // (could draw anyway in fact..) { boolean ready = false; Image image = ir.getImage(); if (image != null) ready = g.drawImage( image, nowX, nowY, l8rX-nowX, l8rY-nowY, ir ); if (ready || ir.ready) // may be ready even 'tho image broken { if (!ir.ready) { // it was ready without loading, so allow more to load, // since we will not get a repaint when loading finishes // (this happens when an image is pulled out of the cache) ir.ready = true; imagesLoading--; } numImagesReady++; } // first clip to the the bounds of the canvas (grumble..) /* // actually it was my bug after all ... d'oh! int sw = (int)Math.abs(q.lonScale), sh = (int)Math.abs(q.latScale); int sx1, sy1, sx2, sy2; int dw = l8rX-nowX, dh = l8rY-nowY; int dx1, dy1, dx2, dy2; if (nowX < 0) { dx1 = 0; sx1 = (0-nowX)*sw/dw; } else { dx1 = nowX; sx1 = 0; } if (nowY < 0) { dy1 = 0; sy1 = (0-nowY)*sh/dh; } else { dy1 = nowY; sy1 = 0; } if (l8rX > r.width) { dx2 = r.width; sx2 = sw-(l8rX-r.width)*sw/dw; } else { dx2 = l8rX; sx2 = sw; } if (l8rY > r.height) { dy2 = r.height; sy2 = sh-(l8rY-r.height)*sh/dh; } else { dy2 = l8rY; sy2 = sh; } g.drawImage( ir.image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null ); */ } numImagesTotal++; // move on to the next x line curKey.x += lonDir; nowX = l8rX; } // move on to the next y line curKey.y += latDir; nowY = l8rY; } // ok done with drawing that quilt now // record how much of it was ready q.readyness = ((float)numImagesReady) / ((float)numImagesTotal); // remember this one as the last quilt we did... if (doneStale) { layers[i].lastQuilt = q; doneStale = false; // set it up for the next layer if (q.readyness >= 1) // all ready, don't need stale one any more { if (layers[i].lastQuiltStale != null) { layers[i].lastQuiltStale.clear(); layers[i].lastQuiltStale = null; rerepaint = true; // for if stale quilt was transparent } } numCurImagesReady += numImagesReady; numCurImagesTotal += numImagesTotal; } // ... or as the last stale one if stale hadn't already been done else { doneStale = true; // now it has been done i--; // I wanna go again! } } if (config.crosshairs) { g.setColor( Color.red ); int midX = r.width/2; int midY = r.height/2; g.drawLine( midX-16, midY-16, midX+16, midY+16 ); g.drawLine( midX+16, midY-16, midX-16, midY+16 ); } if (numCurImagesTotal == 0) numCurImagesTotal = 1; // no divide by zero applet.showStatus( "longitude: "+config.longitude+ " latitude: "+config.latitude+ " scale: "+config.scale+ " loaded: "+(numCurImagesReady*100/numCurImagesTotal)+"%" ); if (rerepaint) this.repaint(); } } /** * The strip of buttons and other controls */ class LandControls extends Panel implements ActionListener, ItemListener, MouseListener, MouseMotionListener { LandViewConfig config; LandCanvas canvas; Button zoomInBut, zoomOutBut, resetCloseBut, resetFarBut; Checkbox doubleBufferBox, layerToggles[]; public LandControls( LandCanvas can, LandViewConfig lvc ) { canvas = can; config = lvc; this.setLayout( new BorderLayout() ); Panel p; p = new Panel(); p.setLayout( new VerticalLayout() ); p.add( new Label( "Controls: " ) ); zoomInBut = new Button("Zoom In"); zoomInBut.addActionListener( this ); p.add( zoomInBut ); zoomOutBut = new Button("Zoom Out"); zoomOutBut.addActionListener( this ); p.add( zoomOutBut ); resetCloseBut = new Button("Reset Near"); resetCloseBut.addActionListener( this ); p.add( resetCloseBut ); resetFarBut = new Button("Reset Far"); resetFarBut.addActionListener( this ); p.add( resetFarBut ); doubleBufferBox = new Checkbox( "Anti-flicker", config.doubleBuffer ); doubleBufferBox.addItemListener( this ); p.add( doubleBufferBox ); this.add( p, BorderLayout.NORTH ); int nlayers = config.layerSpecs.length; int checkboxHeight = 18; if (System.getProperty("os.name").equals( "Mac OS X" )) checkboxHeight = 28; else checkboxHeight = 24; // I can't find any way to get the height out programatically!!! aaargh! p = new Panel(); p.setLayout( new GridLayout( 1+nlayers, 1, 0, 18-checkboxHeight ) ); p.add( new Label( "Layers:" ) ); layerToggles = new Checkbox[nlayers]; for (int i = 0; i < nlayers; i++) { Checkbox c = new Checkbox( config.layerSpecs[i].name ); layerToggles[i] = c; c.setState( config.layerVisible[i] ); c.addItemListener( this ); p.add( layerToggles[i] ); } this.add( p, BorderLayout.SOUTH ); this.setEnabled( true ); canvas.addMouseListener( this ); canvas.addMouseMotionListener( this ); } public void actionPerformed( ActionEvent ev ) { Object s = ev.getSource(); if (s == zoomInBut) { config.scale /= 2; if (config.scale < 0.5) config.scale = 0.5; canvas.repaint(); } else if (s == zoomOutBut) { config.scale *= 2; if (config.scale > 1000000000.0) config.scale = 1000000000.0; // say canvas.repaint(); } else if (s == resetCloseBut) { config.resetClose(); canvas.repaint(); } else if (s == resetFarBut) { config.resetFar(); canvas.repaint(); } } public void itemStateChanged( ItemEvent ev ) { Object s = ev.getSource(); for (int i = 0; i < layerToggles.length; i++) { if (s != layerToggles[i]) continue; config.layerVisible[i] = layerToggles[i].getState(); canvas.repaint(); return; } if (s == doubleBufferBox) { config.doubleBuffer = doubleBufferBox.getState(); canvas.repaint(); return; } } boolean dragging; boolean altWasDown; Point downPoint; public void mousePressed( MouseEvent e ) { dragging = false; altWasDown = e.isAltGraphDown() | e.isShiftDown(); downPoint = e.getPoint(); } public void mouseReleased( MouseEvent e ) { if (dragging) return; // it was a drag not a click // it's a zoom Point p = e.getPoint(); Rectangle r = ((Canvas)e.getSource()).getBounds(); int xoff = p.x-(r.width/2); int yoff = p.y-(r.height/2); // after the op, we want xoff and yoff to be at the same place double oscale = config.scale; if (!altWasDown) { // zoom in config.scale /= 1.5; if (config.scale < 0.5) config.scale = 0.5; } else { // zoom out config.scale *= 1.5; if (config.scale > 1000000000.0) config.scale = 1000000000.0; // say System.out.println("scale "+config.scale); } config.longitude += xoff * (oscale-config.scale); config.latitude += yoff * (oscale-config.scale); canvas.repaint(); } public void mouseClicked( MouseEvent e ) { } public void mouseEntered( MouseEvent e ) { } public void mouseExited( MouseEvent e ) { } public void mouseDragged( MouseEvent e ) { dragging = true; Point nextPoint = e.getPoint(); config.longitude -= (nextPoint.x - downPoint.x) * config.scale; config.latitude -= (nextPoint.y - downPoint.y) * config.scale; downPoint = nextPoint; canvas.repaint(); } public void mouseMoved( MouseEvent e ) { } } /** * A class to make a vertical non-resizing layout (you'd think it was simple...) */ class VerticalLayout extends GridBagLayout { VerticalLayout() { this.defaultConstraints.fill = GridBagConstraints.NONE; this.defaultConstraints.gridwidth = GridBagConstraints.REMAINDER; this.defaultConstraints.anchor = GridBagConstraints.WEST; } }