/*************************************************************************** * Copyright (C) 2009 by Tomasz Stelmach * * http://www.stelmach-online.net/ * * * * 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; either version 2 of the License, or * * (at your option) any later version. * * * * 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., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ package org.openstreetmap.josm.plugins.piclayer.layer; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.Properties; import javax.swing.Action; import javax.swing.Icon; import javax.swing.ImageIcon; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.RenameLayerAction; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; import org.openstreetmap.josm.data.projection.Projection; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.layer.Layer; import org.openstreetmap.josm.plugins.piclayer.actions.LoadPictureCalibrationAction; import org.openstreetmap.josm.plugins.piclayer.actions.LoadPictureCalibrationFromWorldAction; import org.openstreetmap.josm.plugins.piclayer.actions.ResetCalibrationAction; import org.openstreetmap.josm.plugins.piclayer.actions.SavePictureCalibrationAction; import org.openstreetmap.josm.plugins.piclayer.actions.SavePictureCalibrationToWorldAction; import org.openstreetmap.josm.plugins.piclayer.transform.PictureTransform; /** * Base class for layers showing images. Actually it does all the showing. The * subclasses are supposed only to create images in different ways (load from * files, copy from clipboard, hack into a spy satellite and download them, * anything...) */ public abstract class PicLayerAbstract extends Layer { // Counter - just for naming of layers private static int imageCounter = 0; // This is the main image to be displayed protected Image image = null; // Tiles of pin images private static Image pinTiledImage; // Initial position of the image in the real world // protected EastNorth initialImagePosition; // Position of the image in the real world //protected EastNorth imagePosition; // The scale that was set on the map during image creation protected double initialImageScale = 1.0; // Layer icon private Icon layerIcon = null; private boolean drawMarkers = true; public void setDrawPoints(boolean value) { drawMarkers = value; } protected PictureTransform transformer; public PictureTransform getTransformer() { return transformer; } // Keys for loading from old/new Properties private static final String POSITION_X = "POSITION_X"; private static final String POSITION_Y = "POSITION_Y"; private static final String ANGLE = "ANGLE"; private static final String INITIAL_SCALE = "INITIAL_SCALE"; private static final String SCALEX = "SCALEX"; private static final String SCALEY = "SCALEY"; private static final String SHEARX = "SHEARX"; private static final String SHEARY = "SHEARY"; // new properties private static final String MATRIXm00 = "M00"; private static final String MATRIXm01 = "M01"; private static final String MATRIXm10 = "M10"; private static final String MATRIXm11 = "M11"; private static final String MATRIXm02 = "M02"; private static final String MATRIXm12 = "M12"; // pin images properties - tile anchors, width and offset // TODO: load these from properties file in images folder... private static final int pinAnchorX = 31; private static final int pinAnchorY = 31; private static final int[] pinTileOffsetX = {74, 0, 74, 0}; private static final int[] pinTileOffsetY = {0, 74, 74, 0}; private static final int pinWidth = 64; private static final int pinHeight = 64; protected final Projection projection; /** * Constructor */ public PicLayerAbstract() { super("PicLayer #" + imageCounter); //Increase number imageCounter++; // Load layer icon layerIcon = new ImageIcon(Toolkit.getDefaultToolkit().createImage(getClass().getResource("/images/layericon.png"))); if (pinTiledImage == null) { // allow system to load the image and use it in future pinTiledImage = new ImageIcon(Toolkit.getDefaultToolkit().createImage(getClass().getResource("/images/v6_64.png"))).getImage(); } projection = Main.getProjection(); } /** * Initializes the image. Gets the image from a subclass and stores some * initial parameters. Throws exception if something fails. */ public void initialize() throws IOException { // First, we initialize the calibration, so that createImage() can rely on it transformer = new PictureTransform(); // If the map does not exist - we're screwed. We should not get into this situation in the first place! if (Main.map != null && Main.map.mapView != null) { EastNorth center = Main.map.mapView.getCenter(); // imagePosition = new EastNorth(center.east(), center.north()); transformer.setImagePosition(new EastNorth(center.east(), center.north())); // initialImagePosition = new EastNorth(imagePosition.east(), imagePosition.north()); // Initial scale at which the image was loaded initialImageScale = Main.map.mapView.getDist100Pixel(); } else { throw new IOException(tr("Could not find the map object.")); } // Create image image = createImage(); if (image == null) { throw new IOException(tr("PicLayer failed to load or import the image.")); } // Load image completely (new ImageIcon(image)).getImage(); lookForCalibration(); } /** * To be overridden by subclasses. Provides an image from an external sources. * Throws exception if something does not work. * * TODO: Replace the IOException by our own exception. */ protected abstract Image createImage() throws IOException; protected abstract void lookForCalibration() throws IOException; /** * To be overridden by subclasses. Returns the user readable name of the layer. */ public abstract String getPicLayerName(); @Override public Icon getIcon() { return layerIcon; } @Override public Object getInfoComponent() { return null; } @Override public Action[] getMenuEntries() { // Main menu return new Action[] { new ResetCalibrationAction(this, transformer), SeparatorLayerAction.INSTANCE, new SavePictureCalibrationAction(this), new LoadPictureCalibrationAction(this), SeparatorLayerAction.INSTANCE, new SavePictureCalibrationToWorldAction(this), new LoadPictureCalibrationFromWorldAction(this), SeparatorLayerAction.INSTANCE, new RenameLayerAction(null, this), }; } @Override public String getToolTipText() { return getPicLayerName(); } @Override public boolean isMergable(Layer arg0) { return false; } @Override public void mergeFrom(Layer arg0) {} @Override public void paint(Graphics2D g2, MapView mv, Bounds bounds) { if (image != null) { // Position image at the right graphical place EastNorth center = mv.getCenter(); EastNorth leftop = mv.getEastNorth(0, 0); // Number of pixels for one unit in east north space. // This is the same in x- and y- direction. double pixel_per_en = (mv.getWidth() / 2.0) / (center.east() - leftop.east()); // This is now the offset in screen pixels EastNorth imagePosition = transformer.getImagePosition(); double pic_offset_x = ((imagePosition.east() - leftop.east()) * pixel_per_en); double pic_offset_y = ((leftop.north() - imagePosition.north()) * pixel_per_en); Graphics2D g = (Graphics2D) g2.create(); // Move g.translate(pic_offset_x, pic_offset_y); // Scale double scalex = initialImageScale * pixel_per_en / getMetersPerEasting(imagePosition) / 100; double scaley = initialImageScale * pixel_per_en / getMetersPerNorthing(imagePosition) / 100; g.scale(scalex, scaley); g.transform(transformer.getTransform()); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); // Draw picture g.drawImage(image, -image.getWidth(null) / 2, -image.getHeight(null) / 2, null); // Draw additional rectangle for the active pic layer if (mv.getLayerManager().getActiveLayer() == this) { g.setColor(new Color(0xFF0000)); g.drawRect( -image.getWidth(null) / 2, -image.getHeight(null) / 2, image.getWidth(null), image.getHeight(null) ); } if (drawMarkers) { // draw markers for selection Graphics2D gPoints = (Graphics2D) g2.create(); gPoints.translate(pic_offset_x, pic_offset_y); gPoints.setColor(Color.RED); // red color for points output AffineTransform tr = AffineTransform.getScaleInstance(scalex, scaley); tr.concatenate(transformer.getTransform()); for (int i = 0; i < transformer.getOriginPoints().size(); i++) { Point2D trP = tr.transform(transformer.getOriginPoints().get(i), null); int x = (int) trP.getX(), y = (int) trP.getY(); int dstx = x-pinAnchorX; int dsty = y-pinAnchorY; gPoints.drawImage(pinTiledImage, dstx, dsty, dstx+pinWidth, dsty+pinHeight, pinTileOffsetX[i], pinTileOffsetY[i], pinTileOffsetX[i]+pinWidth, pinTileOffsetY[i]+pinHeight, null); } } } else { // TODO: proper logging System.out.println("PicLayerAbstract::paint - general drawing error (image is null or Graphics not 2D"); } } /** * Returns the distance in meter, that corresponds to one unit in east north * space. For normal projections, it is about 1 (but usually changing with * latitude). * For EPSG:4326, it is the distance from one meridian of full degree to the * next (a couple of kilometers). */ protected double getMetersPerEasting(EastNorth en) { /* Natural scale in east/north units per pixel. * This means, the projection should be able to handle * a shift of that size in east north space without * going out of bounds. * * Also, this should get us somewhere in the range of meters, * so we get the result at the point 'en' and not some average. */ double naturalScale = projection.getDefaultZoomInPPD(); naturalScale *= 0.01; // make a little smaller LatLon ll1 = projection.eastNorth2latlon( new EastNorth(en.east() - naturalScale, en.north())); LatLon ll2 = projection.eastNorth2latlon( new EastNorth(en.east() + naturalScale, en.north())); double dist = ll1.greatCircleDistance(ll2) / naturalScale / 2; return dist; } /* see getMetersPerEasting */ private double getMetersPerNorthing(EastNorth en) { double naturalScale = projection.getDefaultZoomInPPD(); naturalScale *= 0.01; LatLon ll1 = projection.eastNorth2latlon( new EastNorth(en.east(), en.north()- naturalScale)); LatLon ll2 = projection.eastNorth2latlon( new EastNorth(en.east(), en.north() + naturalScale)); double dist = ll1.greatCircleDistance(ll2) / naturalScale / 2; return dist; } @Override /** * Computes the (rough) bounding box. * We ignore the rotation, the resulting bounding box contains any possible * rotation. */ public void visitBoundingBox(BoundingXYVisitor arg0) { if (image == null) return; String projcode = projection.toCode(); // TODO: bounding box only supported when coordinates are in meters // The reason for that is that this .cal think makes us a hard time. // The position is stored as a raw data (can be either in degrees or // in meters, depending on the projection used at creation), but the // initial scale is in m/100pix // So for now, we support the bounding box only when everything is in meters if (projcode.equals("EPSG:4326")) return; EastNorth center = transformer.getImagePosition(); double w = image.getWidth(null); double h = image.getHeight(null); double diag_pix = Math.sqrt(w*w+h*h); // initialImageScale is a the scale (unit: m/100pix) at creation time double diag_m = (diag_pix/100) * initialImageScale; AffineTransform trans = transformer.getTransform(); double factor = Math.max(trans.getScaleX(), trans.getScaleY()); double offset = factor * diag_m / 2.0; EastNorth topleft = center.add(-offset, -offset); EastNorth bottomright = center.add(offset, offset); arg0.visit(topleft); arg0.visit(bottomright); } /** * Saves the calibration data into properties structure * @param props Properties to save to */ public void saveCalibration(Properties props) { // Save double[] matrix = new double[6]; transformer.getTransform().getMatrix(matrix); props.put(MATRIXm00, Double.toString(matrix[0])); props.put(MATRIXm01, Double.toString(matrix[1])); props.put(MATRIXm10, Double.toString(matrix[2])); props.put(MATRIXm11, Double.toString(matrix[3])); props.put(MATRIXm02, Double.toString(matrix[4])); props.put(MATRIXm12, Double.toString(matrix[5])); props.put(POSITION_X, Double.toString(transformer.getImagePosition().getX())); props.put(POSITION_Y, Double.toString(transformer.getImagePosition().getY())); props.put(INITIAL_SCALE, Double.toString(initialImageScale)); transformer.resetModified(); } /** * Loads calibration data from file * @param file The file to read from */ public void loadCalibration(InputStream is) throws IOException { Properties props = new Properties(); props.load(is); loadCalibration(props); } /** * Loads calibration data from properties structure * @param props Properties to load from */ public void loadCalibration(Properties props) { // Load AffineTransform transform; double pos_x = Double.valueOf(props.getProperty(POSITION_X, "0")); double pos_y = Double.valueOf(props.getProperty(POSITION_Y, "0")); EastNorth imagePosition = new EastNorth(pos_x, pos_y); transformer.setImagePosition(imagePosition); initialImageScale = Double.valueOf(props.getProperty(INITIAL_SCALE, "1")); //in_scale if (props.containsKey(SCALEX)) { // old format //double in_pos_x = Double.valueOf(props.getProperty(INITIAL_POS_X, "0")); //double in_pos_y = Double.valueOf(props.getProperty(INITIAL_POS_Y, "0")); double angle = Double.valueOf(props.getProperty(ANGLE, "0")); double scale_x = Double.valueOf(props.getProperty(SCALEX, "1")); double scale_y = Double.valueOf(props.getProperty(SCALEY, "1")); double shear_x = Double.valueOf(props.getProperty(SHEARX, "0")); double shear_y = Double.valueOf(props.getProperty(SHEARY, "0")); // transform to matrix from these values - need testing transform = AffineTransform.getRotateInstance(angle/180*Math.PI); transform.scale(scale_x, scale_y); transform.shear(shear_x, shear_y); } else { // initialize matrix double[] matrix = new double[6]; matrix[0] = Double.parseDouble(props.getProperty(MATRIXm00, "1")); matrix[1] = Double.parseDouble(props.getProperty(MATRIXm01, "0")); matrix[2] = Double.parseDouble(props.getProperty(MATRIXm10, "0")); matrix[3] = Double.parseDouble(props.getProperty(MATRIXm11, "1")); matrix[4] = Double.parseDouble(props.getProperty(MATRIXm02, "0")); matrix[5] = Double.parseDouble(props.getProperty(MATRIXm12, "0")); transform = new AffineTransform(matrix); } transformer.resetCalibration(); transformer.getTransform().concatenate(transform); // Refresh Main.map.mapView.repaint(); } public void loadWorldfile(InputStream is) throws IOException { try ( Reader reader = new InputStreamReader(is); BufferedReader br = new BufferedReader(reader) ) { double[] e = new double[6]; for (int i = 0; i < 6; ++i) { String line = br.readLine(); e[i] = Double.parseDouble(line); } double sx = e[0], ry = e[1], rx = e[2], sy = e[3], dx = e[4], dy = e[5]; int w = image.getWidth(null); int h = image.getHeight(null); EastNorth imagePosition = new EastNorth( dx + w/2*sx + h/2*rx, dy + w/2*ry + h/2*sy ); // initialImagePosition.setLocation(imagePosition); // m_angle = 0; double scalex = 100*sx*getMetersPerEasting(imagePosition); double scaley = -100*sy*getMetersPerNorthing(imagePosition); double shearx = rx / sx; double sheary = ry / sy; transformer.setImagePosition(imagePosition); transformer.resetCalibration(); AffineTransform tr = transformer.getTransform(); tr.scale(scalex, scaley); tr.shear(shearx, sheary); initialImageScale = 1; Main.map.mapView.repaint(); } } public void saveWorldFile(double[] values) { double[] matrix = new double[6]; transformer.getTransform().getMatrix(matrix); double a00 = matrix[0], a01 = matrix[2], a02 = matrix[4]; double a10 = matrix[1], a11 = matrix[3], a12 = matrix[5]; int w = image.getWidth(null); int h = image.getHeight(null); EastNorth imagePosition = transformer.getImagePosition(); // piclayer calibration stores 9 parameters // worldfile has 6 parameters // only 6 parameters needed, so write it in a way that // eliminates the 3 redundant parameters double qx = initialImageScale / 100 / getMetersPerEasting(imagePosition); double qy = -initialImageScale / 100 / getMetersPerNorthing(imagePosition); double sx = qx * a00; double sy = qy * a11; double rx = qx * a01; double ry = qy * a10; double dx = imagePosition.getX() + qx * a02 - sx * w / 2 - rx * h / 2; double dy = imagePosition.getY() + qy * a12 - ry * w / 2 - sy * h / 2; values[0] = sx; values[1] = ry; values[2] = rx; values[3] = sy; values[4] = dx; values[5] = dy; } public Point2D transformPoint(Point p) throws NoninvertibleTransformException { // Position image at the right graphical place EastNorth center = Main.map.mapView.getCenter(); EastNorth leftop = Main.map.mapView.getEastNorth(0, 0); // Number of pixels for one unit in east north space. // This is the same in x- and y- direction. double pixel_per_en = (Main.map.mapView.getWidth() / 2.0) / (center.east() - leftop.east()); EastNorth imageCenter = transformer.getImagePosition(); // This is now the offset in screen pixels double pic_offset_x = ((imageCenter.east() - leftop.east()) * pixel_per_en); double pic_offset_y = ((leftop.north() - imageCenter.north()) * pixel_per_en); // something bad... AffineTransform pointTrans = AffineTransform.getTranslateInstance(pic_offset_x, pic_offset_y); double scalex = initialImageScale * pixel_per_en / getMetersPerEasting(imageCenter) / 100; double scaley = initialImageScale * pixel_per_en / getMetersPerNorthing(imageCenter) / 100; pointTrans.scale(scalex, scaley); // ok here pointTrans.concatenate(transformer.getTransform()); Point2D result = pointTrans.inverseTransform(p, null); return result; } /** * Moves the picture. Scaled in EastNorth... */ public void movePictureBy(double x, double y) { transformer.setImagePosition(transformer.getImagePosition().add(x, y)); } public void rotatePictureBy(double angle) { try { Point2D trans = transformPoint(new Point(Main.map.mapView.getWidth()/2, Main.map.mapView.getHeight()/2)); transformer.concatenateTransformPoint(AffineTransform.getRotateInstance(angle), trans); } catch (NoninvertibleTransformException e) { e.printStackTrace(); } } public void scalePictureBy(double scalex, double scaley) { try { Point2D trans = transformPoint(new Point(Main.map.mapView.getWidth()/2, Main.map.mapView.getHeight()/2)); transformer.concatenateTransformPoint(AffineTransform.getScaleInstance(scalex, scaley), trans); } catch (NoninvertibleTransformException e) { e.printStackTrace(); } } public void shearPictureBy(double shx, double shy) { try { Point2D trans = transformPoint(new Point(Main.map.mapView.getWidth()/2, Main.map.mapView.getHeight()/2)); transformer.concatenateTransformPoint(AffineTransform.getShearInstance(shx, shy), trans); } catch (NoninvertibleTransformException e) { e.printStackTrace(); } } public void resetCalibration() { transformer.resetCalibration(); } // get image coordinates by mouse coords public Point2D findSelectedPoint(Point point) { if (image == null) return null; Point2D selected = null; try { Point2D pressed = transformPoint(point); double mindist = 10; for (Point2D p : transformer.getOriginPoints()) { if (p.distance(pressed) < mindist) { // if user clicked to select some of origin point selected = p; mindist = p.distance(pressed); } } return selected; } catch (NoninvertibleTransformException e) { e.printStackTrace(); } return selected; } }