Skip to content
Open
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
102 changes: 101 additions & 1 deletion packages/terra-draw/src/modes/select/select.mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
MARKER_URL_DEFAULT,
FinishActions,
} from "../../common";
import { LineString, Point, Polygon, Position } from "geojson";
import { Feature, LineString, Point, Polygon, Position } from "geojson";
import {
BaseModeOptions,
CustomStyling,
Expand All @@ -37,6 +37,9 @@ import { RotateFeatureBehavior } from "./behaviors/rotate-feature.behavior";
import { ScaleFeatureBehavior } from "./behaviors/scale-feature.behavior";
import { FeatureId, GeoJSONStoreFeatures } from "../../store/store";
import { getDefaultStyling } from "../../util/styling";
import { circle, circleWebMercator } from "../../geometry/shape/create-circle";
import { centroid } from "../../geometry/centroid";
import { haversineDistanceKilometers } from "../../geometry/measure/haversine-distance";
import {
DragCoordinateResizeBehavior,
ResizeOptions,
Expand Down Expand Up @@ -173,6 +176,10 @@ export class TerraDrawSelectMode extends TerraDrawBaseSelectMode<SelectionStylin
type: "none",
};

// Cached center position for circle resize — captured once at drag start
// to avoid centroid drift during progressive updates
private circleResizeCenter: Position | null = null;

// Behaviors
private selectionPoints!: SelectionPointBehavior;
private midPoints!: MidPointBehavior;
Expand Down Expand Up @@ -845,6 +852,22 @@ export class TerraDrawSelectMode extends TerraDrawBaseSelectMode<SelectionStylin
resizableCoordinateIndex,
);

// For circle features, capture the center at drag start so it
// stays fixed throughout the resize operation
const resizeProps = this.readFeature.getProperties(selectedId);
if (
resizeProps.mode === "circle" &&
typeof resizeProps.radiusKilometers === "number"
) {
const geometry = this.readFeature.getGeometry<Polygon>(selectedId);
const feature = {
type: "Feature" as const,
geometry,
properties: {},
};
this.circleResizeCenter = centroid(feature as Feature<Polygon>);
}

setMapDraggability(false);
return;
}
Expand Down Expand Up @@ -966,6 +989,19 @@ export class TerraDrawSelectMode extends TerraDrawBaseSelectMode<SelectionStylin
modeFlags.feature.coordinates &&
modeFlags.feature.coordinates.resizable
) {
// Circle features must be resized by regenerating the circle from
// center + new radius, not by bbox scaling which distorts the shape.
// This applies to both globe and web-mercator projections.
const properties = this.readFeature.getProperties(selectedId);
if (
properties.mode === "circle" &&
typeof properties.radiusKilometers === "number"
) {
setMapDraggability(false);
this.resizeCircle(event, selectedId);
return;
}

if (this.projection === "globe") {
throw new Error(
"Globe is currently unsupported projection for resizable",
Expand Down Expand Up @@ -1039,6 +1075,7 @@ export class TerraDrawSelectMode extends TerraDrawBaseSelectMode<SelectionStylin
this.dragCoordinateResizeFeature.stopDragging();
this.rotateFeature.reset();
this.scaleFeature.reset();
this.circleResizeCenter = null;
setMapDraggability(true);
}

Expand Down Expand Up @@ -1345,6 +1382,69 @@ export class TerraDrawSelectMode extends TerraDrawBaseSelectMode<SelectionStylin
return styles;
}

/**
* Resize a circle feature by recomputing it from its center and the
* haversine distance to the cursor. Uses geodesic circle for globe
* projection and web-mercator circle for web-mercator projection.
*/
private resizeCircle(event: TerraDrawMouseEvent, featureId: FeatureId) {
const geometry = this.readFeature.getGeometry<Polygon>(featureId);

// Use the center captured at drag start to avoid drift
const center = this.circleResizeCenter;
if (!center) {
return;
}

const newRadius = haversineDistanceKilometers(center, [
event.lng,
event.lat,
]);

if (newRadius <= 0) {
return;
}

const steps = geometry.coordinates[0].length - 1; // -1 for closing coord

const updatedCircle =
this.projection === "globe"
? circle({
center,
radiusKilometers: newRadius,
coordinatePrecision: this.coordinatePrecision,
steps,
})
: circleWebMercator({
center,
radiusKilometers: newRadius,
coordinatePrecision: this.coordinatePrecision,
steps,
});

const updated = this.mutateFeature.updatePolygon({
featureId,
coordinateMutations: {
type: Mutations.Replace,
coordinates: updatedCircle.geometry.coordinates,
},
propertyMutations: {
radiusKilometers: newRadius,
},
context: {
updateType: UpdateTypes.Provisional,
},
});

if (!updated) {
return;
}

const featureCoordinates = updated.geometry.coordinates;
this.midPoints.updateAllInPlace({ featureCoordinates });
this.selectionPoints.updateAllInPlace({ featureCoordinates });
}

afterFeatureUpdated(feature: GeoJSONStoreFeatures) {
// If we have a selected feature and it has been updated
// we need to update the selection points and midpoints
Expand Down