/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* 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., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package org.breakout.model;
import static org.andork.util.StringUtils.toStringOrNull;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.andork.collect.CollectionUtils;
import org.andork.math.misc.AngleUtils;
import org.andork.math3d.Vecmath;
import org.andork.q.QObject;
import org.andork.q.QSpec;
import org.andork.swing.async.Subtask;
import org.andork.swing.table.AnnotatingTableRowSorter.AbstractTableModelCopier;
import org.andork.swing.table.EasyTableModel;
import org.andork.swing.table.QObjectRowFormat;
@SuppressWarnings("serial")
public class SurveyTableModel extends EasyTableModel<QObject<SurveyTableModel.Row>> {
public static class Row extends QSpec<Row> {
public static final Attribute<String> from = newAttribute(String.class, "from");
public static final Attribute<String> to = newAttribute(String.class, "to");
public static final Attribute<String> distance = newAttribute(String.class, "dist");
public static final Attribute<String> fsAzm = newAttribute(String.class, "fsAzm");
public static final Attribute<String> fsInc = newAttribute(String.class, "fsInc");
public static final Attribute<String> bsAzm = newAttribute(String.class, "bsAzm");
public static final Attribute<String> bsInc = newAttribute(String.class, "bsInc");
public static final Attribute<CrossSectionType> xSectionType = newAttribute(CrossSectionType.class,
"xSectionType");
public static final Attribute<ShotSide> xSectionSide = newAttribute(ShotSide.class, "xSectionSide");
public static final Attribute<String> left = newAttribute(String.class, "left");
public static final Attribute<String> right = newAttribute(String.class, "right");
public static final Attribute<String> up = newAttribute(String.class, "up");
public static final Attribute<String> down = newAttribute(String.class, "down");
public static final Attribute<ShotSide> positionSide = newAttribute(ShotSide.class, "positionSide");
public static final Attribute<String> north = newAttribute(String.class, "north");
public static final Attribute<String> east = newAttribute(String.class, "east");
public static final Attribute<String> elev = newAttribute(String.class, "elev");
public static final Attribute<String> desc = newAttribute(String.class, "desc");
public static final Attribute<String> date = newAttribute(String.class, "date");
public static final Attribute<String> surveyors = newAttribute(String.class, "surveyors");
public static final Attribute<String> comment = newAttribute(String.class, "comment");
public static final Attribute<String> scannedNotes = newAttribute(String.class, "scannedNotes");
public static final Row instance = new Row();
private Row() {
super();
}
}
public static class SurveyTableModelCopier extends AbstractTableModelCopier<SurveyTableModel> {
public SurveyTableModel copy(SurveyTableModel src) {
SurveyTableModel dest = createEmptyCopy(src);
for (int row = 0; row < src.getRowCount(); row++) {
copyRow(src, row, dest);
}
return dest;
}
@Override
public SurveyTableModel createEmptyCopy(SurveyTableModel model) {
return new SurveyTableModel();
}
}
/**
*
*/
private static final long serialVersionUID = -2919165714804950483L;
private static float coalesceNaNOrInf(float a, float b) {
return Float.isNaN(a) || Float.isInfinite(a) ? b : a;
}
private static Station getStation(Map<String, Station> stations, String name) {
Station station = stations.get(name);
if (station == null) {
station = new Station();
station.name = name;
stations.put(name, station);
}
return station;
}
private static double parse(Object o) {
if (o == null) {
return Double.NaN;
}
try {
return Double.valueOf(o.toString());
} catch (Exception ex) {
return Double.NaN;
}
}
private static float parseFloat(Object o) {
if (o == null) {
return Float.NaN;
}
try {
return Float.valueOf(o.toString());
} catch (Exception ex) {
return Float.NaN;
}
}
private static void updateCrossSections(Station station) {
if (station.shots.size() == 2) {
Iterator<Shot> shotIter = station.shots.iterator();
Shot shot1 = shotIter.next();
Shot shot2 = shotIter.next();
CrossSection sect1 = shot1.crossSectionAt(station);
CrossSection sect2 = shot2.crossSectionAt(station);
boolean opposite = station == shot1.from == (station == shot2.from);
for (int i = 0; i < Math.min(sect1.dist.length, sect2.dist.length); i++) {
int oi = i > 1 ? i : opposite ? 1 - i : i;
if (Double.isNaN(sect1.dist[i])) {
sect1.dist[i] = sect2.dist[oi];
}
if (Double.isNaN(sect2.dist[i])) {
sect2.dist[i] = sect1.dist[oi];
}
}
}
int populatedCount = CollectionUtils.moveToFront(station.shots, shot -> {
CrossSection section = shot.crossSectionAt(station);
return section.type == CrossSectionType.LRUD && !Double.isNaN(section.dist[0])
&& !Double.isNaN(section.dist[1]);
});
for (int i = populatedCount; i < station.shots.size(); i++) {
Shot shot = station.shots.get(i);
CrossSection section = shot.crossSectionAt(station);
if (section.type == CrossSectionType.LRUD) {
double leftAzm = shot.azm - Math.PI * 0.5;
double rightAzm = shot.azm + Math.PI * 0.5;
boolean populateLeft = Double.isNaN(section.dist[0]);
boolean populateRight = Double.isNaN(section.dist[0]);
for (int i2 = 0; i2 < populatedCount; i2++) {
Shot populated = station.shots.get(i2);
CrossSection popCrossSection = populated.crossSectionAt(station);
double popLeftAzm = populated.azm - Math.PI * 0.5;
double popRightAzm = populated.azm + Math.PI * 0.5;
if (populateLeft) {
double candidateLeft;
candidateLeft = popCrossSection.dist[0] * Math.cos(AngleUtils.angle(leftAzm, popLeftAzm));
section.dist[0] = (float) Vecmath.nmax(section.dist[0], candidateLeft);
candidateLeft = popCrossSection.dist[1] * Math.cos(AngleUtils.angle(leftAzm, popRightAzm));
section.dist[0] = (float) Vecmath.nmax(section.dist[0], candidateLeft);
}
if (populateRight) {
double candidateRight;
candidateRight = popCrossSection.dist[0] * Math.cos(AngleUtils.angle(rightAzm, popLeftAzm));
section.dist[1] = (float) Vecmath.nmax(section.dist[1], candidateRight);
candidateRight = popCrossSection.dist[1] * Math.cos(AngleUtils.angle(rightAzm, popRightAzm));
section.dist[1] = (float) Vecmath.nmax(section.dist[1], candidateRight);
}
}
}
}
for (Shot shot : station.shots) {
CrossSection sect1 = shot.crossSectionAt(station);
CrossSection sect2 = shot.crossSectionAt(shot.otherStation(station));
for (int i = 0; i < Math.min(sect1.dist.length, sect2.dist.length); i++) {
if (Double.isNaN(sect1.dist[i])) {
sect1.dist[i] = sect2.dist[i];
}
}
}
}
private Map<Integer, Integer> shotNumberToRowIndexMap = CollectionUtils.newHashMap();
private final List<Shot> shots = new ArrayList<Shot>();
public SurveyTableModel() {
super(true);
setPrototypeFormat(new QObjectRowFormat<Row>(Row.instance));
fixEndRows();
}
public void clear() {
setShots(Collections.<Shot> emptyList());
setRows(Collections.singletonList(Row.instance.newObject()));
}
@Override
public void copyRowsFrom(EasyTableModel<QObject<Row>> src, int srcStart, int srcEnd, int myStart) {
super.copyRowsFrom(src, srcStart, srcEnd, myStart);
fixEndRows();
}
public List<Shot> createShots(Subtask subtask) {
if (subtask != null) {
subtask.setTotal(getRowCount());
}
Map<String, Station> stations = new LinkedHashMap<String, Station>();
Map<String, Shot> shots = new LinkedHashMap<String, Shot>();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
List<Shot> shotList = new ArrayList<Shot>();
for (int i = 0; i < getRowCount(); i++) {
QObject<Row> row = getRow(i);
Shot shot = null;
try {
String fromName = toStringOrNull(row.get(Row.from));
String toName = toStringOrNull(row.get(Row.to));
double dist = parse(row.get(Row.distance));
double fsAzm = Math.toRadians(parse(row.get(Row.fsAzm)));
double bsAzm = Math.toRadians(parse(row.get(Row.bsAzm)));
double fsInc = Math.toRadians(parse(row.get(Row.fsInc)));
double bsInc = Math.toRadians(parse(row.get(Row.bsInc)));
CrossSectionType xSectionType = row.get(Row.xSectionType);
if (xSectionType == null) {
xSectionType = CrossSectionType.LRUD;
}
ShotSide xSectionSide = row.get(Row.xSectionSide);
if (xSectionSide == null) {
xSectionSide = ShotSide.AT_TO;
}
float left = parseFloat(row.get(Row.left));
float right = parseFloat(row.get(Row.right));
float up = parseFloat(row.get(Row.up));
float down = parseFloat(row.get(Row.down));
ShotSide positionSide = row.get(Row.positionSide);
if (positionSide == null) {
positionSide = ShotSide.AT_TO;
}
if (fromName == null || toName == null) {
continue;
}
shot = shots.get(Shot.getName(fromName, toName));
if (shot == null) {
shot = shots.get(Shot.getName(toName, fromName));
if (shot != null) {
shot = new Shot();
String s = fromName;
fromName = toName;
toName = s;
double d = fsAzm;
fsAzm = bsAzm;
bsAzm = d;
d = fsInc;
fsInc = bsInc;
bsInc = d;
xSectionSide = xSectionSide.opposite();
positionSide = positionSide.opposite();
} else {
if (Double.isNaN(dist) || Double.isNaN(fsInc) && Double.isNaN(bsInc)) {
continue;
}
}
}
double north = parse(row.get(Row.north));
double east = parse(row.get(Row.east));
double elev = parse(row.get(Row.elev));
Station from = getStation(stations, fromName);
Station to = getStation(stations, toName);
Vecmath.setdNoNaNOrInf(positionSide == ShotSide.AT_FROM ? from.position : to.position, east, elev,
-north);
shot = new Shot();
shot.from = from;
shot.to = to;
shot.dist = dist;
shot.inc = Shot.averageInc(fsInc, bsInc);
shot.azm = Shot.averageAzm(shot.inc, fsAzm, bsAzm);
shot.desc = row.get(Row.desc);
try {
shot.date = dateFormat.parse(row.get(Row.date));
} catch (Exception ex) {
}
CrossSection xSection = xSectionSide == ShotSide.AT_FROM ? shot.fromXsection : shot.toXsection;
xSection.type = xSectionType;
xSection.dist[0] = coalesceNaNOrInf(left, xSection.dist[0]);
xSection.dist[1] = coalesceNaNOrInf(right, xSection.dist[1]);
xSection.dist[2] = coalesceNaNOrInf(up, xSection.dist[2]);
xSection.dist[3] = coalesceNaNOrInf(down, xSection.dist[3]);
if (subtask != null) {
if (subtask.isCanceling()) {
return null;
}
subtask.setCompleted(i);
}
} catch (Exception ex) {
shot = null;
} finally {
if (shot != null) {
shots.put(shotName(shot), shot);
}
// DO add null shots to shotList
shotList.add(shot);
}
}
for (Shot shot : shots.values()) {
shot.from.shots.add(shot);
shot.to.shots.add(shot);
}
for (Station station : stations.values()) {
updateCrossSections(station);
}
int number = 0;
for (Shot shot : shotList) {
if (shot != null) {
shot.number = number++;
}
}
return shotList;
}
private void fixEndRows() {
int startOfEmptyRows = getRowCount();
while (startOfEmptyRows > 0 && isEmpty(startOfEmptyRows - 1)) {
startOfEmptyRows--;
}
if (startOfEmptyRows == getRowCount()) {
addRow(Row.instance.newObject());
} else if (startOfEmptyRows <= getRowCount() - 2) {
removeRows(startOfEmptyRows, getRowCount() - 2);
}
}
private boolean isEmpty(int row) {
for (int column = 0; column < getColumnCount(); column++) {
Object value = getValueAt(row, column);
if (value != null && !"".equals(value)) {
return false;
}
}
return true;
}
public void rebuildShotNumberToRowMap() {
shotNumberToRowIndexMap.clear();
for (int i = 0; i < shots.size(); i++) {
Shot shot = shots.get(i);
if (shot != null) {
shotNumberToRowIndexMap.put(shot.number, i);
}
}
}
public int rowOfShot(int shotNumber) {
Integer row = shotNumberToRowIndexMap.get(shotNumber);
return row == null ? -1 : row;
}
@Override
public void setRow(int index, QObject<Row> row) {
while (index >= getRowCount()) {
addRow(Row.instance.newObject());
}
super.setRow(index, row);
if (index >= getRowCount() - 2) {
fixEndRows();
}
}
public void setShots(List<Shot> shotList) {
shots.clear();
shots.addAll(shotList);
rebuildShotNumberToRowMap();
}
@Override
public void setValueAt(Object aValue, int row, int column, boolean fireEvent) {
while (row >= getRowCount()) {
addRow(Row.instance.newObject());
}
Object prevValue = getValueAt(row, column);
super.setValueAt(aValue, row, column, fireEvent);
if (prevValue == null || "".equals(prevValue) != (aValue == null || "".equals(aValue))) {
fixEndRows();
}
}
public Shot shotAtRow(int rowIndex) {
if (rowIndex < 0) {
throw new IndexOutOfBoundsException("row index out of bounds: " + rowIndex + " < 0");
}
if (rowIndex >= getRowCount()) {
throw new IndexOutOfBoundsException("row index out of bounds: " + rowIndex + " >= " + getRowCount());
}
return rowIndex < shots.size() ? shots.get(rowIndex) : null;
}
protected String shotName(Shot shot) {
return shot.from.name + " - " + shot.to.name;
}
}