/* * Copyright (C) 2012 Brockmann Consult GmbH (info@brockmann-consult.de) * * 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 3 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, see http://www.gnu.org/licenses/ */ package org.esa.snap.rcp.magicwand; import com.bc.ceres.core.Assert; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.SingleValueConverter; import org.esa.snap.core.datamodel.Band; import org.esa.snap.core.datamodel.Mask; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.dataop.barithm.BandArithmetic; import org.esa.snap.core.jexp.ParseException; import org.esa.snap.core.util.ObjectUtils; import org.esa.snap.core.util.StringUtils; import java.awt.Color; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Utilities for the magic wand tool (interactor). * * @author Norman Fomferra * @since BEAM 4.10 */ public class MagicWandModel implements Cloneable { public enum PickMode { SINGLE, PLUS, MINUS, } public enum SpectrumTransform { INTEGRAL, IDENTITY, DERIVATIVE, } public enum PixelTest { DISTANCE, AVERAGE, LIMITS, } public static final String MAGIC_WAND_MASK_NAME = "magic_wand"; private double tolerance; private double minTolerance; private double maxTolerance; private ArrayList<String> bandNames; private SpectrumTransform spectrumTransform; private PixelTest pixelTest; private boolean normalize; private PickMode pickMode; private ArrayList<double[]> plusSpectra; private ArrayList<double[]> minusSpectra; private transient ArrayList<Listener> listeners; public MagicWandModel() { pickMode = PickMode.SINGLE; pixelTest = PixelTest.DISTANCE; spectrumTransform = SpectrumTransform.IDENTITY; bandNames = new ArrayList<>(); plusSpectra = new ArrayList<>(); minusSpectra = new ArrayList<>(); tolerance = 0.1; minTolerance = 0.0; maxTolerance = 1.0; } public void addListener(Listener listener) { if (listeners == null) { listeners = new ArrayList<>(); } listeners.add(listener); } public void removeListeners() { if (listeners != null) { listeners.clear(); listeners = null; } } public void fireModelChanged(boolean recomputeMask) { if (listeners != null) { for (Listener listener : listeners) { listener.modelChanged(this, recomputeMask); } } } @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") @Override public MagicWandModel clone() { try { MagicWandModel clone = (MagicWandModel) super.clone(); clone.bandNames = new ArrayList<>(bandNames); clone.plusSpectra = new ArrayList<>(plusSpectra); clone.minusSpectra = new ArrayList<>(minusSpectra); clone.listeners = null; return clone; } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); } } public void assign(MagicWandModel other) { pixelTest = other.pixelTest; spectrumTransform = other.spectrumTransform; pickMode = other.pickMode; tolerance = other.tolerance; minTolerance = other.minTolerance; maxTolerance = other.maxTolerance; bandNames = new ArrayList<>(other.bandNames); plusSpectra = new ArrayList<>(other.plusSpectra); minusSpectra = new ArrayList<>(other.minusSpectra); fireModelChanged(true); } int getSpectrumCount() { return plusSpectra.size() + minusSpectra.size(); } int getPlusSpectrumCount() { return plusSpectra.size(); } int getMinusSpectrumCount() { return minusSpectra.size(); } int getBandCount() { return bandNames.size(); } void addSpectrum(double... spectrum) { Assert.argument(spectrum.length == bandNames.size(), "spectrum size does not match # selected bands"); if (pickMode == PickMode.SINGLE) { plusSpectra.clear(); minusSpectra.clear(); plusSpectra.add(spectrum); } else if (pickMode == PickMode.PLUS) { plusSpectra.add(spectrum); } else if (pickMode == PickMode.MINUS) { minusSpectra.add(spectrum); } fireModelChanged(true); } void clearSpectra() { plusSpectra.clear(); minusSpectra.clear(); fireModelChanged(true); } public List<String> getBandNames() { return Collections.unmodifiableList(bandNames); } public void setBandNames(String... bandNames) { setBandNames(Arrays.asList(bandNames)); } public void setBandNames(List<String> bandNames) { plusSpectra.clear(); minusSpectra.clear(); this.bandNames = new ArrayList<>(bandNames); fireModelChanged(true); } public PickMode getPickMode() { return pickMode; } public void setPickMode(PickMode pickMode) { this.pickMode = pickMode; fireModelChanged(false); } public PixelTest getPixelTest() { return pixelTest; } public void setPixelTest(PixelTest pixelTest) { this.pixelTest = pixelTest; fireModelChanged(false); } public SpectrumTransform getSpectrumTransform() { return spectrumTransform; } public void setSpectrumTransform(SpectrumTransform spectrumTransform) { this.spectrumTransform = spectrumTransform; fireModelChanged(false); } public double getTolerance() { return tolerance; } public void setTolerance(double tolerance) { this.tolerance = tolerance; fireModelChanged(true); } public double getMinTolerance() { return minTolerance; } public void setMinTolerance(double minTolerance) { this.minTolerance = minTolerance; fireModelChanged(false); } public double getMaxTolerance() { return maxTolerance; } public void setMaxTolerance(double maxTolerance) { this.maxTolerance = maxTolerance; fireModelChanged(false); } public void setNormalize(boolean normalize) { this.normalize = normalize; fireModelChanged(false); } public void setSpectralBandNames(Product product) { List<Band> bands = getSpectralBands(product); List<String> bandNames = new ArrayList<>(); for (Band band : bands) { bandNames.add(band.getName()); } if (!bandNames.isEmpty()) { setBandNames(bandNames.toArray(new String[bandNames.size()])); } } public static List<Band> getSpectralBands(Product product) { List<Band> bands = new ArrayList<>(); for (Band band : product.getBands()) { if (band.getSpectralWavelength() > 0.0 || band.getSpectralBandIndex() >= 0) { bands.add(band); } } Collections.sort(bands, new SpectralBandComparator()); return bands; } List<Band> getBands(Product product) { List<String> names = getBandNames(); List<Band> bands = new ArrayList<>(names.size()); for (String name : names) { Band band = product.getBand(name); if (band == null) { return null; } bands.add(band); } return bands; } static void setMagicWandMask(Product product, String expression) { String validMaskExpression; try { validMaskExpression = BandArithmetic.getValidMaskExpression(expression, product, null); } catch (ParseException e) { validMaskExpression = null; } if (validMaskExpression != null) { expression = "(" + validMaskExpression + ") && (" + expression + ")"; } final Mask magicWandMask = product.getMaskGroup().get(MAGIC_WAND_MASK_NAME); if (magicWandMask != null) { magicWandMask.getImageConfig().setValue("expression", expression); } else { product.addMask(MAGIC_WAND_MASK_NAME, expression, "Magic wand mask", Color.RED, 0.5); } } String createMaskExpression() { final String plusPart; final String minusPart; if (getPixelTest() == PixelTest.DISTANCE) { plusPart = getDistancePart(bandNames, spectrumTransform, plusSpectra, tolerance, normalize); minusPart = getDistancePart(bandNames, spectrumTransform, minusSpectra, tolerance, normalize); } else if (getPixelTest() == PixelTest.AVERAGE) { plusPart = getAveragePart(bandNames, spectrumTransform, plusSpectra, tolerance, normalize); minusPart = getAveragePart(bandNames, spectrumTransform, minusSpectra, tolerance, normalize); } else if (getPixelTest() == PixelTest.LIMITS) { plusPart = getLimitsPart(bandNames, spectrumTransform, plusSpectra, tolerance, normalize); minusPart = getLimitsPart(bandNames, spectrumTransform, minusSpectra, tolerance, normalize); } else { throw new IllegalStateException("Unhandled method " + getPixelTest()); } if (plusPart != null && minusPart != null) { return String.format("(%s) && !(%s)", plusPart, minusPart); } else if (plusPart != null) { return plusPart; } else if (minusPart != null) { return String.format("!(%s)", minusPart); } else { return "0"; } } private static String getDistancePart(List<String> bandNames, SpectrumTransform spectrumTransform, List<double[]> spectra, double tolerance, boolean normalize) { if (spectra.isEmpty()) { return null; } final StringBuilder part = new StringBuilder(); for (int i = 0; i < spectra.size(); i++) { double[] spectrum = getSpectrum(spectra.get(i), normalize); if (i > 0) { part.append(" || "); } part.append(getDistanceSubPart(bandNames, spectrumTransform, spectrum, tolerance, normalize)); } return part.toString(); } private static String getAveragePart(List<String> bandNames, SpectrumTransform spectrumTransform, List<double[]> spectra, double tolerance, boolean normalize) { if (spectra.isEmpty()) { return null; } double[] avgSpectrum = getAvgSpectrum(bandNames.size(), spectra, normalize); return getDistanceSubPart(bandNames, spectrumTransform, avgSpectrum, tolerance, normalize); } private static String getLimitsPart(List<String> bandNames, SpectrumTransform spectrumTransform, List<double[]> spectra, double tolerance, boolean normalize) { if (spectra.isEmpty()) { return null; } double[] minSpectrum = getMinSpectrum(bandNames.size(), spectra, tolerance, normalize); double[] maxSpectrum = getMaxSpectrum(bandNames.size(), spectra, tolerance, normalize); return getLimitsSubPart(bandNames, spectrumTransform, minSpectrum, maxSpectrum, normalize); } private static double[] getSpectrum(double[] spectrum, boolean normalize) { if (normalize) { double[] normSpectrum = new double[spectrum.length]; for (int i = 0; i < spectrum.length; i++) { normSpectrum[i] = getSpectrumValue(spectrum, i, true); } return normSpectrum; } else { return spectrum; } } private static double[] getAvgSpectrum(int numBands, List<double[]> spectra, boolean normalize) { double[] avgSpectrum = new double[numBands]; for (double[] spectrum : spectra) { for (int i = 0; i < spectrum.length; i++) { double value = getSpectrumValue(spectrum, i, normalize); avgSpectrum[i] += value; } } for (int i = 0; i < avgSpectrum.length; i++) { avgSpectrum[i] /= spectra.size(); } return avgSpectrum; } private static double[] getMinSpectrum(int numBands, List<double[]> spectra, double tolerance, boolean normalize) { double[] minSpectrum = new double[numBands]; Arrays.fill(minSpectrum, +Double.MAX_VALUE); for (double[] spectrum : spectra) { for (int i = 0; i < spectrum.length; i++) { double value = getSpectrumValue(spectrum, i, normalize); minSpectrum[i] = Math.min(minSpectrum[i], value - tolerance); } } return minSpectrum; } private static double[] getMaxSpectrum(int numBands, List<double[]> spectra, double tolerance, boolean normalize) { double[] maxSpectrum = new double[numBands]; Arrays.fill(maxSpectrum, -Double.MAX_VALUE); for (double[] spectrum : spectra) { for (int i = 0; i < spectrum.length; i++) { double value = getSpectrumValue(spectrum, i, normalize); maxSpectrum[i] = Math.max(maxSpectrum[i], value + tolerance); } } return maxSpectrum; } private static double getSpectrumValue(double[] spectrum, int i, boolean normalize) { return normalize ? spectrum[i] / spectrum[0] : spectrum[i]; } private static String getDistanceSubPart(List<String> bandNames, SpectrumTransform spectrumTransform, double[] spectrum, double tolerance, boolean normalize) { if (bandNames.isEmpty()) { return "0"; } final StringBuilder arguments = new StringBuilder(); appendSpectrumBandNames(bandNames, normalize, arguments); appendSpectrumBandValues(spectrum, arguments); String functionName; if (spectrumTransform == SpectrumTransform.IDENTITY) { functionName = "distance"; } else if (spectrumTransform == SpectrumTransform.DERIVATIVE) { functionName = "distance_deriv"; } else if (spectrumTransform == SpectrumTransform.INTEGRAL) { functionName = "distance_integ"; } else { throw new IllegalStateException("unhandled operator " + spectrumTransform); } if (bandNames.size() == 1) { return String.format("%s(%s) < %s", functionName, arguments, tolerance); } else { return String.format("%s(%s)/%s < %s", functionName, arguments, bandNames.size(), tolerance); } } private static String getLimitsSubPart(List<String> bandNames, SpectrumTransform spectrumTransform, double[] minSpectrum, double[] maxSpectrum, boolean normalize) { if (bandNames.isEmpty()) { return "0"; } final StringBuilder arguments = new StringBuilder(); appendSpectrumBandNames(bandNames, normalize, arguments); appendSpectrumBandValues(minSpectrum, arguments); appendSpectrumBandValues(maxSpectrum, arguments); String functionName; if (spectrumTransform == SpectrumTransform.IDENTITY) { functionName = "inrange"; } else if (spectrumTransform == SpectrumTransform.DERIVATIVE) { functionName = "inrange_deriv"; } else if (spectrumTransform == SpectrumTransform.INTEGRAL) { functionName = "inrange_integ"; } else { throw new IllegalStateException("unhandled operator " + spectrumTransform); } return String.format("%s(%s)", functionName, arguments); } private static void appendSpectrumBandNames(List<String> bandNames, boolean normalize, StringBuilder arguments) { String firstName = BandArithmetic.createExternalName(bandNames.get(0)); for (int i = 0; i < bandNames.size(); i++) { if (i > 0) { arguments.append(","); } String name = BandArithmetic.createExternalName(bandNames.get(i)); if (normalize) { arguments.append(name).append("/").append(firstName); } else { arguments.append(name); } } } private static void appendSpectrumBandValues(double[] spectrum, StringBuilder arguments) { for (double value : spectrum) { arguments.append(","); arguments.append(value); } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MagicWandModel that = (MagicWandModel) o; if (Double.compare(that.maxTolerance, maxTolerance) != 0) return false; if (Double.compare(that.minTolerance, minTolerance) != 0) return false; if (normalize != that.normalize) return false; if (Double.compare(that.tolerance, tolerance) != 0) return false; if (pixelTest != that.pixelTest) return false; if (pickMode != that.pickMode) return false; if (spectrumTransform != that.spectrumTransform) return false; if (!ObjectUtils.equalObjects(bandNames, that.bandNames)) return false; if (!ObjectUtils.equalObjects(plusSpectra.toArray(), that.plusSpectra.toArray())) return false; if (!ObjectUtils.equalObjects(minusSpectra.toArray(), that.minusSpectra.toArray())) return false; return true; } @Override public int hashCode() { int result; long temp; result = pickMode.hashCode(); result = 31 * result + spectrumTransform.hashCode(); result = 31 * result + pixelTest.hashCode(); result = 31 * result + plusSpectra.hashCode(); result = 31 * result + minusSpectra.hashCode(); result = 31 * result + bandNames.hashCode(); temp = tolerance != +0.0d ? Double.doubleToLongBits(tolerance) : 0L; result = 31 * result + (int) (temp ^ (temp >>> 32)); result = 31 * result + (normalize ? 1 : 0); temp = minTolerance != +0.0d ? Double.doubleToLongBits(minTolerance) : 0L; result = 31 * result + (int) (temp ^ (temp >>> 32)); temp = maxTolerance != +0.0d ? Double.doubleToLongBits(maxTolerance) : 0L; result = 31 * result + (int) (temp ^ (temp >>> 32)); return result; } public static MagicWandModel fromXml(String xml) { return (MagicWandModel) createXStream().fromXML(xml); } public String toXml() { return createXStream().toXML(this); } private static XStream createXStream() { final XStream xStream = new XStream(); xStream.alias("magicWandSettings", MagicWandModel.class); xStream.registerConverter(new SingleValueConverter() { @Override public boolean canConvert(Class type) { return type.equals(double[].class); } @Override public String toString(Object obj) { return StringUtils.arrayToString(obj, ","); } @Override public Object fromString(String str) { return StringUtils.toDoubleArray(str, ","); } }); return xStream; } public static interface Listener { void modelChanged(MagicWandModel model, boolean recomputeMask); } }