package org.goko.tools.autoleveler.bean.grid;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections.CollectionUtils;
import org.goko.core.common.exception.GkException;
import org.goko.core.common.exception.GkTechnicalException;
import org.goko.core.common.measure.Units;
import org.goko.core.common.measure.quantity.Length;
import org.goko.core.common.measure.quantity.Speed;
import org.goko.core.common.utils.AbstractIdBean;
import org.goko.core.math.Tuple6b;
import org.goko.tools.autoleveler.bean.IHeightMap;
public class GridHeightMap extends AbstractIdBean implements IHeightMap {
/** The spatial repartition of positions */
private int[][] vertices;
/** The indexed list of position */
private List<Tuple6b> offsets;
/** The start point of this map */
private Tuple6b start;
/** The end point of this map */
private Tuple6b end;
/** Number of divisions on the X axis*/
private int xDivisionCount;
/** Number of divisions on the Y axis*/
private int yDivisionCount;
/** The clearance height */
private Length clearanceHeight;
/** The probe start height */
private Length probeStartHeight;
/** The probe lower height */
private Length probeLowerHeight;
/** The probe feed rate */
private Speed probeFeedrate;
/** The move feed rate */
private Speed moveFeedrate;
/** Boolean indicating that the map has been probed */
private boolean isProbed;
/**
* Constructor
* @param vertices the grid of offsets indexes
* @param offsets the list of indexes
*/
public GridHeightMap(int[][] vertices, List<Tuple6b> offsets) {
super();
this.vertices = vertices;
this.offsets = offsets;
this.xDivisionCount = vertices.length - 1;
this.yDivisionCount = vertices[0].length - 1;
this.start = offsets.get( vertices[0][0]);
this.end = offsets.get( vertices[xDivisionCount - 1][yDivisionCount - 1]);
}
/** (inheritDoc)
* @see org.goko.tools.autoleveler.bean.IHeightMap#getHeight(org.goko.core.common.measure.quantity.type.BigDecimalQuantity, org.goko.core.common.measure.quantity.type.BigDecimalQuantity)
*/
@Override
public Length getHeight(Length x, Length y) throws GkException {
Tuple6b clippedPosition = new Tuple6b(x, y, Length.ZERO);
if(x.greaterThan(start.getX()) && x.lowerThan(end.getX())
&& y.greaterThan(start.getY()) && y.lowerThan(end.getY())){
// Clamp the target position in the map area
clippedPosition = clippedPosition.max(start);
clippedPosition = clippedPosition.min(end);
int cellXIndex = getCellXIndex(x);
int cellYIndex = getCellYIndex(y);
// Get the corner position around the target point
Tuple6b pA = offsets.get( vertices[cellXIndex ][cellYIndex ]);
Tuple6b pB = offsets.get( vertices[cellXIndex ][cellYIndex+1]);
Tuple6b pC = offsets.get( vertices[cellXIndex+1][cellYIndex ]);
Tuple6b pD = offsets.get( vertices[cellXIndex+1][cellYIndex+1]);
return findOffsetBilinear(clippedPosition, pA, pB, pC, pD);
}
return null;
}
/** (inheritDoc)
* @see org.goko.tools.autoleveler.bean.IHeightMap#splitSegment(org.goko.core.math.Tuple6b, org.goko.core.math.Tuple6b)
*/
@Override
public List<Tuple6b> splitSegment(final Tuple6b pStart, final Tuple6b pEnd) throws GkException {
List<Tuple6b> lstSubSegment = new ArrayList<Tuple6b>();
List<Tuple6b> lstSubSegmentX = new ArrayList<Tuple6b>();
// The given segment is a purely vertical segment, we won't split it
if( pEnd.getX().equals(pStart.getX())
&& pEnd.getY().equals(pStart.getY())){
lstSubSegment.add(pStart);
lstSubSegment.add(pEnd);
return lstSubSegment;
}
BigDecimal slope = BigDecimal.ZERO;
if( !pEnd.getX().equals(pStart.getX()) ){
slope = (pEnd.getY().subtract(pStart.getY())).divide(pEnd.getX().subtract(pStart.getX()));
}
Tuple6b start = new Tuple6b(pStart);
Tuple6b end = new Tuple6b(pEnd);
start.setZ(Length.ZERO);
end.setZ(Length.ZERO);
{
// Let's divide the segment along the X axis (in this case, segmenting line are along Y axis
List<Length> lstIntersectionXCoordinate = getIntersectingDivisionsX(start, end);
lstSubSegmentX.add(start);
if(CollectionUtils.isNotEmpty(lstIntersectionXCoordinate)){
for (Length xCoord : lstIntersectionXCoordinate) {
Length computedY = start.getY().add( xCoord.subtract(start.getX()).multiply(slope) );
Tuple6b pt = new Tuple6b(Units.MILLIMETRE, xCoord, computedY, Length.ZERO);
lstSubSegmentX.add(pt);
}
}
lstSubSegmentX.add(end);
}
{
// Now we can divise along the Y axis
int iterationMax = lstSubSegmentX.size() - 1;
for (int j = 0; j < iterationMax; j++) {
Tuple6b tmpStart = lstSubSegmentX.get(j);
Tuple6b tmpEnd = lstSubSegmentX.get(j+1);
List<Length> lstIntersectionYCoordinate = getIntersectingDivisionsY(tmpStart, tmpEnd);
lstSubSegment.add(tmpStart);
if(CollectionUtils.isNotEmpty(lstIntersectionYCoordinate)){
for (Length yCoord : lstIntersectionYCoordinate) {
Length computedX = tmpStart.getX();
if(!slope.equals(BigDecimal.ZERO)){
computedX = yCoord.subtract(tmpStart.getY()).divide(slope).add( tmpStart.getX() );
}
Tuple6b pt = new Tuple6b(Units.MILLIMETRE, computedX, yCoord, Length.ZERO);
lstSubSegment.add(pt);
}
}
}
lstSubSegment.add(end);
}
return lstSubSegment;
}
/**
* Returns the ordered list of position along the X axis which intersects the given segment. The points are ordered from start to end
* @param start the start point of the segment
* @param end the end point of the segment
* @return a list of Quantity
*/
private List<Length> getIntersectingDivisionsX(Tuple6b start, Tuple6b end){
List<Length> lstXPosition = new ArrayList<>();
// Start < End
if(start.getX().lowerThanOrEqualTo(end.getX())){
for(int i = 0; i < xDivisionCount ; i++){
Tuple6b gridPoint = offsets.get(vertices[i][0]);
if(gridPoint.getX().greaterThan(start.getX()) && gridPoint.getX().lowerThan(end.getX())){
lstXPosition.add( gridPoint.getX() );
}else if(gridPoint.getX().greaterThanOrEqualTo(end.getX())){
break;
}
}
}else{
// Start > End
for(int i = xDivisionCount - 1; i >= 0 ; i--){
Tuple6b gridPoint = offsets.get(vertices[i][0]);
if(gridPoint.getX().greaterThan(end.getX()) && gridPoint.getX().lowerThan(start.getX())){
lstXPosition.add( gridPoint.getX() );
}else if(gridPoint.getX().lowerThanOrEqualTo(start.getX())){
break;
}
}
}
return lstXPosition;
}
/**
* Returns the ordered list of position along the Y axis which intersects the given segment. The points are ordered from start to end
* @param start the start point of the segment
* @param end the end point of the segment
* @return a list of Quantity
*/
private List<Length> getIntersectingDivisionsY(Tuple6b start, Tuple6b end){
List<Length> lstYPosition = new ArrayList<>();
// Start < End
if(start.getY().lowerThanOrEqualTo(end.getY())){
for(int i = 0; i < yDivisionCount ; i++){
Tuple6b gridPoint = offsets.get(vertices[0][i]);
if(gridPoint.getY().greaterThan(start.getY()) && gridPoint.getY().lowerThan(end.getY())){
lstYPosition.add( gridPoint.getY() );
}else if(gridPoint.getY().greaterThanOrEqualTo(end.getY())){
break;
}
}
}else{
// End < Start
for(int i = yDivisionCount - 1; i >= 0 ; i--){
Tuple6b gridPoint = offsets.get(vertices[0][i]);
if(gridPoint.getY().greaterThan(end.getY()) && gridPoint.getY().lowerThan(start.getY())){
lstYPosition.add( gridPoint.getY() );
}else if(gridPoint.getY().lowerThanOrEqualTo(start.getY())){
break;
}
}
}
return lstYPosition;
}
/**
* Returns the index row containing the X value of the given position
* @param position the row position
* @return integer
*/
private int getCellXIndex(Length x){
int cellIndex = 0;
for(int i = 0; i <= xDivisionCount ; i++){
Tuple6b gridPoint = offsets.get(vertices[i][0]);
if(gridPoint.getX().greaterThanOrEqualTo(x) ){
break;
}
cellIndex = i;
}
return cellIndex;
}
/**
* Returns the index column containing the Y value of the given position
* @param position the column position
* @return integer
*/
private int getCellYIndex(Length y){
int cellIndex = 0;
for(int i = 0; i <= yDivisionCount; i++){
Tuple6b gridPoint = offsets.get(vertices[0][i]);
if(gridPoint.getY().greaterThanOrEqualTo(y) ){
break;
}
cellIndex = i;
}
return cellIndex;
}
/**
* Find the interpolated height of the given position using a bilinear interpolation of the height probed at the 4 corners
* @param position the calculation position
* @param v1 probed position of corner 1
* @param v2 probed position of corner 2
* @param v3 probed position of corner 3
* @param v4 probed position of corner 4
* @return the probed height
*/
private Length findOffsetBilinear(Tuple6b position,Tuple6b v1,Tuple6b v2,Tuple6b v3,Tuple6b v4){
Length x1 = position.getY().subtract(v1.getY()).abs();
Length dx1 = v2.getY().subtract(v1.getY()).abs();
Length a1z = v2.getZ().multiply(x1.divide(dx1)).add(v1.getZ().multiply( BigDecimal.ONE.subtract(x1.divide(dx1)) ));
Length x2 = position.getY().subtract(v3.getY()).abs();
Length dx2 = v4.getY().subtract(v3.getY()).abs();
Length a2z = v4.getZ().multiply(x2.divide(dx2)).add(v3.getZ().multiply( BigDecimal.ONE.subtract(x2.divide(dx2) )));
Length y1 = position.getX().subtract(v1.getX()).abs();
Length dy1 = v3.getX().subtract(v1.getX()).abs();
Length zFinal = a2z.multiply(y1.divide(dy1)).add(a1z.multiply( BigDecimal.ONE.subtract(y1.divide(dy1) )));
return zFinal;
}
/**
* @return the offsets
*/
public List<Tuple6b> getOffsets() {
return offsets;
}
public Tuple6b getPoint(int x, int y){
if(vertices != null && vertices.length > x){
if(vertices[x] != null && vertices[x].length > y){
int index = vertices[x][y];
return offsets.get(index);
}
}
return null;
}
public void build() throws GkException{
if(xDivisionCount <= 0 || yDivisionCount <= 0){
throw new GkTechnicalException("X/Y division count should be positive");
}
Length dx = end.getX().subtract(start.getX()).divide(xDivisionCount);
Length dy = end.getY().subtract(start.getY()).divide(yDivisionCount);
offsets = new ArrayList<Tuple6b>();
vertices = new int[xDivisionCount + 1][yDivisionCount + 1];
for(int x = 0 ; x <= xDivisionCount; x++){
Length xCoord = start.getX().add(dx.multiply(x));
for(int y = 0 ; y <= yDivisionCount; y++){
vertices[x][y] = offsets.size();
offsets.add(new Tuple6b(xCoord, start.getY().add(dy.multiply(y)), Length.ZERO));
}
}
}
/**
* @param offsets the offsets to set
*/
public void setOffsets(List<Tuple6b> offsets) {
this.offsets = offsets;
}
/**
* @return the start
*/
public Tuple6b getStart() {
return start;
}
/**
* @param start the start to set
*/
public void setStart(Tuple6b start) {
this.start = start;
}
/**
* @return the end
*/
public Tuple6b getEnd() {
return end;
}
/**
* @param end the end to set
*/
public void setEnd(Tuple6b end) {
this.end = end;
}
/**
* @return the xDivisionCount
*/
public int getxDivisionCount() {
return xDivisionCount;
}
/**
* @param xDivisionCount the xDivisionCount to set
*/
public void setxDivisionCount(int xDivisionCount) {
this.xDivisionCount = xDivisionCount;
}
/**
* @return the yDivisionCount
*/
public int getyDivisionCount() {
return yDivisionCount;
}
/**
* @param yDivisionCount the yDivisionCount to set
*/
public void setyDivisionCount(int yDivisionCount) {
this.yDivisionCount = yDivisionCount;
}
/**
* @return
*/
public Length getStepSizeX() {
return end.getX().subtract(start.getX()).divide(xDivisionCount);
}
/**
* @return
*/
public Length getStepSizeY() {
return end.getY().subtract(start.getY()).divide(yDivisionCount);
}
/**
* @return the clearanceHeight
*/
public Length getClearanceHeight() {
return clearanceHeight;
}
/**
* @param clearanceHeight the clearanceHeight to set
*/
public void setClearanceHeight(Length clearanceHeight) {
this.clearanceHeight = clearanceHeight;
}
/**
* @return the probeStartHeight
*/
public Length getProbeStartHeight() {
return probeStartHeight;
}
/**
* @param probeStartHeight the probeStartHeight to set
*/
public void setProbeStartHeight(Length probeStartHeight) {
this.probeStartHeight = probeStartHeight;
}
/**
* @return the probeLowerHeight
*/
public Length getProbeLowerHeight() {
return probeLowerHeight;
}
/**
* @param probeLowerHeight the probeLowerHeight to set
*/
public void setProbeLowerHeight(Length probeLowerHeight) {
this.probeLowerHeight = probeLowerHeight;
}
/**
* @return the probeFeedrate
*/
public Speed getProbeFeedrate() {
return probeFeedrate;
}
/**
* @param probeFeedrate the probeFeedrate to set
*/
public void setProbeFeedrate(Speed probeFeedrate) {
this.probeFeedrate = probeFeedrate;
}
/** (inheritDoc)
* @see org.goko.tools.autoleveler.bean.IHeightMap#isProbed()
*/
@Override
public boolean isProbed() {
return isProbed;
}
/**
* @param isProbed the isProbed to set
*/
public void setProbed(boolean isProbed) {
this.isProbed = isProbed;
}
/**
* @return the moveFeedrate
*/
public Speed getMoveFeedrate() {
return moveFeedrate;
}
/**
* @param moveFeedrate the moveFeedrate to set
*/
public void setMoveFeedrate(Speed moveFeedrate) {
this.moveFeedrate = moveFeedrate;
}
}