package com.revolsys.swing.map.layer.raster;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.beans.PropertyChangeEvent;
import java.util.Map;
import java.util.function.Predicate;
import javax.swing.JOptionPane;
import com.revolsys.collection.map.MapEx;
import com.revolsys.collection.map.Maps;
import com.revolsys.geometry.model.BoundingBox;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.impl.PointDoubleXY;
import com.revolsys.io.FileUtil;
import com.revolsys.io.IoFactory;
import com.revolsys.logging.Logs;
import com.revolsys.raster.GeoreferencedImage;
import com.revolsys.raster.GeoreferencedImageReadFactory;
import com.revolsys.raster.MappedLocation;
import com.revolsys.spring.resource.Resource;
import com.revolsys.swing.Borders;
import com.revolsys.swing.Icons;
import com.revolsys.swing.SwingUtil;
import com.revolsys.swing.component.BasePanel;
import com.revolsys.swing.component.TabbedValuePanel;
import com.revolsys.swing.component.ValueField;
import com.revolsys.swing.layout.GroupLayouts;
import com.revolsys.swing.map.layer.AbstractLayer;
import com.revolsys.swing.map.layer.Project;
import com.revolsys.swing.menu.MenuFactory;
import com.revolsys.swing.menu.Menus;
import com.revolsys.swing.parallel.Invoke;
import com.revolsys.util.Property;
import com.revolsys.util.number.Doubles;
public class GeoreferencedImageLayer extends AbstractLayer {
static {
final MenuFactory menu = MenuFactory.getMenu(GeoreferencedImageLayer.class);
menu.addGroup(0, "table");
menu.addGroup(2, "edit");
final Predicate<GeoreferencedImageLayer> notReadOnly = ((Predicate<GeoreferencedImageLayer>)GeoreferencedImageLayer::isReadOnly)
.negate();
final Predicate<GeoreferencedImageLayer> editable = GeoreferencedImageLayer::isEditable;
Menus.<GeoreferencedImageLayer> addMenuItem(menu, "table", "View Tie-Points", "table_go",
GeoreferencedImageLayer::showTableView, false);
Menus.<GeoreferencedImageLayer> addCheckboxMenuItem(menu, "edit", "Editable", "pencil",
notReadOnly, GeoreferencedImageLayer::toggleEditable, editable, true);
Menus.<GeoreferencedImageLayer> addCheckboxMenuItem(menu, "edit", "Show Original Image",
(String)null, editable.and(GeoreferencedImageLayer::isHasTransform),
GeoreferencedImageLayer::toggleShowOriginalImage,
GeoreferencedImageLayer::isShowOriginalImage, true);
Menus.<GeoreferencedImageLayer> addMenuItem(menu, "edit", "Fit to Screen", "arrow_out",
editable, GeoreferencedImageLayer::fitToViewport, true);
menu.deleteMenuItem("refresh", "Refresh");
}
public static GeoreferencedImageLayer newLayer(final Map<String, Object> properties) {
return new GeoreferencedImageLayer(properties);
}
private GeoreferencedImage image;
private int opacity = 255;
private Resource resource;
private boolean showOriginalImage = false;
private String url;
public GeoreferencedImageLayer(final Map<String, Object> properties) {
super("geoReferencedImageLayer");
setProperties(properties);
setSelectSupported(false);
setQuerySupported(false);
setRenderer(new GeoreferencedImageLayerRenderer(this));
final int opacity = Maps.getInteger(properties, "opacity", 255);
setOpacity(opacity);
setIcon(Icons.getIcon("picture"));
}
public void cancelChanges() {
if (this.image == null && this.resource != null) {
GeoreferencedImage image = null;
final Resource imageResource = Resource.getResource(this.url);
if (imageResource.exists()) {
try {
image = GeoreferencedImageReadFactory.loadGeoreferencedImage(imageResource);
if (image == null) {
Logs.error(GeoreferencedImageLayer.class, "Cannot load image: " + this.url);
}
} catch (final RuntimeException e) {
Logs.error(GeoreferencedImageLayer.class, "Unable to load image: " + this.url, e);
}
} else {
Logs.error(GeoreferencedImageLayer.class, "Image does not exist: " + this.url);
}
setImage(image);
} else {
this.image.cancelChanges();
}
firePropertyChange("hasChanges", true, false);
}
public void deleteTiePoint(final MappedLocation tiePoint) {
if (isEditable()) {
this.image.deleteTiePoint(tiePoint);
} else {
Logs.error(this, "Cannot delete tie-point. Layer " + getPath() + " is not editable");
}
}
public synchronized BoundingBox fitToViewport() {
final Project project = getProject();
if (project == null || this.image == null || !isInitialized()) {
return BoundingBox.empty();
} else {
final BoundingBox oldValue = this.image.getBoundingBox();
final BoundingBox viewBoundingBox = project.getViewBoundingBox();
if (viewBoundingBox.isEmpty()) {
return viewBoundingBox;
} else {
final double viewRatio = viewBoundingBox.getAspectRatio();
final double imageRatio = this.image.getImageAspectRatio();
BoundingBox boundingBox;
if (viewRatio > imageRatio) {
boundingBox = viewBoundingBox.expandPercent(-1 + imageRatio / viewRatio, 0.0);
} else if (viewRatio < imageRatio) {
boundingBox = viewBoundingBox.expandPercent(0.0, -1 + viewRatio / imageRatio);
} else {
boundingBox = viewBoundingBox;
}
this.image.setBoundingBox(boundingBox);
firePropertyChange("boundingBox", oldValue, boundingBox);
return boundingBox;
}
}
}
@Override
public BoundingBox getBoundingBox() {
final GeoreferencedImage image = getImage();
if (image == null) {
return BoundingBox.empty();
} else {
BoundingBox boundingBox = image.getBoundingBox();
if (boundingBox.isEmpty()) {
final boolean hasChanges = isHasChanges();
boundingBox = fitToViewport();
if (hasChanges) {
saveChanges();
} else {
cancelChanges();
}
}
return boundingBox;
}
}
@Override
public BoundingBox getBoundingBox(final boolean visibleLayersOnly) {
if (isExists() && (isVisible() || !visibleLayersOnly)) {
return getBoundingBox();
} else {
return getGeometryFactory().newBoundingBoxEmpty();
}
}
@Override
public GeometryFactory getGeometryFactory() {
if (this.image == null) {
return getBoundingBox().getGeometryFactory();
} else {
return this.image.getGeometryFactory();
}
}
public GeoreferencedImage getImage() {
return this.image;
}
public int getOpacity() {
return this.opacity;
}
@Override
protected boolean initializeDo() {
final String url = getProperty("url");
if (Property.hasValue(url)) {
this.url = url;
this.resource = Resource.getResource(url);
cancelChanges();
return true;
} else {
Logs.error(this, "Layer definition does not contain a 'url' property");
return false;
}
}
public boolean isHasTransform() {
if (this.image == null) {
return false;
} else {
return this.image.isHasTransform();
}
}
public boolean isShowOriginalImage() {
return this.image.isHasTransform() && this.showOriginalImage;
}
@Override
public boolean isVisible() {
return super.isVisible() || isEditable();
}
@Override
public TabbedValuePanel newPropertiesPanel() {
final TabbedValuePanel propertiesPanel = super.newPropertiesPanel();
final TiePointsPanel tiePointsPanel = newTableViewComponent(null);
Borders.titled(tiePointsPanel, "Tie Points");
propertiesPanel.addTab("Geo-Referencing", tiePointsPanel);
return propertiesPanel;
}
@Override
protected ValueField newPropertiesTabGeneralPanelSource(final BasePanel parent) {
final ValueField panel = super.newPropertiesTabGeneralPanelSource(parent);
if (this.url.startsWith("file:")) {
final String fileName = this.url.replaceFirst("file:(//)?", "");
SwingUtil.addLabelledReadOnlyTextField(panel, "File", fileName);
} else {
SwingUtil.addLabelledReadOnlyTextField(panel, "URL", this.url);
}
final String fileNameExtension = FileUtil.getFileNameExtension(this.url);
if (Property.hasValue(fileNameExtension)) {
SwingUtil.addLabelledReadOnlyTextField(panel, "File Extension", fileNameExtension);
final GeoreferencedImageReadFactory factory = IoFactory
.factoryByFileExtension(GeoreferencedImageReadFactory.class, fileNameExtension);
if (factory != null) {
SwingUtil.addLabelledReadOnlyTextField(panel, "File Type", factory.getName());
}
}
GroupLayouts.makeColumns(panel, 2, true);
return panel;
}
@Override
protected TiePointsPanel newTableViewComponent(final Map<String, Object> config) {
return new TiePointsPanel(this);
}
@Override
public void propertyChange(final PropertyChangeEvent event) {
super.propertyChange(event);
final String propertyName = event.getPropertyName();
if ("hasChanges".equals(propertyName)) {
final GeoreferencedImage image = getImage();
if (event.getSource() == image) {
image.saveChanges();
}
}
}
protected void saveImageChanges() {
if (this.image != null) {
this.image.saveChanges();
}
}
@Override
public void setBoundingBox(final BoundingBox boundingBox) {
if (this.image != null) {
this.image.setBoundingBox(boundingBox);
}
}
@Override
public void setEditable(final boolean editable) {
Invoke.background("Set Editable " + this, () -> {
synchronized (getSync()) {
if (editable) {
setShowOriginalImage(true);
} else {
firePropertyChange("preEditable", false, true);
if (isHasChanges()) {
final Integer result = Invoke.andWait(() -> {
return JOptionPane.showConfirmDialog(JOptionPane.getRootFrame(),
"The layer has unsaved changes. Click Yes to save changes. Click No to discard changes. Click Cancel to continue editing.",
"Save Changes", JOptionPane.YES_NO_CANCEL_OPTION);
});
if (result == JOptionPane.YES_OPTION) {
if (!saveChanges()) {
setVisible(true);
return;
}
} else if (result == JOptionPane.NO_OPTION) {
cancelChanges();
} else {
setVisible(true);
// Don't allow state change if cancelled
return;
}
}
}
super.setEditable(editable);
if (editable == false) {
setShowOriginalImage(false);
}
}
});
}
public void setImage(final GeoreferencedImage image) {
final GeoreferencedImage old = this.image;
Property.removeListener(this.image, this);
this.image = image;
if (image == null) {
setExists(false);
} else {
setExists(true);
Property.addListener(image, this);
}
firePropertyChange("image", old, this.image);
}
public void setOpacity(int opacity) {
final int oldValue = this.opacity;
if (opacity < 0) {
opacity = 0;
} else if (opacity > 255) {
opacity = 255;
}
this.opacity = opacity;
firePropertyChange("opacity", oldValue, opacity);
}
public void setShowOriginalImage(final boolean showOriginalImage) {
final Object oldValue = this.showOriginalImage;
this.showOriginalImage = showOriginalImage;
firePropertyChange("showOriginalImage", oldValue, showOriginalImage);
}
@Override
public void setVisible(final boolean visible) {
super.setVisible(visible);
if (!visible) {
setEditable(false);
}
}
public Point sourcePixelToTargetPoint(final BoundingBox boundingBox, final boolean useTransform,
final double... coordinates) {
if (useTransform) {
final AffineTransform transform = this.image.getAffineTransformation(boundingBox);
transform.transform(coordinates, 0, coordinates, 0, 1);
}
final double imageX = coordinates[0];
final double imageY = coordinates[1];
final GeoreferencedImage image = getImage();
final double xPercent = imageX / image.getImageWidth();
final double yPercent = imageY / image.getImageHeight();
final double modelWidth = boundingBox.getWidth();
final double modelHeight = boundingBox.getHeight();
final double modelX = boundingBox.getMinX() + modelWidth * xPercent;
final double modelY = boundingBox.getMinY() + modelHeight * yPercent;
final GeometryFactory geometryFactory = boundingBox.getGeometryFactory();
final Point imagePoint = geometryFactory.point(modelX, modelY);
return imagePoint;
}
public Point sourcePixelToTargetPoint(final MappedLocation tiePoint) {
final Point sourcePixel = tiePoint.getSourcePixel();
return sourcePixelToTargetPoint(sourcePixel);
}
public Point sourcePixelToTargetPoint(final Point sourcePixel) {
final BoundingBox boundingBox = getBoundingBox();
final double x = sourcePixel.getX();
final double y = sourcePixel.getY();
final boolean useTransform = !isShowOriginalImage();
return sourcePixelToTargetPoint(boundingBox, useTransform, x, y);
}
public Point targetPointToSourcePixel(Point targetPoint) {
final GeoreferencedImage image = getImage();
final BoundingBox boundingBox = getBoundingBox();
targetPoint = targetPoint.convertPoint2d(boundingBox.getGeometryFactory());
final double modelX = targetPoint.getX();
final double modelY = targetPoint.getY();
final double modelDeltaX = modelX - boundingBox.getMinX();
final double modelDeltaY = modelY - boundingBox.getMinY();
final double modelWidth = boundingBox.getWidth();
final double modelHeight = boundingBox.getHeight();
final double xRatio = modelDeltaX / modelWidth;
final double yRatio = modelDeltaY / modelHeight;
final double imageX = image.getImageWidth() * xRatio;
final double imageY = image.getImageHeight() * yRatio;
final double[] coordinates = new double[] {
imageX, imageY
};
if (!isShowOriginalImage()) {
try {
final AffineTransform transform = image.getAffineTransformation(boundingBox)
.createInverse();
transform.transform(coordinates, 0, coordinates, 0, 1);
} catch (final NoninvertibleTransformException e) {
}
}
return new PointDoubleXY(Doubles.makePrecise(1, coordinates[0]),
Doubles.makePrecise(1, coordinates[1]));
}
public void toggleShowOriginalImage() {
final boolean showOriginalImage = isShowOriginalImage();
setShowOriginalImage(!showOriginalImage);
}
@Override
public MapEx toMap() {
final MapEx map = super.toMap();
map.remove("querySupported");
map.remove("selectSupported");
map.remove("editable");
map.remove("showOriginalImage");
map.remove("imageSettings");
addToMap(map, "url", this.url);
addToMap(map, "opacity", this.opacity, 1);
return map;
}
@Override
public void zoomToLayer() {
final Project project = getProject();
final GeometryFactory geometryFactory = project.getGeometryFactory();
final BoundingBox layerBoundingBox = getBoundingBox();
BoundingBox boundingBox = layerBoundingBox;
final AffineTransform transform = this.image.getAffineTransformation(layerBoundingBox);
if (!transform.isIdentity()) {
final GeoreferencedImage image = getImage();
final double width = image.getImageWidth() - 1;
final double height = image.getImageHeight() - 1;
final double[] targetCoordinates = MappedLocation.toModelCoordinates(image, layerBoundingBox,
true, 0, height, width, height, width, 0, 0, 0, 0, height);
final LineString line = layerBoundingBox.getGeometryFactory().lineString(2,
targetCoordinates);
boundingBox = boundingBox.expandToInclude(line);
}
boundingBox = boundingBox.convert(geometryFactory).expandPercent(0.1).clipToCoordinateSystem();
project.setViewBoundingBox(boundingBox);
}
}