package com.revolsys.raster; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.beans.IndexedPropertyChangeEvent; import java.beans.PropertyChangeEvent; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.revolsys.beans.AbstractPropertyChangeSupportProxy; import com.revolsys.collection.PropertyChangeArrayList; import com.revolsys.collection.map.LinkedHashMapEx; import com.revolsys.collection.map.MapEx; import com.revolsys.datatype.DataType; import com.revolsys.geometry.cs.CoordinateSystem; import com.revolsys.geometry.cs.esri.EsriCoordinateSystems; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.impl.PointDoubleXY; import com.revolsys.io.FileUtil; import com.revolsys.io.map.MapObjectFactory; import com.revolsys.logging.Logs; import com.revolsys.record.io.format.json.Json; import com.revolsys.record.io.format.xml.DomUtil; import com.revolsys.spring.resource.Resource; import com.revolsys.util.Property; public abstract class AbstractGeoreferencedImage extends AbstractPropertyChangeSupportProxy implements GeoreferencedImage { public static int[] getResolution(final ImageReader r) throws IOException { int hdpi = 96, vdpi = 96; final double mm2inch = 25.4; NodeList lst; final Element node = (Element)r.getImageMetadata(0).getAsTree("javax_imageio_1.0"); lst = node.getElementsByTagName("HorizontalPixelSize"); if (lst != null && lst.getLength() == 1) { hdpi = (int)(mm2inch / Float.parseFloat(((Element)lst.item(0)).getAttribute("value"))); } lst = node.getElementsByTagName("VerticalPixelSize"); if (lst != null && lst.getLength() == 1) { vdpi = (int)(mm2inch / Float.parseFloat(((Element)lst.item(0)).getAttribute("value"))); } return new int[] { hdpi, vdpi }; } private BoundingBox boundingBox = BoundingBox.empty(); private int[] dpi; private File file; private GeometryFactory geometryFactory = GeometryFactory.DEFAULT_3D; private boolean hasChanges; private int imageHeight = -1; private Resource imageResource; private int imageWidth = -1; private List<Dimension> overviewSizes = new ArrayList<>(); private final Map<CoordinateSystem, AbstractGeoreferencedImage> projectedImages = new HashMap<>(); private RenderedImage renderedImage; private double resolution; private final PropertyChangeArrayList<MappedLocation> tiePoints = new PropertyChangeArrayList<>(); public AbstractGeoreferencedImage() { } protected void addOverviewSize(final int width, final int height) { final Dimension size = new Dimension(width, height); this.overviewSizes.add(size); } @Override public void deleteTiePoint(final MappedLocation tiePoint) { if (this.tiePoints.remove(tiePoint)) { this.hasChanges = true; } } @Override public BoundingBox getBoundingBox() { return this.boundingBox; } @Override public int[] getDpi() { if (this.dpi == null) { int[] dpi = new int[] { 96, 96 }; try { final Resource imageResource = getImageResource(); final InputStream in = imageResource.getInputStream(); final ImageInputStream iis = ImageIO.createImageInputStream(in); final Iterator<ImageReader> i = ImageIO.getImageReaders(iis); if (i.hasNext()) { final ImageReader r = i.next(); r.setInput(iis); dpi = getResolution(r); if (dpi[0] == 0) { dpi[0] = 96; } if (dpi[1] == 0) { dpi[1] = 96; } r.dispose(); } iis.close(); } catch (final Throwable e) { e.printStackTrace(); } this.dpi = dpi; } return this.dpi; } public File getFile() { return this.file; } @Override public GeometryFactory getGeometryFactory() { return this.geometryFactory; } @Override public AbstractGeoreferencedImage getImage(final CoordinateSystem coordinateSystem) { synchronized (this.projectedImages) { if (coordinateSystem.equals(getCoordinateSystem())) { return this; } else { AbstractGeoreferencedImage projectedImage = this.projectedImages.get(coordinateSystem); if (projectedImage == null) { projectedImage = getImage(coordinateSystem, this.resolution); this.projectedImages.put(coordinateSystem, projectedImage); } return projectedImage; } } } @Override public AbstractGeoreferencedImage getImage(final CoordinateSystem coordinateSystem, final double resolution) { final int imageSrid = getGeometryFactory().getCoordinateSystemId(); if (imageSrid > 0 && imageSrid != coordinateSystem.getCoordinateSystemId()) { final BoundingBox boundingBox = getBoundingBox(); final ProjectionImageFilter filter = new ProjectionImageFilter(boundingBox, coordinateSystem, resolution); final BufferedImage newImage = filter.filter(getBufferedImage()); final BoundingBox destBoundingBox = filter.getDestBoundingBox(); return new BufferedGeoreferencedImage(destBoundingBox, newImage); } return this; } @Override public AbstractGeoreferencedImage getImage(final GeometryFactory geometryFactory) { final CoordinateSystem coordinateSystem = geometryFactory.getCoordinateSystem(); return getImage(coordinateSystem); } @Override public int getImageHeight() { if (this.imageHeight == -1) { if (this.renderedImage != null) { this.imageHeight = this.renderedImage.getHeight(); } } return this.imageHeight; } @Override public Resource getImageResource() { return this.imageResource; } @Override public int getImageWidth() { if (this.imageWidth == -1) { if (this.renderedImage != null) { this.imageWidth = this.renderedImage.getWidth(); } } return this.imageWidth; } @Override public List<Dimension> getOverviewSizes() { return this.overviewSizes; } @Override public RenderedImage getRenderedImage() { return this.renderedImage; } @Override public double getResolution() { return this.resolution; } @Override public List<MappedLocation> getTiePoints() { return this.tiePoints; } @Override public String getWorldFileExtension() { return "tfw"; } @Override public int hashCode() { return this.boundingBox.hashCode(); } @Override public boolean isHasChanages() { return this.hasChanges; } protected void loadAuxXmlFile(final long modifiedTime) { final Resource resource = getImageResource(); final String extension = resource.getFileNameExtension(); final Resource auxFile = resource.newResourceChangeExtension(extension + ".aux.xml"); if (auxFile.exists() && auxFile.getLastModified() > modifiedTime) { loadWorldFileX(); final int[] dpi = getDpi(); try { int srid = 0; final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); final DocumentBuilder builder = factory.newDocumentBuilder(); final InputStream in = auxFile.getInputStream(); try { final Document doc = builder.parse(in); final NodeList spatialReferences = doc.getElementsByTagName("SpatialReference"); for (int i = 0; i < spatialReferences.getLength() && srid == 0; i++) { final Node spatialReference = spatialReferences.item(i); Element sridElement = DomUtil.getFirstChildElement(spatialReference, "LatestWKID"); if (sridElement == null) { sridElement = DomUtil.getFirstChildElement(spatialReference, "WKID"); } if (sridElement != null) { srid = DomUtil.getInteger(sridElement); } } if (srid == 0) { final NodeList srsList = doc.getElementsByTagName("SRS"); for (int i = 0; i < srsList.getLength() && srid == 0; i++) { final Node srsNode = srsList.item(i); final String srsWkt = srsNode.getTextContent(); final CoordinateSystem coordinateSystem = EsriCoordinateSystems .getCoordinateSystem(srsWkt); if (coordinateSystem != null) { srid = coordinateSystem.getCoordinateSystemId(); } } } final GeometryFactory geometryFactory = GeometryFactory.floating(srid, 2); setGeometryFactory(geometryFactory); final List<Double> sourceControlPoints = DomUtil.getDoubleList(doc, "SourceGCPs"); final List<Double> targetControlPoints = DomUtil.getDoubleList(doc, "TargetGCPs"); if (sourceControlPoints.size() > 0 && targetControlPoints.size() > 0) { final List<MappedLocation> tiePoints = new ArrayList<>(); for (int i = 0; i < sourceControlPoints.size() && i < targetControlPoints.size(); i += 2) { final double imageX = sourceControlPoints.get(i) * dpi[0]; final double imageY = sourceControlPoints.get(i + 1) * dpi[1]; final Point sourcePixel = new PointDoubleXY(imageX, imageY); final double x = targetControlPoints.get(i); final double y = targetControlPoints.get(i + 1); final Point targetPoint = geometryFactory.point(x, y); final MappedLocation tiePoint = new MappedLocation(sourcePixel, targetPoint); tiePoints.add(tiePoint); } setTiePoints(tiePoints); } } finally { FileUtil.closeSilent(in); } } catch (final Throwable e) { Logs.error(this, "Unable to read: " + auxFile, e); } } } protected void loadImageMetaData() { loadMetaDataFromImage(); final long modifiedTime = loadSettings(); loadAuxXmlFile(modifiedTime); if (!hasGeometryFactory()) { loadProjectionFile(); } if (!hasBoundingBox()) { loadWorldFile(); } } protected void loadMetaDataFromImage() { } protected void loadProjectionFile() { final Resource resource = getImageResource(); final GeometryFactory geometryFactory = EsriCoordinateSystems.getGeometryFactory(resource); setGeometryFactory(geometryFactory); } protected long loadSettings() { final Resource resource = getImageResource(); final Resource settingsFile = resource.newResourceAddExtension("rgobject"); if (settingsFile.exists()) { try { final Map<String, Object> settings = Json.toMap(settingsFile); final String boundingBoxWkt = (String)settings.get("boundingBox"); if (Property.hasValue(boundingBoxWkt)) { final BoundingBox boundingBox = BoundingBox.newBoundingBox(boundingBoxWkt); if (!boundingBox.isEmpty()) { setBoundingBox(boundingBox); } } final List<?> tiePointsProperty = (List<?>)settings.get("tiePoints"); final List<MappedLocation> tiePoints = new ArrayList<>(); if (tiePointsProperty != null) { for (final Object tiePointValue : tiePointsProperty) { if (tiePointValue instanceof MappedLocation) { tiePoints.add((MappedLocation)tiePointValue); } else if (tiePointValue instanceof Map) { @SuppressWarnings("unchecked") final Map<String, Object> map = (Map<String, Object>)tiePointValue; tiePoints.add(new MappedLocation(map)); } } } if (!tiePoints.isEmpty()) { setTiePoints(tiePoints); } return settingsFile.getLastModified(); } catch (final Throwable e) { Logs.error(this, "Unable to load:" + settingsFile, e); return -1; } } else { return -1; } } protected void loadWorldFile() { final Resource resource = getImageResource(); final Resource worldFile = resource.newResourceChangeExtension(getWorldFileExtension()); loadWorldFile(worldFile); } @SuppressWarnings("unused") protected void loadWorldFile(final Resource worldFile) { if (worldFile.exists()) { try { try ( final BufferedReader reader = worldFile.newBufferedReader()) { final double pixelWidth = Double.parseDouble(reader.readLine()); final double yRotation = Double.parseDouble(reader.readLine()); final double xRotation = Double.parseDouble(reader.readLine()); final double pixelHeight = Double.parseDouble(reader.readLine()); // Top left final double x1 = Double.parseDouble(reader.readLine()); final double y1 = Double.parseDouble(reader.readLine()); setResolution(pixelWidth); // TODO rotation using a warp filter setBoundingBox(x1, y1, pixelWidth, pixelHeight); } } catch (final IOException e) { Logs.error(this, "Error reading world file " + worldFile, e); } } } protected void loadWorldFileX() { final Resource resource = getImageResource(); final Resource worldFile = resource.newResourceChangeExtension(getWorldFileExtension() + "x"); if (worldFile.exists()) { loadWorldFile(worldFile); } else { loadWorldFile(); } } protected void postConstruct() { setHasChanges(false); Property.addListener(this.tiePoints, this); } @Override public void propertyChange(final PropertyChangeEvent event) { final Object source = event.getSource(); if (source == this.tiePoints) { if (event instanceof IndexedPropertyChangeEvent) { final Object oldValue = event.getOldValue(); if (oldValue instanceof MappedLocation) { ((MappedLocation)oldValue).removePropertyChangeListener(this); } final Object newValue = event.getOldValue(); if (newValue instanceof MappedLocation) { ((MappedLocation)newValue).addPropertyChangeListener(this); } } setHasChanges(true); } else if (source instanceof MappedLocation) { setHasChanges(true); } firePropertyChange(event); } @Override public boolean saveChanges() { try { final Resource resource = this.imageResource; final Resource rgResource = resource.newResourceAddExtension("rgobject"); MapObjectFactory.write(rgResource, this); setHasChanges(false); return true; } catch (final Throwable e) { Logs.error(this, "Unable to save: " + this.imageResource + ".rgobject", e); return false; } } @Override public void setBoundingBox(final BoundingBox boundingBox) { if (!DataType.equal(boundingBox, this.boundingBox)) { setGeometryFactory(boundingBox.getGeometryFactory()); this.boundingBox = boundingBox; setHasChanges(true); } } @Override public void setDpi(final int... dpi) { this.dpi = dpi; } public void setGeometryFactory(final GeometryFactory geometryFactory) { if (geometryFactory != null) { this.geometryFactory = geometryFactory.convertAxisCount(2); for (final MappedLocation mappedLocation : this.tiePoints) { mappedLocation.setGeometryFactory(geometryFactory); } } } protected void setHasChanges(final boolean hasChanges) { final boolean oldValue = this.hasChanges; this.hasChanges = hasChanges; firePropertyChange("hasChanges", oldValue, hasChanges); } protected void setImageHeight(final int imageHeight) { this.imageHeight = imageHeight; } protected void setImageResource(final Resource imageResource) { this.imageResource = imageResource; this.file = Resource.getOrDownloadFile(this.imageResource); } protected void setImageWidth(final int imageWidth) { this.imageWidth = imageWidth; } protected void setOverviewSizes(final List<Dimension> overviewSizes) { this.overviewSizes = overviewSizes; } @Override public void setRenderedImage(final RenderedImage renderedImage) { this.renderedImage = renderedImage; } protected void setResolution(final double resolution) { this.resolution = resolution; } @Override public void setTiePoints(final List<MappedLocation> tiePoints) { if (!DataType.equal(tiePoints, this.tiePoints)) { for (final MappedLocation mappedLocation : this.tiePoints) { mappedLocation.removePropertyChangeListener(this); } this.tiePoints.clear(); this.tiePoints.addAll(tiePoints); final GeometryFactory geometryFactory = getGeometryFactory(); for (final MappedLocation mappedLocation : tiePoints) { mappedLocation.setGeometryFactory(geometryFactory); mappedLocation.addPropertyChangeListener(this); } setHasChanges(true); } } @Override public MapEx toMap() { final MapEx map = new LinkedHashMapEx(); addTypeToMap(map, "bufferedImage"); final BoundingBox boundingBox = getBoundingBox(); if (boundingBox != null) { addToMap(map, "boundingBox", boundingBox.toString()); } final List<MappedLocation> tiePoints = getTiePoints(); addToMap(map, "tiePoints", tiePoints); return map; } @Override public String toString() { if (this.imageResource == null) { return super.toString(); } else { return this.imageResource.toString(); } } }