001: //=============================================================================
002: //=== Copyright (C) 2001-2007 Food and Agriculture Organization of the
003: //=== United Nations (FAO-UN), United Nations World Food Programme (WFP)
004: //=== and United Nations Environment Programme (UNEP)
005: //===
006: //=== This program is free software; you can redistribute it and/or modify
007: //=== it under the terms of the GNU General Public License as published by
008: //=== the Free Software Foundation; either version 2 of the License, or (at
009: //=== your option) any later version.
010: //===
011: //=== This program is distributed in the hope that it will be useful, but
012: //=== WITHOUT ANY WARRANTY; without even the implied warranty of
013: //=== MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: //=== General Public License for more details.
015: //===
016: //=== You should have received a copy of the GNU General Public License
017: //=== along with this program; if not, write to the Free Software
018: //=== Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
019: //===
020: //=== Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
021: //=== Rome - Italy. email: geonetwork@osgeo.org
022: //==============================================================================
023:
024: package org.wfp.vam.intermap.kernel.map.images;
025:
026: import java.awt.*;
027:
028: import java.awt.image.BufferedImage;
029: import org.wfp.vam.intermap.kernel.map.mapServices.BoundingBox;
030:
031: /**
032: * @author ETj
033: */
034: public class ScaleBar {
035: public static final float EARTH_RADIUS_KM = 6371;
036:
037: /**
038: * Build a scalebar image.
039: *
040: * @param lat the map latitude
041: * @param westBL the map westBL
042: * @param eastBL the map eastBL
043: * @param mapwidth the width of the map in pixel
044: * @param initscalewidth an approximative length in pixel of the scale bar. It will be increased to reach a nice number for the scale.
045: *
046: * @return The scalebar image
047: *
048: */
049: static public BufferedImage getScaleBar(BoundingBox bb,
050: int mapwidth, int initscalewidth) {
051: double mapKMwidth = getMapRad(bb) * EARTH_RADIUS_KM;
052: System.out
053: .println("MAP LENGTH (" + bb + ") ---> " + mapKMwidth);
054: ScaleBarInfo sbw = getNormalizedScalebar(mapKMwidth, mapwidth,
055: initscalewidth);
056: System.out.println("PREF SCALEBAR WDT " + initscalewidth
057: + " --> " + sbw.px);
058: System.out.println("SCALEBAR " + sbw.kmmant + "E" + sbw.kmexp);
059: String kmstr = getKMString(sbw.kmexp, sbw.kmmant);
060:
061: return buildScaleBarImage(sbw.px, kmstr);
062: }
063:
064: /**
065: * Method getKMString
066: *
067: * @param kmexp an int
068: * @param p1 an int
069: *
070: * @return a String
071: */
072: private static String getKMString(int exp, int mant) {
073: StringBuffer sb = new StringBuffer();
074: if (exp >= 0) {
075: sb.append(mant);
076: for (int i = 0; i < exp; i++)
077: sb.append("0");
078: sb.append(" Km");
079: } else {
080: sb.append("This map is really small!"); // FIXME
081: }
082:
083: return sb.toString();
084: }
085:
086: /**
087: * Method getScalebarWidth
088: *
089: * @param mapKMwidth a double
090: * @param mapwidth an int
091: * @param initscalewidth an approximative length in pixel of the scale bar. It will be increased to reach a nice number for the scale.
092: *
093: * @return an int
094: */
095: private static ScaleBarInfo getNormalizedScalebar(double mapKM,
096: int mapPX, int prefSBpx) {
097: double initsbw = mapKM / mapPX * prefSBpx; // length in km of the proposed scalebar width
098: System.out.println("SB proposed len KM " + initsbw);
099:
100: double[] norm = exp(initsbw);
101: int quant = (int) Math.ceil(norm[1]);
102:
103: int sbkm = (int) (quant * Math.pow(10.0, norm[0]));
104: int sbpx = (int) (mapPX * sbkm / mapKM);
105:
106: return new ScaleBarInfo((int) norm[0], quant, sbpx);
107: }
108:
109: static class ScaleBarInfo {
110: int kmexp;
111: int kmmant;
112: int px;
113:
114: public ScaleBarInfo(int kmexp, int kmmant, int px) {
115: this .kmexp = kmexp;
116: this .kmmant = kmmant;
117: this .px = px;
118: }
119: }
120:
121: /**
122: * Computes exponent and mantissa.
123: *
124: * @return a double[] {exp, mant}
125: */
126: private static double[] exp(double mant) {
127: if (mant <= 0)
128: throw new IllegalArgumentException("" + mant);
129:
130: double exp = 0;
131:
132: if (mant > 1)
133: while (mant > 10) {
134: exp++;
135: mant /= 10;
136: }
137: else
138: while (mant < 1) {
139: exp--;
140: mant *= 10;
141: }
142:
143: return new double[] { exp, mant };
144: }
145:
146: /**
147: * Great Circle Distances
148: * The great circle distance between two points is often difficult to measure on a globe and,
149: * in general, cannot be measured accurately on a map due to distortion introduced in representing
150: * the approximately spherical geometry of the Earth on a flat map.
151: * However, great circle distances can be calculated easily
152: * given the latitudes and longitudes of the two points,
153: * using the following formula from spherical trigonometry:
154: *
155: * [Law of Cosines for Spherical Trigonometry]
156: * cos D = ( sin a )(sin b) + (cos a)(cos b)(cos P)
157: *
158: * where:
159: *
160: * D is the angular distance between points A and B
161: * a is the latitude of point A
162: * b is the latitude of point B
163: * P is the longitudinal difference between points A and B
164: *
165: * In applying the above formula, south latitudes and west longitudes
166: * are treated as negative angles. Once cos D has been calculated,
167: * the angle D can be determined using the ARCOS function.
168: *
169: * The distance in km is obtained by multiplying the angle D in degrees by 111 km
170: *
171: * @param lat a float
172: * @param westBL a float
173: * @param eastBL a float
174: *
175: * @return the lenght between the two points in kilometers
176: */
177: private static double getMapLength_LawOfCosines(BoundingBox bb) {
178: float lat;
179:
180: if (bb.getNorth() * bb.getSouth() < 0)
181: lat = 0; // take the equator when it is inside the map
182: else
183: lat = Math.min(Math.abs(bb.getNorth()), Math.abs(bb
184: .getSouth())); // take the more meaningful one
185:
186: // This method is mathematically bugged
187: // FIXME Furthermore, if W*E<0, we should compute
188: // DIST(w,e) = DIST(W,0) + DIST(0,E)
189: // or we get the MINOR distance between the two points
190: // (think for instance w=-179,e=179)
191:
192: double sina = Math.sin(Math.toRadians(lat));
193: double cosa = Math.cos(Math.toRadians(lat));
194: double cosd = sina * sina + cosa * cosa
195: * Math.cos(Math.toRadians(bb.getLongDiff()));
196: double d = Math.acos(cosd);
197: System.out.print(" --- SIN(LAT=" + lat + ")=" + sina);
198: System.out.print(" --- COS(LAT=" + lat + ")=" + cosa);
199: System.out.println(" --- COSD=" + cosd);
200: return Math.toDegrees(d) * 111;
201: }
202:
203: /**
204: * Presuming a spherical Earth with radius R (see below),
205: * and the locations of the two points in spherical coordinates(longitude and latitude)
206: * are lon1,lat1 and lon2,lat2
207: * then the
208: * Haversine Formula (from R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159):
209: * will give mathematically and computationally exact results.
210: *
211: * The intermediate result c is the great circle distance in radians.
212: * The great circle distance d will be in the same units as R.
213: *
214: * dlon = lon2 - lon1
215: * dlat = lat2 - lat1
216: * a = sin^2(dlat/2) + cos(lat1) * cos(lat2) * sin^2(dlon/2)
217: * c = 2 * arcsin(min(1,sqrt(a)))
218: * d = R * c
219: */
220: private static double getMapRad_Haversine(double nrad, double erad,
221: double srad, double wrad) {
222: double dlon = nrad - srad;
223: double dlat = wrad - erad;
224:
225: double sindlat2 = Math.sin(dlat / 2);
226: double sindlon2 = Math.sin(dlon / 2);
227: double a = sindlat2 * sindlat2 + Math.cos(srad)
228: * Math.cos(nrad) * sindlon2 * sindlon2;
229: double c = 2 * Math.asin(Math.min(1, Math.sqrt(a)));
230:
231: System.out.print(" --- SIN(DLAT=" + dlat + "/2)=" + sindlat2);
232: System.out.println(" --- COS(DLON=" + dlon + "/2)=" + sindlon2);
233: System.out.print(" --- A=" + a);
234: System.out.println(" --- C=" + c);
235:
236: return c;
237: }
238:
239: private static double getMapRad(BoundingBox bb) {
240: double n, s;
241: double e = Math.toRadians(bb.getEast());
242: double w = Math.toRadians(bb.getWest());
243:
244: if (bb.getNorth() * bb.getSouth() < 0) {
245: // take the equator when it is inside the map
246: n = s = 0;
247: } else
248: n = s = Math.toRadians(Math.min(Math.abs(bb.getNorth()),
249: Math.abs(bb.getSouth()))); // take the more meaningful one
250:
251: if (e * w < 0) {
252: // compute 2 semi-arcs
253: double rad1 = getMapRad_Haversine(n, 0, s, w);
254: double rad2 = getMapRad_Haversine(n, e, s, 0);
255:
256: return rad1 + rad2;
257: } else {
258: return getMapRad_Haversine(n, e, s, w);
259: }
260: }
261:
262: /**
263: * Build the image
264: *
265: * @param px an int
266: * @param kmstr a String
267: *
268: * @return a BufferedImage
269: */
270: private static BufferedImage buildScaleBarImage(int px, String text) {
271: Font font = new Font("helvetica", Font.BOLD, 15);
272: Color bg = Color.WHITE;
273: Color fg = Color.BLACK;
274: int paddingleft = 5;
275: int paddingbottom = 5;
276: int dist = 3;
277: int sbheight = 10;
278:
279: FontMetrics fm = Toolkit.getDefaultToolkit().getFontMetrics(
280: font);
281: int fw = fm.stringWidth(text) + 4;
282: int fh = fm.getHeight() + 2;
283:
284: int width = paddingleft + Math.max(fw, px);
285: int height = fh + dist + sbheight + paddingbottom;
286:
287: //System.out.println("Creating label image '"+text+"' "+w+"x"+h);
288:
289: BufferedImage img = new BufferedImage(width, height,
290: BufferedImage.TYPE_INT_ARGB);
291: Graphics2D g2d = img.createGraphics();
292:
293: g2d.setFont(font);
294: // g2d.setColor(bg);
295: // // paddingleft + shadow + offset
296: // g2d.drawString(text, paddingleft +1 +0, fm.getAscent() -1 );
297: // g2d.drawString(text, paddingleft +1 +1, fm.getAscent() -1 );
298: // g2d.drawString(text, paddingleft +1 +1, fm.getAscent() -0 );
299: // g2d.drawString(text, paddingleft +1 +1, fm.getAscent() +1 );
300: // g2d.drawString(text, paddingleft +1 +0, fm.getAscent() +1 );
301: // g2d.drawString(text, paddingleft +1 -1, fm.getAscent() +1 );
302: // g2d.drawString(text, paddingleft +1 -1, fm.getAscent() -0 );
303: // g2d.drawString(text, paddingleft +1 -1, fm.getAscent() -1 );
304:
305: g2d.setColor(fg);
306: g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
307: RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
308: g2d.drawString(text, paddingleft + 1, fm.getAscent());
309:
310: g2d.setColor(bg);
311: g2d.fillRect(paddingleft, fh + dist - 1, px + 2, sbheight + 2);
312:
313: g2d.setColor(fg);
314: g2d.fillRect(paddingleft + 1, fh + dist, px, sbheight);
315:
316: return img;
317: }
318:
319: }
|