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