diff --git a/Zero_engine.alpx b/Zero_engine.alpx
index 604453f..f061231 100644
--- a/Zero_engine.alpx
+++ b/Zero_engine.alpx
@@ -1354,11 +1354,6 @@ LARGE_CONNECTION (grootverbruik, > 3x80)
1753342021373
-
- 1764938574089
-
- 1752680962144
-
1772100250031
@@ -1443,6 +1438,10 @@ LARGE_CONNECTION (grootverbruik, > 3x80)
1772100318398
+
+ 1783069714235
+
+
EULER
@@ -2097,7 +2096,7 @@ LARGE_CONNECTION (grootverbruik, > 3x80)
1770210283060
- 1764938574089
+ 1783069714235
1770648718773
@@ -2251,6 +2250,7 @@ LARGE_CONNECTION (grootverbruik, > 3x80)
1781523461253
+ 1783069714235
1781622084102
@@ -2265,11 +2265,17 @@ LARGE_CONNECTION (grootverbruik, > 3x80)
1781710977201
+ 1783069714235
1782137734965
- 1764938574089
+ 1783069714235
+
+
+ 1783014017243
+
+ 1783069714235
diff --git a/_alp/Classes/Class.GISUtil.java b/_alp/Classes/Class.GISUtil.java
index 974fc82..ee37bdc 100644
--- a/_alp/Classes/Class.GISUtil.java
+++ b/_alp/Classes/Class.GISUtil.java
@@ -2,10 +2,141 @@
* GISUtil
*/
public class GISUtil {
+
public static double calculateManhattanDistance_m(double latitude1, double longitude1, double latitude2, double longitude2) {
double latDist_m = getDistanceGIS(latitude1, longitude1, latitude2, longitude1); //Use anylogic getDistanceGIS function for point 1
double longDist_m = getDistanceGIS(latitude2, longitude1, latitude2, longitude2); //Use anylogic getDistanceGIS function
double totalDist_m = latDist_m + longDist_m;
return totalDist_m;
}
+
+ public static double[] calculateSquareCoordinates(double lat, double lon, double area_m2) {
+ /**
+ * Computes a list of double[] coordinates representing a perfectly square polygon centered
+ * at (lat, lon) with a physical area of 'area_m2' in square meters, correcting for local latitude "deformation".
+ */
+
+ double side_m = Math.sqrt(area_m2);
+ double halfSide_m = side_m / 2.0;
+
+ // Earth conversion factors (approximate for localized areas)
+ double metersPerDegLat = 111320.0;
+ double metersPerDegLon = 111320.0 * Math.cos(Math.toRadians(lat));
+
+ double offsetLat = halfSide_m / metersPerDegLat;
+ double offsetLon = halfSide_m / metersPerDegLon;
+
+ return new double[]{
+ lat + offsetLat, lon - offsetLon, // Top-left
+ lat + offsetLat, lon + offsetLon, // Top-right
+ lat - offsetLat, lon + offsetLon, // Bottom-right
+ lat - offsetLat, lon - offsetLon // Bottom-left
+ };
+ }
+
+ public static double[] calculateCircleCoordinates(double lat, double lon, double area_m2) {
+ /**
+ * Computes a list of double[] coordinates representing a perfectly circular polygon centered
+ * at (lat, lon) with a physical area of 'area_m2' in square meters, correcting for local latitude "deformation".
+ */
+
+ double radius_m = Math.sqrt(area_m2 / Math.PI); // A circle's radius in meters from its area in m²: Area = pi * r^2 => r = sqrt(Area / pi)
+
+ int numPoints = 48; // Number of points (vertices) to approximate the circle
+ double[] polyCoords = new double[numPoints * 2];
+
+ // Earth conversion factors (approximate for localized areas)
+ double metersPerDegLat = 111320.0;
+ double metersPerDegLon = 111320.0 * Math.cos(Math.toRadians(lat));
+
+ for (int i = 0; i < numPoints; i++) {
+ double angle = 2.0 * Math.PI * i / numPoints;
+ double offsetLat = (radius_m * Math.sin(angle)) / metersPerDegLat;
+ double offsetLon = (radius_m * Math.cos(angle)) / metersPerDegLon;
+
+ polyCoords[2 * i] = lat + offsetLat;
+ polyCoords[2 * i + 1] = lon + offsetLon;
+ }
+
+ return polyCoords;
+ }
+
+ public static double[] calculateCustomPolygonCoordinates(List coordinateList) {
+ /**
+ * Computes a list of double[] coordinates from a list of coordinate Points.
+ */
+
+ int size = coordinateList.size();
+ double[] polyCoords = new double[size * 2];
+ for (int i = 0; i < size; i++) {
+ Point p = coordinateList.get(i);
+ polyCoords[2 * i] = p.getX();
+ polyCoords[2 * i + 1] = p.getY();
+ }
+ return polyCoords;
+ }
+
+ public static boolean arePolygonEdgesSelfIntersecting(List vertices) {
+ /**
+ * Checks if a polygon represented by a list of Point vertices is self-intersecting.
+ * Returns true if any two non-adjacent edges intersect.
+ */
+
+ int n = vertices.size();
+ if (n < 4) {
+ return false; // A polygon with less than 4 vertices cannot self-intersect
+ }
+ for (int i = 0; i < n; i++) {
+ Point p1 = vertices.get(i);
+ Point q1 = vertices.get((i + 1) % n);
+ for (int j = i + 2; j < n; j++) {
+ // Skip adjacent edges (they naturally share a vertex)
+ if (i == 0 && j == n - 1) {
+ continue;
+ }
+ Point p2 = vertices.get(j);
+ Point q2 = vertices.get((j + 1) % n);
+ if (areSegmentsIntersecting(p1.getX(), p1.getY(), q1.getX(), q1.getY(), p2.getX(), p2.getY(), q2.getX(), q2.getY())) {
+ return true; // Intersection detected
+ }
+ }
+ }
+ return false;
+
+ }
+
+ public static boolean areSegmentsIntersecting(double p1x, double p1y, double q1x, double q1y,
+ double p2x, double p2y, double q2x, double q2y) {
+ /**
+ * Checks if line segment p1q1 and p2q2 intersect.
+ */
+
+ int o1 = getOrientation(p1x, p1y, q1x, q1y, p2x, p2y);
+ int o2 = getOrientation(p1x, p1y, q1x, q1y, q2x, q2y);
+ int o3 = getOrientation(p2x, p2y, q2x, q2y, p1x, p1y);
+ int o4 = getOrientation(p2x, p2y, q2x, q2y, q1x, q1y);
+ // General Case: Segments cross each other
+ if (o1 != o2 && o3 != o4) return true;
+ // Special Cases (Collinear segments overlapping)
+ if (o1 == 0 && isPointOnSegment(p1x, p1y, p2x, p2y, q1x, q1y)) return true;
+ if (o2 == 0 && isPointOnSegment(p1x, p1y, q2x, q2y, q1x, q1y)) return true;
+ if (o3 == 0 && isPointOnSegment(p2x, p2y, p1x, p1y, q2x, q2y)) return true;
+ if (o4 == 0 && isPointOnSegment(p2x, p2y, q1x, q1y, q2x, q2y)) return true;
+ return false;
+ }
+
+ // Helper: Checks if point q lies on line segment pr
+ private static boolean isPointOnSegment(double px, double py, double qx, double qy, double rx, double ry) {
+ return qx <= max(px, rx) && qx >= min(px, rx) &&
+ qy <= max(py, ry) && qy >= min(py, ry);
+ }
+
+ // Helper: Finds the orientation of ordered triplet (p, q, r).
+ // Returns: 0 -> Collinear, 1 -> Clockwise, 2 -> Counterclockwise
+ private static int getOrientation(double px, double py, double qx, double qy, double rx, double ry) {
+ double val = (qy - py) * (rx - qx) - (qx - px) * (ry - qy);
+ if (DoubleCompare.equalsZero(val)) return 0;
+ return (val > 0) ? 1 : 2;
+ }
+
}
\ No newline at end of file
diff --git a/_alp/Classes/Class.UIUtil.java b/_alp/Classes/Class.UIUtil.java
new file mode 100644
index 0000000..3562814
--- /dev/null
+++ b/_alp/Classes/Class.UIUtil.java
@@ -0,0 +1,113 @@
+/**
+ * UIUtil
+ *
+ * Notes/assumptions:
+ * - Shapes are assumed to be unrotated and in the same coordinate space
+ * (i.e. same parent group / level).
+ * - The text shape's Y is treated as the top of the text block, matching
+ * AnyLogic's text shape anchor.
+ */
+
+import java.awt.font.FontRenderContext;
+import java.awt.geom.Rectangle2D;
+
+public class UIUtil {
+
+ /**
+ * Standalone render context for text measurement. Arguments:
+ * 1. transform (null) - no scaling/rotation of the target surface; measure in plain user-space pixels (identity)
+ * 2. isAntiAliased (true) - assume glyph edges are smoothed, matching how AnyLogic renders presentation text
+ * 3. usesFractionalMetrics (true) - keep exact fractional glyph widths instead of rounding to whole pixels, so measured size scales linearly with font size
+ */
+ private static final FontRenderContext FRC = new FontRenderContext(null, true, true);
+
+ public static void fitTextInRectangleVertical(ShapeText textShape, ShapeRectangle rectShape, double topMargin_px, double bottomMargin_px) {
+ fitTextInRectangle(textShape, rectShape, topMargin_px, bottomMargin_px, 0.0, 0.0);
+ }
+
+ public static void fitTextInRectangleHorizontal(ShapeText textShape, ShapeRectangle rectShape, double leftMargin_px, double rightMargin_px) {
+ fitTextInRectangle(textShape, rectShape, 0.0, 0.0, leftMargin_px, rightMargin_px);
+ }
+
+ public static void fitTextInRectangle(ShapeText textShape, ShapeRectangle rectShape, double margin_px) {
+ fitTextInRectangle(textShape, rectShape, margin_px, margin_px, margin_px, margin_px);
+ }
+
+ public static void fitTextInRectangle(ShapeText textShape, ShapeRectangle rectShape, double topMargin_px, double bottomMargin_px, double leftMargin_px, double rightMargin_px) {
+ /**
+ * Scales the font of textShape (up or down) so the text block fits exactly
+ * within rectShape minus the given margins, and centers the block inside
+ * that margin box. The binding axis (width or height) touches its margins
+ * exactly; the other axis gets the remaining space distributed equally.
+ */
+ if (textShape == null || rectShape == null || textShape.getText() == null || textShape.getText().isEmpty() || textShape.getFont() == null) {
+ return;
+ }
+
+ double availableWidth = rectShape.getWidth() - leftMargin_px - rightMargin_px;
+ double availableHeight = rectShape.getHeight() - topMargin_px - bottomMargin_px;
+ if (availableWidth <= 0 || availableHeight <= 0) {
+ return; // margins consume the whole rectangle
+ }
+
+ Font font = textShape.getFont();
+ String[] lines = textShape.getText().split("\n", -1); // Limit -1 keeps trailing empty lines, so they still count for height
+
+ double maxLineWidth = maxLineWidth(font, lines);
+ double textHeight = lines.length * lineHeight(font);
+ if (maxLineWidth <= 0 || textHeight <= 0) {
+ return; // text contains no measurable glyphs
+ }
+
+ // One scale factor for both axes; the smaller ratio is the binding axis
+ double scale = Math.min(availableWidth / maxLineWidth, availableHeight / textHeight);
+ float newSize = (float) (font.getSize2D() * scale);
+ Font fittedFont = font.deriveFont(newSize);
+
+ // Font metrics are not perfectly linear in size (hinting, rounding): step down until the fit is guaranteed
+ while (maxLineWidth(fittedFont, lines) > availableWidth || lines.length * lineHeight(fittedFont) > availableHeight) {
+ newSize = newSize - 0.5f;
+ fittedFont = fittedFont.deriveFont(newSize);
+ }
+ textShape.setFont(fittedFont);
+
+ // Center the fitted block inside the margin box
+ double fittedWidth = maxLineWidth(fittedFont, lines);
+ double fittedHeight = lines.length * lineHeight(fittedFont);
+ double blockLeft = rectShape.getX() + leftMargin_px + 0.5 * (availableWidth - fittedWidth);
+ double blockTop = rectShape.getY() + topMargin_px + 0.5 * (availableHeight - fittedHeight);
+
+ textShape.setY(blockTop);
+ textShape.setX(blockLeft + alignmentAnchorOffset(textShape, fittedWidth));
+ }
+
+ public static double maxLineWidth(Font font, String[] lines) {
+ /** Width of the widest line of text. */
+ double max = 0.0;
+ for (String line : lines) {
+ if (line.isEmpty()) continue;
+ Rectangle2D bounds = font.getStringBounds(line, FRC);
+ if (bounds.getWidth() > max) {
+ max = bounds.getWidth();
+ }
+ }
+ return max;
+ }
+
+ public static double lineHeight(Font font) {
+ /** Height of a single line (ascent + descent + leading) for this font. */
+ return font.getLineMetrics("Ag", FRC).getHeight();
+ }
+
+ public static double alignmentAnchorOffset(ShapeText textShape, double textWidth) {
+ /**
+ * AnyLogic anchors a text shape's X at its alignment point: left edge for
+ * left-aligned, center for center-aligned, right edge for right-aligned.
+ */
+ TextAlignment alignment = textShape.getAlignment();
+ if (alignment == TextAlignment.ALIGNMENT_CENTER) return 0.5 * textWidth;
+ if (alignment == TextAlignment.ALIGNMENT_RIGHT) return textWidth;
+ return 0.0; // ALIGNMENT_LEFT or null
+ }
+
+}
\ No newline at end of file