package uk.ac.leeds.ccg.geotools; import java.awt.*; import java.util.*; import uk.ac.leeds.ccg.geotools.projections.*; /** * Used to convert real world co-ordinates to and from screen co-ordinates. * Once set up with a geographic region and a screen region the scaler can * perform all the calculations needed to scale a point from one to the other. */ public final class Scaler extends java.awt.Component { private final boolean DEBUG=false; /** * The Coordinates of the full extent of the world */ GeoRectangle mapExtent; /** * The coordinates of the Graphics context */ Rectangle graphicsExtent; boolean Isotrophic = true; /** * The scale factor requried to transform the map data */ double scaleFactor=Double.NaN,xScaleFactor=Double.NaN,yScaleFactor=Double.NaN; /** * Listeners for changes in scale */ Vector listeners = new Vector(); /** * If not null then this is used to project co-ordinates */ Projection projection = null; //Constructs a new scaler; /** * Contructs an empty scaller, it WILL NOT work. */ public Scaler() { } /** * Construct a working scaler. * @param world A GeoRectangle that defines the bounds of the geographic region to map to the screen. * @param screen A Rectangle that defines the size of the on-screen area to map the 'world' into. */ public Scaler(GeoRectangle world,Rectangle screen){ graphicsExtent = screen; setMapExtent(world); calcScaleFactor(); if(DEBUG)System.out.println("New Scaler created "+scaleFactor); if(scaleFactor == Double.POSITIVE_INFINITY) { scaleFactor = 1.0d; } } public String toString(){ return(super.toString()+" "+scaleFactor); } /** * Changes the extent of the reagion of geographic interest * the scaler will recalcualte the scale factor needed to fit this * new region to the screen and subsequently notify all listeners of the * change in scale. * If a projection is in use then the extent will be projected before * the scale factor is calculated. * @param extent A GeoRectangle that defines the new region. */ public void setMapExtent(GeoRectangle extent){ setMapExtent(extent,false); } /** * Changes the extent of the reagion of geographic interest * the scaler will recalcualte the scale factor needed to fit this * new region to the screen and subsequently notify all listeners of the * change in scale ONLY IF keepQuite is false. * This keepQuite option is designed to stop infinate rescaling of * two viewers that are litening to each other for scale changes. * @param extent A GeoRectangle that defines the new region. * @param keepQuiet A boolean stating if the scaler should NOT tell listenres. */ public void setMapExtent(GeoRectangle extent,boolean keepQuiet){ mapExtent = project(extent); calcScaleFactor(keepQuiet); //notifyScaleChanged(); } public void setProjectedMapExtent(GeoRectangle extent){ mapExtent = extent; calcScaleFactor(false); } /** * Changes the extent of the reagion of the onscreen view in pixels * the scaler will recalcualte the scale factor needed to fit this * new region to the screen and subsequently notify all listeners of the * change in scale. * @param extent A Rectangle that defines the new region. */ public void setGraphicsExtent(Rectangle extent){ graphicsExtent = new Rectangle(extent); calcScaleFactor(true); //notifyScaleChanged(); } /** * Gets the geographic extent of the region currenly scaled to fit into * the on-screen region. * @return GeoRectangle the current map extent. */ public GeoRectangle getMapExtent(){ return unproject(mapExtent); } public GeoRectangle getProjectedMapExtent(){ return mapExtent; } private void calcScaleFactor(){ calcScaleFactor(false); } /** * Caclulates the scaling factor required to fit the current mapExtent into * the on-screen region. * Internal use only. */ private void calcScaleFactor(boolean keepQuiet){ //Set scaleFactor here... if(Isotrophic){ if (mapExtent.width / graphicsExtent.width > mapExtent.height / graphicsExtent.height) { scaleFactor = (double)mapExtent.width/ (double)graphicsExtent.width; }else{ scaleFactor = (double)mapExtent.height/ (double)graphicsExtent.height; } if(scaleFactor == Double.POSITIVE_INFINITY) { scaleFactor = 1.0d; if(DEBUG)System.out.println("Fixed"); } }else{ xScaleFactor = (double)mapExtent.width/ (double)graphicsExtent.width; if(DEBUG)System.out.println("X "+mapExtent.width+"/"+graphicsExtent.width+" "+ xScaleFactor); yScaleFactor = (double)mapExtent.height/ (double)graphicsExtent.height; if(DEBUG)System.out.println("Y "+mapExtent.height+"/"+graphicsExtent.height+" "+ yScaleFactor); if(xScaleFactor == Double.POSITIVE_INFINITY) { xScaleFactor = 1.0d; if(DEBUG)System.out.println("Fixed"); } if(yScaleFactor == Double.POSITIVE_INFINITY) { yScaleFactor = 1.0d; if(DEBUG)System.out.println("Fixed"); } } double[] far=toProj(graphicsExtent.width,0); double xshift,yshift,width,height; width=mapExtent.width; height=mapExtent.height; mapExtent.add(new GeoPoint(far[0],far[1])); xshift=(mapExtent.width-width)/2d; yshift=(mapExtent.height-height)/2d; mapExtent=new GeoRectangle(mapExtent.x-xshift,mapExtent.y-yshift,mapExtent.width,mapExtent.height); // System.out.println("Projected : "+mapExtent); // System.out.println("Unprojected: "+unproject(mapExtent)+"/n"); if(!keepQuiet){ if(DEBUG)System.out.println("Fireing scale changed event"); notifyScaleChanged();} } //will scale a single value //DO NOT USE TO SCALE A value that will be used in a co-ordinate /** * Converts a single geographic scaler value into on-screen units. * DO NOT USE TO SCALE a value that will be used in a CO-ORDINATE.
* If you wish to scale a point use the point version. * @param value The double to be scaled to on-screen units. * @return int The on-screen units equivelent of value. * @see toMap */ public int toGraphics(double value){ value /= scaleFactor; return (int)Math.round(value); } public final int toXGraphics(double value){ value /= xScaleFactor; return (int)value; } public final int toYGraphics(double value){ value /= yScaleFactor; return (int)value; } public final double[] project(double x,double y){ double p[] = new double[2]; if(projection==null){p[0] = x;p[1] = y;return p;} p = projection.project(x,y); return p; } public GeoRectangle project(GeoRectangle g){ if(projection==null){return new GeoRectangle(g);} return projection.projectedExtent(g); } public final double[] unproject(double x,double y){ double p[] = new double[2]; if(projection==null){p[0] = x;p[1]=y;return p;} p = projection.unproject(x,y); return p; } public GeoRectangle unproject(GeoRectangle g){ if(projection==null){return new GeoRectangle(g);} return projection.unprojectedExtent(g); } //can be used to scale a point /** * returns the screen co-ordinate equvelent of a geographic point. * It will both scale and TRANSLATE the point to the correct screen location. * therefore ONLY USE TO SCALE CO-ORDINATE PAIRS.
* @param x The double that makes up the x part of the co-ordinate * @param y The double that makes up the y part of the co-ordinate * @return int[] The scaled values of x and y, int[0]=scaledx,int[1]=scaledy; * @see toMap */ public int[] toGraphics(double x,double y){ if(Isotrophic){ double p[] = project(x,y); //project it x=p[0]; y=p[1]; int scaled[] = {0,0}; x -= mapExtent.x; x /= scaleFactor; y -= mapExtent.y; y /= scaleFactor; y = graphicsExtent.height - y;//Flip map the right way up. scaled[0] = (int)Math.round(x); scaled[1] = (int)Math.round(y); return scaled; }else{ int scaled[] = {0,0}; x -= mapExtent.x; x /= xScaleFactor; y -= mapExtent.y; y /= yScaleFactor; y = graphicsExtent.height - y;//Flip map the right way up. scaled[0] = (int)x; scaled[1] = (int)y; return scaled; } } /** * returns the screen co-ordinate equvelent of a geographic point. * @param p The GeoPoint to scale to screen co-ordinates. * @return int[] The scaled values of x and y, int[0]=scaledx,int[1]=scaledy; * @see toMap */ public int[] toGraphics(GeoPoint p){ return toGraphics(p.x,p.y); } /** * Converts a single screen units value into geographic unints. * DO NOT USE TO SCALE a value that will be used in a CO-ORDINATE.
* @param value The int of the number of onscreen units to scale to geographic units. * @return double The geographic equivelent of value. * @see toGraphics */ public double toMap(double value){ if(Isotrophic){ value *= scaleFactor; return value; }else{ /* this doesn't really work */ scaleFactor=Math.min(xScaleFactor,yScaleFactor); value *= scaleFactor; return value; } //throw new InternalError("bad scale call "); } /* * returns the geographic co-ordinate equvelent of an on-screen point. * It will both scale and TRANSLATE the point to the correct geographic location. * therefore ONLY USE TO SCALE CO-ORDINATE PAIRS.
* @param x The int that makes up the x part of the co-ordinate * @param y The int that makes up the y part of the co-ordinate * @return double[] The scaled values of x and y, int[0]=scaledx,int[1]=scaledy; * @see toGraphics */ public double[] toMap(int x,int y){ return toMap(x,y,true); } public double[] toProj(int x,int y){ return toMap(x,y,false); } public double[] toMap(int x,int y,boolean doProjection){ if(Isotrophic){ double scaled[] = {0,0}; double dx,dy; dx = (double)x; dy = (double)y; dx *= scaleFactor; dx += mapExtent.x; dy = graphicsExtent.height - y; dy *= scaleFactor; dy += mapExtent.y; //scaled[0] = dx; //scaled[1] = dy; if(doProjection){ scaled = unproject(dx,dy); }else{ scaled[0] = dx; scaled[1] = dy; } return scaled; }else{ double scaled[] = {0,0}; double dx,dy; dx = (double)x; dy = (double)y; dx *= xScaleFactor; dx += mapExtent.x; dy = graphicsExtent.height - dy; dy *= yScaleFactor; dy += mapExtent.y; scaled[0] = dx; scaled[1] = dy; return scaled; } } public void setProjection(Projection p){ projection = p; calcScaleFactor(); } //Sets the scale factor, not sure this should be available /** * Sets a new scale Factor, DO NOT USE. * This is a very low level call that should not be available.
* @param scaleFactor A double for the new scaling factor * @deprecated */ public void setScaleFactor(double scaleFactor) { this.scaleFactor = scaleFactor; } //Gets the scale factor, not sure if this is usefull /** * Gets the current scaling factor used by this scaler. * @return double the current scale factor */ public double getScaleFactor() { return this.scaleFactor; } /** * Any listeners added will be notified when ever the scale changes. * @param sce The ScaleChangedListener to add. */ public synchronized void addScaleChangedListener(ScaleChangedListener sce) { listeners.addElement(sce); } /** * Stop notification of scale change events to the given listener * @param sce The ScaleChangedListener to remove. */ public synchronized void removeScaleChangedListener(ScaleChangedListener sce) { listeners.removeElement(sce); } /** * Notifies all listeners of scale change event. */ protected void notifyScaleChanged() { Vector l; if(DEBUG)System.out.println("Scale notify"); ScaleChangedEvent sce = new ScaleChangedEvent(this,scaleFactor); synchronized(this) {l = (Vector)listeners.clone(); } /* if we do this in reverse order we actually tell the viewer * that sent the event first rather than last which I think * looks nicer if it takes any time at all to do the redraw * if you can't see the redraw then you won't notice, if you do * this way looks right in my code any way! Ian */ if(l.size()>0){ for (int i = l.size()-1;i>=0;--i) { if(DEBUG)System.out.println("notifying "+l.elementAt(i)); ((ScaleChangedListener)l.elementAt(i)).scaleChanged(sce); } } } public void SetIsotrophic(boolean f){ Isotrophic=f; calcScaleFactor(false); } /** * Convinence method for scaling whole polygons to screen co-ordinates. * After calling you may want to call GeoPolygon.toAWTPolygon(); * * @param poly the GeoPolygon to scale. * @return GeoPolygon scaled version of polygon. */ public GeoPolygon scalePolygon(GeoPolygon poly) { double x[] = new double[poly.xpoints.length]; double y[] = new double[poly.ypoints.length]; int p[] = {0,0}; for(int i = 0;i < poly.xpoints.length;i++) { p = toGraphics(poly.xpoints[i],poly.ypoints[i]); x[i] = p[0]; y[i] = p[1]; } return new GeoPolygon(poly.getID(),x,y,poly.getNPoints()); } }