/* GNU General Public License CacheWolf is a software for PocketPC, Win and Linux that enables paperless caching. It supports the sites geocaching.com and opencaching.de Copyright (C) 2006 CacheWolf development team See http://www.cachewolf.de/ for more information. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package CacheWolf.navi; import CacheWolf.Preferences; import CacheWolf.database.BoundingBox; import CacheWolf.database.CWPoint; import CacheWolf.database.CoordinatePoint; import CacheWolf.utils.Common; import CacheWolf.utils.Matrix; import CacheWolf.utils.MyLocale; import ewe.fx.Point; import ewe.io.BufferedWriter; import ewe.io.FileInputStream; import ewe.io.FileWriter; import ewe.io.IOException; import ewe.io.InputStreamReader; import ewe.io.PrintWriter; import ewe.sys.Convert; import ewe.util.mString; // World file: // x scale affine[1] // y scale affine[2] // x rotation affine[0] // y rotation affine[3] // lon of upper left corner of image // lat of upper left corner of image // lon of lower right corner of image // lat of lower right corner of image /** * class to read, save and do the calculations for calibrated and calibrating maps * start offset for language file: 4300 * * @author pfeffer * */ public class MapInfoObject extends BoundingBox { public CWPoint center = new CWPoint(); public float sizeKm = 0; // Diagonal in meters per pixel public float scale; // identifying the scale of the map, automatically adjusted when zooming public float zoomFactor = 1; // if the image is zoomed else 1 public Point shift = new Point(0, 0); public CWPoint origAffineUpperLeft; // this is only valid after zooming public float rotationRad; // contains the rotation of the map == north direction in rad private MapImageFileNameObject mapImageFileNameObject; private byte mapType = 0; // 0=aus wfl-file, 1=empty map, 2=aus Tilestructure, 3=aus pack-file private final double[] affine = { 0, 0, 0, 0 }; private CWPoint affineTopleft = new CWPoint();; private double transLatX, transLatY, transLonX, transLonY; // needed for the inverse calculation from lat/lon to x/y private int coordTrans = 0; public MapInfoObject() { this.mapImageFileNameObject = new MapImageFileNameObject("", "", ""); } public MapInfoObject(MapInfoObject mapInfoObject) { super(mapInfoObject.topleft, mapInfoObject.bottomright); this.mapImageFileNameObject = mapInfoObject.mapImageFileNameObject; affine[0] = mapInfoObject.affine[0]; affine[1] = mapInfoObject.affine[1]; affine[2] = mapInfoObject.affine[2]; affine[3] = mapInfoObject.affine[3]; origAffineUpperLeft = new CWPoint(mapInfoObject.origAffineUpperLeft); affineTopleft = new CWPoint(mapInfoObject.affineTopleft); zoomFactor = mapInfoObject.zoomFactor; shift.set(mapInfoObject.shift); coordTrans = mapInfoObject.coordTrans; doCalculations(); this.mapType = mapInfoObject.mapType; } /** * constructes an MapInfoObject without an associated map but with 1 Pixel = scale meters */ public MapInfoObject(double scalei, double lat, MapImageFileNameObject mapImageFileNameObject) { super(new CWPoint(1, 0), new CWPoint(0, 1)); this.mapImageFileNameObject = mapImageFileNameObject; final double meters2deg = 1 / (1000 * (new CWPoint(0, 0)).getDistance(new CWPoint(1, 0))); final double pixel2deg = meters2deg * scalei; affine[0] = 0; // x2lat affine[1] = pixel2deg / java.lang.Math.cos(lat * java.lang.Math.PI / 180); // x2lon affine[2] = -pixel2deg; // y2lat affine[3] = 0; // y2lon topleft.latDec = 1; // top topleft.lonDec = 0; // left bottomright.latDec = 0; // bottom bottomright.lonDec = 1; // right affineTopleft.set(topleft); doCalculations(); origAffineUpperLeft = new CWPoint(affineTopleft); this.mapType = 1; } /** * using wfl-file */ public MapInfoObject(MapImageFileNameObject mapImageFileNameObject) throws IOException, ArithmeticException { super(); this.mapImageFileNameObject = mapImageFileNameObject; this.loadWFL(); this.mapType = 0; } // used by download Tile und calc easyFindString in generation MapsList /** * using Tile - Infos x, y, zoom * Tile must have Size of 256 * 256 */ public MapInfoObject(int x, int y, int zoom, MapImageFileNameObject mapImageFileNameObject) { super(); calcTile(x, y, zoom); this.mapImageFileNameObject = mapImageFileNameObject; this.mapType = 2; // oder 3 } /** * using mapListEntry * */ public MapInfoObject(MapListEntry mapListEntry) { super(); // creates this.topleft and this.bottomright String p[]; int zoom; int x; int y; // this.mapType = mapListEntry.mapType; this.mapImageFileNameObject = mapListEntry.getMapImageFileNameObject(); // switch (mapListEntry.mapType) { case 1: Preferences.itself().log("create MapInfoObject from MapListEntry should never be called for empty map. the MapInfoObject should be created on creation of MapListEntry"); break; case 2: p = ewe.util.mString.split(mapImageFileNameObject.getPath(), '/'); zoom = Common.parseInt(p[p.length - 3]); x = Common.parseInt(p[p.length - 2]); y = Common.parseInt(mapImageFileNameObject.getMapName()); calcTile(x, y, zoom); mapImageFileNameObject.setImageExtension(Common.getExtension(Common.getImageName(Preferences.itself().absoluteMapsBaseDir + mapImageFileNameObject.getPath() + mapImageFileNameObject.getMapName()))); break; case 3: p = ewe.util.mString.split(mapImageFileNameObject.getMapName(), '!'); zoom = Common.parseInt(p[4]); x = Common.parseInt(p[5]); y = Common.parseInt(p[6]); calcTile(x, y, zoom); break; default: // 0 try { this.loadWFL(); } catch (Exception e) { } break; } mapListEntry.map = this; } private CWPoint Tile2LatLon(int x, int y, int zoom) { double xx = x; double yy = y; // double zz = zoom; double zz2 = Math.pow(2, zoom); double lon = xx / zz2 * 360.0 - 180.0; double n = Math.PI - (2.0 * Math.PI * yy) / zz2; double lat = (Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))) * 180.0 / Math.PI; return new CWPoint(lat, lon); } /** * * @param n * without ".wfl" * @return name of the map including fast-find-prefix */ public String createMapName(String n) { return getFfPrefix() + n; } /** * @return the filename of the associated map image<br> * null : the referenced file doesn't exist<br> * "" = empty map -> no file is associated<br> */ public String getImagePathAndName() { if (this.mapType == 1) return ""; // 1=empty map else if (this.mapType == 3) { return Preferences.itself().absoluteMapsBaseDir + this.mapImageFileNameObject.getPath() + ".pack!" + this.mapImageFileNameObject.getMapName(); } else { if (this.mapImageFileNameObject.getImageExtension().length() > 0) { return Preferences.itself().absoluteMapsBaseDir + this.mapImageFileNameObject.getPath() + this.mapImageFileNameObject.getMapName() + this.mapImageFileNameObject.getImageExtension(); } else { String in = Common.getImageName(Preferences.itself().absoluteMapsBaseDir + this.mapImageFileNameObject.getPath() + this.mapImageFileNameObject.getMapName()); this.mapImageFileNameObject.setImageExtension(Common.getExtension(in)); return in; } } } /** * Method to load a .wfl-file * * @param mapsPath * path to the map inclunding / at the end * @param thisMap * name of the map without extension * @throws IOException * when there was a problem reading .wfl-file * @throws IOException * when lat/lon were out of range * @throws ArithmeticException * when affine data is not correct, e.g. it is not possible to inverse affine-transformation */ public void loadWFL() throws IOException, ArithmeticException { final String FilePaN = Preferences.itself().absoluteMapsBaseDir + mapImageFileNameObject.getPath() + "/" + mapImageFileNameObject.getMapName() + ".wfl"; final FileInputStream instream = new FileInputStream(FilePaN); final InputStreamReader in = new InputStreamReader(instream); try { for (int i = 0; i < 4; i++) { affine[i] = Common.parseDoubleException(in.readLine()); } affineTopleft.latDec = Common.parseDoubleException(in.readLine()); affineTopleft.lonDec = Common.parseDoubleException(in.readLine()); bottomright.latDec = Common.parseDoubleException(in.readLine()); bottomright.lonDec = Common.parseDoubleException(in.readLine()); // readLine returns null, if End of File reached (==> coordTrans = 0) coordTrans = Common.parseInt(in.readLine()); in.close(); if (!bottomright.isValid()) { affine[0] = 0; affine[1] = 0; affine[2] = 0; affine[3] = 0; topleft.makeInvalid(); throw (new IOException(MyLocale.getMsg(4301, "Lat/Lon out of range while reading ") + FilePaN)); } } catch (final NullPointerException e) { // in.readline liefert null zur�ck, wenn keine Daten mehr vorhanden sind throw (new IOException(MyLocale.getMsg(4303, "not enough lines in file ") + FilePaN)); } doCalculations(); origAffineUpperLeft = new CWPoint(affineTopleft); } private void calcTile(int x, int y, int zoom) { coordTrans = TransformCoordinates.EPSG_WGS84; // coordTrans = TransformCoordinates.EPSG_Mercator_1SP_Google; CWPoint tl = Tile2LatLon(x, y, zoom); affineTopleft = new CWPoint(TransformCoordinatesProperties.fromWgs84(tl, coordTrans)); bottomright = Tile2LatLon(x + 1, y + 1, zoom); CWPoint affineBottomright = new CWPoint(TransformCoordinatesProperties.fromWgs84(bottomright, coordTrans)); affine[1] = (affineBottomright.lonDec - affineTopleft.lonDec) / 256; affine[2] = (affineBottomright.latDec - affineTopleft.latDec) / 256; affine[0] = 0; affine[3] = 0; doCalculations(); origAffineUpperLeft = new CWPoint(affineTopleft); } public void evalGCP(ewe.util.Vector GCPs, int imageWidth, int imageHeight) throws IllegalArgumentException { evalGCP(GCPs, imageWidth, imageHeight, 0); } /** * Method to evaluate ground control points (georeferenced points) and identify the parameters for the affine transformation * * @throws IllegalArgumentException * when less than 3 georeferenced points were given in GCPs */ public void evalGCP(ewe.util.Vector GCPs, int imageWidth, int imageHeight, int epsg_code) throws IllegalArgumentException { // N 48 16.000 E 11 32.000 // N 48 16.000 E 11 50.000 // N 48 9.000 E 11 32.000 if (GCPs.size() < 3) throw new IllegalArgumentException(MyLocale.getMsg(4304, "not enough points to calibrate the map")); GCPoint gcp = new GCPoint(); // Calculate parameters for latitutde affine transformation (affine // 0,2,4) Matrix X = new Matrix(GCPs.size(), 3); Matrix trg = new Matrix(GCPs.size(), 1); for (int i = 0; i < GCPs.size(); i++) { gcp = (GCPoint) GCPs.get(i); X.matrix[i][0] = 1; X.matrix[i][1] = gcp.bitMapX; X.matrix[i][2] = gcp.bitMapY; trg.matrix[i][0] = gcp.latDec; } Matrix Xtran = new Matrix(X); Xtran.Transpose(); Matrix XtranX = new Matrix(Xtran); XtranX.Multiply(X); Matrix XtranXinv = new Matrix(XtranX); XtranXinv.Inverse(); Matrix beta = new Matrix(XtranXinv); beta.Multiply(Xtran); beta.Multiply(trg); affine[0] = beta.matrix[1][0]; affine[2] = beta.matrix[2][0]; affineTopleft.latDec = beta.matrix[0][0]; // Calculate parameters for longitude affine transformation (affine // 1,3,5) X = new Matrix(GCPs.size(), 3); trg = new Matrix(GCPs.size(), 1); for (int i = 0; i < GCPs.size(); i++) { gcp = (GCPoint) GCPs.get(i); X.matrix[i][0] = 1; X.matrix[i][1] = gcp.bitMapX; X.matrix[i][2] = gcp.bitMapY; trg.matrix[i][0] = gcp.lonDec; } Xtran = new Matrix(X); Xtran.Transpose(); XtranX = new Matrix(Xtran); XtranX.Multiply(X); XtranXinv = new Matrix(XtranX); XtranXinv.Inverse(); beta = new Matrix(XtranXinv); beta.Multiply(Xtran); beta.Multiply(trg); affine[1] = beta.matrix[1][0]; affine[3] = beta.matrix[2][0]; affineTopleft.lonDec = beta.matrix[0][0]; coordTrans = epsg_code; bottomright = calcLatLon(imageWidth, imageHeight); doCalculations(); } /** * calculates centre, diagonal size of the map and inverse to affine transformation * * @throws ArithmeticException * when affine data is not correct, e.g. it is not possible to inverse affine-transformation */ private void doCalculations() throws ArithmeticException { try { // topleft = wgs84(affineTopLeft) topleft.set(calcLatLon(0, 0)); // die Mitte durch Halbierung center.set((bottomright.latDec + topleft.latDec) / 2, (bottomright.lonDec + topleft.lonDec) / 2); // Diagonale L�nge = 2 * (Mitte bis UntenRechts) sizeKm = java.lang.Math.abs((float) center.getDistance(bottomright)) * 2; // calculate reverse affine // nenner == 0 cannot happen as long als affine is correct final double nenner = (-affine[1] * affine[2] + affine[0] * affine[3]); transLatX = affine[3] / nenner; transLonX = -affine[2] / nenner; transLatY = -affine[1] / nenner; transLonY = affine[0] / nenner; // calculate north direction final Point c = calcMapXY(center); final int heightpixel = c.y * 2; c.y -= 1000; rotationRad = (float) (center.getBearing(calcLatLon(c)) / 180 * Math.PI); // note: the direction of nord can vary across the image. // In Gau�-Kr�ger Projection it does change about 1 degree per 10km! // (float)java.lang.Math.atan(rotationX2y); if (rotationRad > Math.PI) rotationRad -= 2 * Math.PI; // calculate scale in meters per pixel final double heightkm = calcLatLon(0, heightpixel).getDistance(topleft); scale = (float) (heightkm * 1000 / heightpixel); } catch (final ArithmeticException ex) { throw new ArithmeticException(MyLocale.getMsg(4305, "Not allowed values in affine\n (matrix cannot be inverted)\n in file \n") + this.mapImageFileNameObject.getMapName()); } } /** * Method to save a world file (.wfl) * * @throws IOException * when there was a problem writing .wfl-file * @throws IllegalArgumentException * when affine[x] for all x == 0 ("map not calibrated"). */ public void saveWFL() throws IOException, IllegalArgumentException { if (affine[0] == 0 && affine[1] == 0 && affine[2] == 0 && affine[3] == 0 && !topleft.isValid()) throw (new IllegalArgumentException(MyLocale.getMsg(4306, "map not calibrated"))); String PaN = Preferences.itself().absoluteMapsBaseDir + this.mapImageFileNameObject.getPath() + this.mapImageFileNameObject.getMapName() + ".wfl"; final PrintWriter outp = new PrintWriter(new BufferedWriter(new FileWriter(PaN))); final StringBuffer towriteB = new StringBuffer(400); towriteB.append(Convert.toString(affine[0])).append("\n"); towriteB.append(Convert.toString(affine[1])).append("\n"); towriteB.append(Convert.toString(affine[2])).append("\n"); towriteB.append(Convert.toString(affine[3])).append("\n"); towriteB.append(Convert.toString(affineTopleft.latDec)).append("\n"); // EPSG values towriteB.append(Convert.toString(affineTopleft.lonDec)).append("\n"); // EPSG values towriteB.append(Convert.toString(bottomright.latDec)).append("\n"); // WGS84 values towriteB.append(Convert.toString(bottomright.lonDec)).append("\n"); // WGS84 values towriteB.append(((coordTrans == 0 || coordTrans == TransformCoordinates.EPSG_WGS84) ? "" : "" + coordTrans + "\n")); String towrite = towriteB.toString(); if (Common.getDigSeparator() == ',') towrite = towrite.replace(',', '.'); outp.print(towrite); outp.close(); } /** * zoom in / out * * @param zf * zf > 1 == zoom in, zoom is relative to original unscaled image * @param diffX * shift of map in pixels (if the map was cropped while zooming) in the not zoomed image * @param diffY */ public void zoom(float zf, int diffX, int diffY) { // restore original values to calculate corret shift (upperleft) affineTopleft.latDec = origAffineUpperLeft.latDec; affineTopleft.lonDec = origAffineUpperLeft.lonDec; affine[0] = affine[0] * zoomFactor; affine[1] = affine[1] * zoomFactor; affine[2] = affine[2] * zoomFactor; affine[3] = affine[3] * zoomFactor; CoordinatePoint upperleft = calcLatLon(diffX, diffY); if (coordTrans != 0) upperleft = TransformCoordinatesProperties.fromWgs84(upperleft, coordTrans); affineTopleft.latDec = upperleft.latDec; affineTopleft.lonDec = upperleft.lonDec; affine[0] = affine[0] / zf; affine[1] = affine[1] / zf; affine[2] = affine[2] / zf; affine[3] = affine[3] / zf; zoomFactor = zf; shift.x = diffX; shift.y = diffY; doCalculations(); } /** * Method to calculate bitmap x,y of the current map using lat and lon target coordinates.<br> * There is no guaranty that the returned coordinates are inside of the map. They can be negative.<br> * * @param ll * @return Point */ public Point calcMapXY(CoordinatePoint ll) { CoordinatePoint t; if (coordTrans != 0) t = TransformCoordinatesProperties.fromWgs84(ll, coordTrans); else t = ll; final Point coords = new Point(); double b0, b1; b0 = t.latDec - affineTopleft.latDec; b1 = t.lonDec - affineTopleft.lonDec; final double mapx = transLatX * b0 + transLonX * b1; final double mapy = transLatY * b0 + transLonY * b1; coords.x = (int) Math.round(mapx); coords.y = (int) Math.round(mapy); return coords; } /** * gives back lat/lon from x, y in map * * @param x * @param y * @return */ public CWPoint calcLatLon(int x, int y) { CWPoint ll = new CWPoint(); ll.latDec = x * affine[0] + y * affine[2] + affineTopleft.latDec; ll.lonDec = x * affine[1] + y * affine[3] + affineTopleft.lonDec; if (coordTrans != 0) ll = TransformCoordinatesProperties.toWgs84(ll, coordTrans); return ll; } public CWPoint calcLatLon(Point p) { return calcLatLon(p.x, p.y); } /** * Get the prefix used for easy and fast finding of the best map The filname of the .wfl and respective image should start with this prefix in order to make finding the best map much faster * * @return */ public String getFfPrefix() { return "FF1" + getEasyFindString() + "E-"; } public MapImageFileNameObject getMapImageFileNameObject() { return mapImageFileNameObject; } public String getMapNameForList() { return mapImageFileNameObject.getMapNameForList(mapType); } public byte getMapType() { return mapType; } } /** * Class based on CWPoint but intended to handle bitmap x and y Used for georeferencing bitmaps. */ class GCPoint extends CWPoint { public int bitMapX = 0; public int bitMapY = 0; public GCPoint() { // Public constructor } public GCPoint(CWPoint p) { super(p); } /** * If you are using Gau�-Kr�ger, put lat = northing, lon = easting * * @param lat * @param lon */ public GCPoint(double lat, double lon) { this.latDec = lat; this.lonDec = lon; } public GCPoint(CWPoint ll, Point px) { super(ll); bitMapX = px.x; bitMapY = px.y; } } class MapImageFileNameObject { private String path; // relativ to Preferences.itself().absoluteMapsBaseDir private String mapName; // filename without Extension private String imageExtension; // ".png"; with dot MapImageFileNameObject(String path, String mapName, String extension) { this.path = path; this.mapName = mapName; this.imageExtension = extension; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getMapName() { return mapName; } public void setMapName(String mapName) { this.mapName = mapName; } public String getImageExtension() { return imageExtension; } public void setImageExtension(String imageExtension) { this.imageExtension = imageExtension; } public String getMapNameForList(byte mapType) { if (mapType == 2) { return this.path + this.mapName; } else if (mapType == 3) { String s[] = mString.split(this.mapName, '!'); return this.path + " zoom: " + s[4] + " x,y: " + s[5] + "," + s[6]; } else return this.mapName; } }