Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ public void mouseReleased(MouseEvent e) {
startIndicatorLoc = null;
}

/**
* Canvas drag translate. Whole-geom path is
* {@code copy(); apply(translation)} — the same apply as
* {@link AffineTransformation#transform(Geometry)}. Curve honesty
* (ISO/IEC 13249-3 CIRCULARSTRING three-point arcs, including
* CompoundCurve member starts) lives in that apply. Translate only;
* not a general-affine SIGN.
*/
private void execute(Coordinate fromLoc, Coordinate toLoc, boolean isComponentMoved) {
double dx = toLoc.getX() - fromLoc.getX();
double dy = toLoc.getY() - fromLoc.getY();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright (c) 2026 grootstebozewolf
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jtstest.function;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.curve.CircularString;
import org.locationtech.jts.geom.curve.CompoundCurve;
import org.locationtech.jts.geom.curve.MultiCurve;
import org.locationtech.jts.geom.util.AffineTransformation;
import org.locationtech.jts.io.curve.CurveWKTWriter;
import org.locationtech.jtstest.testbuilder.geom.GeometryComponentTransformer;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

/**
* Canvas miss is MoveTool drag, not Function-tree AffineTranslation.
* <p>
* {@code logoLines} then MoveTool.execute ({@code translationInstance}
* + {@link GeometryComponentTransformer#transform(Geometry, AffineTransformation)},
* which is {@code copy(); apply(trans)}) must keep the ISO/IEC 13249-3
* MultiCurve and every three-point {@code CIRCULARSTRING}, including
* each circular member's start.
* <p>
* Verified same apply: Function-tree
* {@link AffineTransformationFunctions#translate} is
* {@code translationInstance(dx, dy).transform(g)}, which is also
* {@code copy(); apply(trans)}. That is stated, not a UX SIGN of the
* Function-tree. Testers still shoot AffineTranslation (10, 8) on pin
* JAR {@code 61eb3377}. Do not retip that pin. Do not rebuild the
* guides JAR. Translate only. No Bézier I/O type.
* {@code logoBuffer} stays named CHORD-PATH / toLinear + BufferOp.
*/
public class JTSFunctionsLogoLinesTranslateTest extends TestCase {

private static final double DX = 10.0;
private static final double DY = 8.0;
private static final double EPS = 1.0e-12;

public static void main(String[] args) { TestRunner.run(suite()); }
public static Test suite() { return new TestSuite(JTSFunctionsLogoLinesTranslateTest.class); }
public JTSFunctionsLogoLinesTranslateTest(String name) { super(name); }

public void testMoveToolDragKeepsTypesAndArcStarts() {
Geometry logo = JTSFunctions.logoLines(null);
Geometry moved = moveToolTranslate(logo, DX, DY);

assertTrue(moved instanceof MultiCurve);
assertEquals(logo.getNumGeometries(), moved.getNumGeometries());
assertTranslatedTree(logo, moved);

String wkt = new CurveWKTWriter().write(moved);
assertTrue(wkt.contains("MULTICURVE"));
assertTrue(wkt.contains("COMPOUNDCURVE"));
assertTrue(wkt.contains("CIRCULARSTRING"));
assertFalse(wkt.toUpperCase().contains("BEZIER"));
}

/**
* Same apply, not a Function-tree SIGN. MoveTool.execute and
* AffineTransformation.translate are both {@code copy(); apply(trans)}.
*/
public void testFunctionTreeTranslateIsTheSameApplyAsMoveTool() {
Geometry logo = JTSFunctions.logoLines(null);
Geometry viaMoveTool = moveToolTranslate(logo, DX, DY);
Geometry viaFunctionTree = AffineTransformationFunctions.translate(logo, DX, DY);
assertTrue("Function-tree AffineTranslation is the same copy(); apply(trans) as MoveTool",
viaMoveTool.equalsExact(viaFunctionTree));
}

public void testLogoBufferPathUnchangedAfterTranslate() {
Geometry moved = moveToolTranslate(JTSFunctions.logoLines(null), DX, DY);
Geometry halo = JTSFunctions.logoBuffer(moved, 4.0);
assertTrue("logoBuffer stays polygonal CHORD-PATH",
halo instanceof Polygon || halo.getGeometryType().contains("Polygon"));
assertFalse("named fallback, never isApproximate()=false",
halo.getGeometryType().equals("CircularString"));
}

/** MoveTool.execute whole-geom path. */
private static Geometry moveToolTranslate(Geometry geom, double dx, double dy) {
return GeometryComponentTransformer.transform(
geom, AffineTransformation.translationInstance(dx, dy));
}

private static void assertTranslatedTree(Geometry original, Geometry moved) {
assertEquals(original.getGeometryType(), moved.getGeometryType());
if (original instanceof CircularString) {
assertEquals(3, moved.getNumPoints());
assertTranslated(original.getCoordinates(), moved.getCoordinates());
return;
}
if (original instanceof CompoundCurve) {
CompoundCurve a = (CompoundCurve) original;
CompoundCurve b = (CompoundCurve) moved;
assertEquals(a.getNumMembers(), b.getNumMembers());
for (int i = 0; i < a.getNumMembers(); i++) {
assertTranslatedTree(a.getMemberN(i), b.getMemberN(i));
}
return;
}
if (original instanceof GeometryCollection) {
assertEquals(original.getNumGeometries(), moved.getNumGeometries());
for (int i = 0; i < original.getNumGeometries(); i++) {
assertTranslatedTree(original.getGeometryN(i), moved.getGeometryN(i));
}
return;
}
if (original instanceof LineString) {
assertTranslated(original.getCoordinates(), moved.getCoordinates());
}
}

private static void assertTranslated(Coordinate[] from, Coordinate[] to) {
assertEquals(from.length, to.length);
for (int i = 0; i < from.length; i++) {
assertEquals(from[i].x + DX, to[i].x, EPS);
assertEquals(from[i].y + DY, to[i].y, EPS);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import java.util.List;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateFilter;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateSequenceFilter;
import org.locationtech.jts.geom.CoordinateSequences;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.IntersectionMatrix;
Expand Down Expand Up @@ -112,6 +115,79 @@ protected CompoundCurve copyInternal() {
return new CompoundCurve(copies, getFactory());
}

/**
* Applies the filter to every member, including each circular
* member's start.
* <p>
* The inherited {@link LineString#apply(CoordinateFilter)} walks only
* the concatenated sequence. {@link #concatMembers} drops each later
* member's start (the junction already stored as the previous member's
* end). Those starts are distinct {@link Coordinate} objects, so a
* translate moved mid and end of a {@code CIRCULARSTRING} (ISO/IEC
* 13249-3) and left the start behind — the arc warped. Member-first
* apply keeps the three-point arc and the type.
* <p>
* Affine <em>translate</em> is the signed type-honest case. This does
* not claim that shear or non-uniform scale still describes a circular
* arc. No Bézier I/O type is introduced.
*/
@Override
public void apply(CoordinateFilter filter) {
if (members.length == 0) {
super.apply(filter);
return;
}
for (int i = 0; i < members.length; i++) {
members[i].apply(filter);
}
syncConcatenatedSequence();
}

/**
* Same member-first walk as {@link #apply(CoordinateFilter)}. The
* concatenated sequence is then copied from the members so
* {@code getCoordinates()} stays a continuous polyline. Do not apply
* the filter to the parent sequence as well — those points are often
* the same objects as the members', and a second pass would translate
* twice.
*/
@Override
public void apply(CoordinateSequenceFilter filter) {
if (members.length == 0) {
super.apply(filter);
return;
}
for (int i = 0; i < members.length; i++) {
members[i].apply(filter);
if (filter.isDone()) {
break;
}
}
syncConcatenatedSequence();
if (filter.isGeometryChanged()) {
geometryChanged();
}
}

/**
* Copies member coordinates into the concatenated parent sequence,
* skipping each later member's start the same way
* {@link #concatMembers} does. In-place so a {@code CurvePolygon}
* flat ring that wraps this sequence stays aligned.
*/
private void syncConcatenatedSequence() {
CoordinateSequence dest = points;
int k = 0;
for (int i = 0; i < members.length; i++) {
CoordinateSequence src = members[i].getCoordinateSequence();
int from = (i == 0) ? 0 : 1;
for (int j = from; j < src.size() && k < dest.size(); j++) {
CoordinateSequences.copyCoord(src, j, dest, k);
k++;
}
}
}

/**
* §3.7 — type identity is required. Without this override a CompoundCurve
* would compare equal to a plain LineString that happens to have the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
package org.locationtech.jts.geom.curve;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateFilter;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateSequenceFilter;
import org.locationtech.jts.geom.CoordinateSequences;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.IntersectionMatrix;
Expand Down Expand Up @@ -208,6 +211,114 @@ private static boolean isCurve(LineString ring) {
return ring instanceof CircularString || ring instanceof CompoundCurve;
}

/**
* Applies the filter to structural rings when they are not the same
* object as the flat {@link LinearRing} views.
* <p>
* Inherited {@link Polygon#apply(CoordinateFilter)} visits only the
* flat rings. A {@code CIRCULARSTRING} shell often shares Coordinate
* objects with that view, so a translate already reached it. A
* {@code COMPOUNDCURVE} shell (ISO/IEC 13249-3) does not: the flat
* ring is the concatenated sequence, which omits each later member's
* start. Applying here lets {@link CompoundCurve#apply} move every
* control point, including those starts. Do not also apply to the
* flat rings — shared coordinates would translate twice.
* <p>
* Affine translate is the signed type-honest case. Shear /
* non-uniform scale is not signed as keeping a circular arc.
*/
@Override
public void apply(CoordinateFilter filter) {
if (!applyToStructuralRings()) {
super.apply(filter);
return;
}
applyStructural(filter);
syncFlatRingsFromStructural();
}

/**
* Same structural-first walk as {@link #apply(CoordinateFilter)}.
*/
@Override
public void apply(CoordinateSequenceFilter filter) {
if (!applyToStructuralRings()) {
super.apply(filter);
return;
}
if (structuralShell != null) {
structuralShell.apply(filter);
}
if (!filter.isDone()) {
for (int i = 0; i < structuralHoles.length; i++) {
if (structuralHoles[i] != null) {
structuralHoles[i].apply(filter);
if (filter.isDone()) {
break;
}
}
}
}
syncFlatRingsFromStructural();
if (filter.isGeometryChanged()) {
geometryChanged();
}
}

private void applyStructural(CoordinateFilter filter) {
if (structuralShell != null) {
structuralShell.apply(filter);
}
for (int i = 0; i < structuralHoles.length; i++) {
if (structuralHoles[i] != null) {
structuralHoles[i].apply(filter);
}
}
}

/**
* True when a structural ring is a distinct object from the legacy
* flat view ({@code CircularString} / {@code CompoundCurve}).
*/
private boolean applyToStructuralRings() {
if (structuralShell != null && structuralShell != getExteriorRing()) {
return true;
}
for (int i = 0; i < structuralHoles.length; i++) {
if (structuralHoles[i] != null
&& structuralHoles[i] != getInteriorRingN(i)) {
return true;
}
}
return false;
}

/**
* Copies structural control points onto the flat rings when those
* sequences are not the same objects. Default-factory CircularString
* shells already share Coordinates, so this is a no-op there.
*/
private void syncFlatRingsFromStructural() {
if (structuralShell != null && structuralShell != getExteriorRing()) {
syncSequence(structuralShell.getCoordinateSequence(),
getExteriorRing().getCoordinateSequence());
}
for (int i = 0; i < structuralHoles.length; i++) {
if (structuralHoles[i] != null
&& structuralHoles[i] != getInteriorRingN(i)) {
syncSequence(structuralHoles[i].getCoordinateSequence(),
getInteriorRingN(i).getCoordinateSequence());
}
}
}

private static void syncSequence(CoordinateSequence src, CoordinateSequence dest) {
int n = Math.min(src.size(), dest.size());
for (int i = 0; i < n; i++) {
CoordinateSequences.copyCoord(src, i, dest, i);
}
}

/**
* Invalidates the structural rings' cached envelopes along with this polygon's.
* <p>
Expand Down
Loading
Loading