package cn.com.taiji.tile.util; import cn.com.taiji.tile.model.POI; import cn.com.taiji.tile.style.ShapeStyle; import cn.hutool.core.util.BooleanUtil; import org.apache.commons.lang3.StringUtils; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Geometry; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; /** * @author zzyx 2024/1/5 */ public class ShapeUtils { public static final int WIDTH = 256; public static final int HEIGHT = 256; /** * 获取瓦片字节数组 * * @param bboxStr 逗号拼接的bbox字符串 * @param dataList 数据集合 * @param shapeStyle 样式 * @return 瓦片字节数组 */ public static byte[] getTileByte(String bboxStr, List dataList, ShapeStyle shapeStyle) { BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); renderAndDetectClick(image, bboxStr, dataList, shapeStyle); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try { ImageIO.write(image, "png", buffer); } catch (IOException e) { e.printStackTrace(); } return buffer.toByteArray(); } private static void drawPolygonShape(double[] bbox, Graphics2D graphics, Geometry geometry, POI poi, ShapeStyle shapeStyle) { drawPolygonToTile(shapeStyle, geometry, graphics, bbox); //是否需要设置字名称 if (BooleanUtil.isTrue(shapeStyle.getIsShowName()) && StringUtils.isNotBlank(poi.getName())) { org.locationtech.jts.geom.Point centroid = geometry.getCentroid(); int[] pxy = TileUtils.toPixelXY(centroid.getX(), centroid.getY(), bbox[0], bbox[1], bbox[2], bbox[3]); //设置字体样式 Font font = new Font("Arial", Font.TRUETYPE_FONT, shapeStyle.getFontSize()); String s = poi.getName(); graphics.setColor(shapeStyle.getFontColor()); graphics.setFont(font); graphics.drawString(s, pxy[0], pxy[1]); //设置面的图标 BufferedImage cpi = shapeStyle.getCenterPointImage(); if (cpi != null) { graphics.drawImage(TileUtils.scaleWidthHeight(cpi, 13, 13), pxy[0], pxy[1], null); } } } public static void drawLineShape(double[] bbox, Graphics2D graphics, Geometry geometry, ShapeStyle shapeStyle) { int[] xpoint = new int[(geometry.getCoordinates()).length]; int[] ypoint = new int[(geometry.getCoordinates()).length]; convertCoordinatesToPixels(geometry.getCoordinates(), xpoint, ypoint, bbox); graphics.setColor(shapeStyle.getColor()); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (Objects.equals(shapeStyle.getLineType(), 2)) { graphics.setStroke(new BasicStroke(shapeStyle.getWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, shapeStyle.getDashPattern(), 0.0f)); } else if (Objects.equals(shapeStyle.getLineType(), 3)) { graphics.setStroke(new BasicStroke(shapeStyle.getWidth(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, shapeStyle.getDashPattern(), 0.0f)); } else { graphics.setStroke(new BasicStroke(shapeStyle.getWidth())); } //端点的样式可以是圆形 (CAP_ROUND)、平直 (CAP_BUTT) 或方形 (CAP_SQUARE) // 连接样式:线段之间的连接可以设置为斜接 (JOIN_MITER)、圆角 (JOIN_ROUND) 或平直 (JOIN_BEVEL)。 // graphics.setStroke(new BasicStroke(10f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL)); graphics.drawPolyline(xpoint, ypoint, geometry.getCoordinates().length); } public static void drawPointShape(double[] bbox, Graphics2D graphics, Geometry geometry, POI poi, ShapeStyle shapeStyle) { int[] pxy = TileUtils.toPixelXY(geometry.getCoordinate().x, geometry.getCoordinate().y, bbox[0], bbox[1], bbox[2], bbox[3]); //设置边框 实现轮廓 graphics.setColor(shapeStyle.getColor()); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); //设置字体 //设置字体样式 if (BooleanUtil.isTrue(shapeStyle.getIsShowName())) { Font font = new Font("Arial", Font.BOLD, shapeStyle.getFontSize()); String s = poi.getName(); graphics.setColor(shapeStyle.getFontColor()); graphics.setFont(font); graphics.drawString(s, pxy[0], pxy[1]); } BufferedImage pointMarkerImage = shapeStyle.getPointMarkerImage(); if (pointMarkerImage != null) { graphics.drawImage(TileUtils.scaleWidthHeight(pointMarkerImage, shapeStyle.getWidth(), shapeStyle.getHeight()), pxy[0], pxy[1], null); } else { drawPointShape(shapeStyle, graphics, pxy); } } public static void convertCoordinatesToPixels(Coordinate[] coordinates, int[] xPoints, int[] yPoints, double[] bbox) { // 坐标转换逻辑 for (int i = 0; i < coordinates.length; i++) { Coordinate c = coordinates[i]; //坐标转换:对于每个坐标 c,代码调用 TileUtils.toPixelXY 方法,将地理坐标(经纬度)转换为像素坐标。这个转换考虑了一个称为 bbox(边界框)的地理区域,这个区域定义了地理坐标到像素坐标转换的上下文。bbox 通常包含边界框的最小经度、最小纬度、最大经度和最大纬度。 int[] pxy = TileUtils.toPixelXY(c.getX(), c.getY(), bbox[0], bbox[1], bbox[2], bbox[3]); xPoints[i] = pxy[0]; yPoints[i] = pxy[1]; } } /** * 面填充样式 * 在地理信息系统(GIS)和空间数据处理领域中,Geometry 类是一个常用的概念,用于表示空间对象的形状和位置。这个类通常包含各种方法来操作和查询几何形状。getCoordinates() 方法是 Geometry 类中的一个方法,它的作用是获取构成该几何形状的所有坐标点。 * 当你调用 Geometry 对象的 getCoordinates() 方法时,它会返回一个坐标数组,这个数组包含了组成该几何形状的所有顶点坐标。这些坐标通常是 Coordinate 对象,每个 Coordinate 包含 x(经度)、y(纬度)和可选的 z(高度)值。 * 例如,如果你的 Geometry 对象是一个多边形,getCoordinates() 方法将返回构成该多边形边界的所有顶点坐标。如果是一条线,它将返回线的所有顶点坐标。 * 这个方法在进行空间分析和几何处理时非常有用,比如当你需要遍历几何形状的所有顶点、计算形状的边界、或者在不同几何形状之间进行坐标转换时。 *

* Coordinate 是在地理信息系统(GIS)和计算机图形学中常用的一个基本概念,用于表示空间中的点。在多种编程环境和库中,Coordinate 类或结构体通常用于存储和操作这些点的位置信息。其主要作用和特点包括: * 存储位置信息:Coordinate 通常包含至少两个维度的数据,即 x(经度)和 y(纬度)。在三维空间中,它还可以包含 z(高程或深度)值。 * 空间分析和计算:在 GIS 和计算机图形学应用中,Coordinate 用于执行各种空间分析和计算,如测量距离、定义几何形状(如线和多边形)的边界等。 * 数据转换和处理:Coordinate 在进行坐标系转换、地图投影、空间查询等操作中也非常重要。 * 可视化和绘图:在地图制作和空间数据可视化中,Coordinate 用于确定应在地图上绘制的点的确切位置。 * 在不同的编程库和框架中,Coordinate 类可能还包含了其他辅助方法,如计算两个坐标之间的距离、比较坐标点是否相等等。例如,在 Java 的 JTS Topology Suite(一个常用的空间数据处理库)中,Coordinate 类就提供了这样的功能。 */ public static void drawPolygonToTile(ShapeStyle shapeStyle, Geometry shape, Graphics2D graphics, double[] bbox) { //普通面 if (shape != null && shape.getCoordinates().length > 0) { int[] xpoint = new int[(shape.getCoordinates()).length]; int[] ypoint = new int[(shape.getCoordinates()).length]; convertCoordinatesToPixels(shape.getCoordinates(), xpoint, ypoint, bbox); //设置样式 //2.设置抗锯齿 graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); //是否填充 if (BooleanUtil.isTrue(shapeStyle.getIsFill())) { //fill 是将设置的颜色全部填充 graphics.setColor(shapeStyle.getColor()); //设置背景面 setBackgroundImage(graphics, shapeStyle); Polygon polygon = new Polygon(xpoint, ypoint, shape.getNumPoints()); //填充 graphics.fill(polygon); //设置边框颜色 setBorderStyle(graphics, shapeStyle); //drawPolygon 是透明的只会出来边框 graphics.drawPolygon(polygon); } else { setBorderStyle(graphics, shapeStyle); graphics.drawPolygon(xpoint, ypoint, (shape.getCoordinates()).length); } } } private static void setBackgroundImage(Graphics2D graphics, ShapeStyle shapeStyle) { BufferedImage backgroundImage = shapeStyle.getBgImage(); if (backgroundImage != null) { TexturePaint texturePaint = new TexturePaint(backgroundImage, new Rectangle(0, 0, backgroundImage.getWidth(), backgroundImage.getHeight())); graphics.setPaint(texturePaint); } } private static void setBorderStyle(Graphics2D graphics, ShapeStyle shapeStyle) { //不填充只显示边框颜色 graphics.setColor(shapeStyle.getBorderColor()); // 设置虚线:5像素实线,5像素空白 if (BooleanUtil.isTrue(shapeStyle.getIsDashed())) { Stroke dashed = new BasicStroke(shapeStyle.getBorderSize(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, shapeStyle.getDashPattern(), 0); graphics.setStroke(dashed); } else { graphics.setStroke(new BasicStroke(shapeStyle.getBorderSize())); } } /** * Converts a hexadecimal color string to RGBA format. * * @param hexColor The hexadecimal color string (e.g., "#2860F1"). * @param alpha The alpha value (0-255) for opacity. * @return An array containing the RGBA values. */ public static int[] hexToRgba(String hexColor, int alpha) { // Remove the hash (#) sign if it exists hexColor = hexColor.replace("#", ""); // Parse the string int red = Integer.valueOf(hexColor.substring(0, 2), 16); int green = Integer.valueOf(hexColor.substring(2, 4), 16); int blue = Integer.valueOf(hexColor.substring(4, 6), 16); // Return the RGBA values return new int[]{red, green, blue, alpha}; } private static void drawPointShape(ShapeStyle shapeStyle, Graphics2D graphics, int[] pxy) { if (BooleanUtil.isTrue(shapeStyle.getIsDashed())) { //如果是 Stroke dashed = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, shapeStyle.getPointDashPattern(), 0); graphics.setStroke(dashed); } if (Objects.equals(shapeStyle.getPointType(), 1)) { //正方形 graphics.drawRect(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight()); if (BooleanUtil.isTrue(shapeStyle.getIsFill())) { graphics.fillRect(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight()); } } else if (Objects.equals(shapeStyle.getPointType(), 2)) { //三角形 int[] xPoints = {pxy[0], pxy[0] - shapeStyle.getHeight(), pxy[0] + shapeStyle.getHeight()}; int[] yPoints = {pxy[1] - shapeStyle.getHeight(), pxy[1] + shapeStyle.getHeight(), pxy[1] + shapeStyle.getHeight()}; int nPoints = 3; // 顶点数量 graphics.drawPolygon(xPoints, yPoints, nPoints); if (BooleanUtil.isTrue(shapeStyle.getIsFill())) { graphics.fillPolygon(xPoints, yPoints, nPoints); } } else if (Objects.equals(shapeStyle.getPointType(), 3)) { //圆形 graphics.drawOval(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight()); if (BooleanUtil.isTrue(shapeStyle.getIsFill())) { graphics.fillOval(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight()); } } else { //画一个简单的点 graphics.drawArc(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight(), 0, 360); //是否填充 if (BooleanUtil.isTrue(shapeStyle.getIsFill())) { graphics.fillArc(pxy[0], pxy[1], shapeStyle.getWidth(), shapeStyle.getHeight(), 0, 360); } } } public static List detectClick(String bboxStr, List dataList, ShapeStyle shapeStyle, int x, int y) { List clickSelectedDataList = new CopyOnWriteArrayList<>(); // renderAndDetectClick(image, bboxStr, dataList, shapeStyle, x, y, clickSelectedDataList, true); String[] split = bboxStr.split(","); double[] bbox = new double[split.length]; for (int i = 0; i < split.length; i++) { bbox[i] = Double.parseDouble(split[i]); } dataList.parallelStream().forEach(poi -> { BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = image.createGraphics(); Geometry shape = GeoUtils.getGeoUtils().createGeometry(poi.getShape()); // //判断数据点线面 if (StringUtils.equals(shape.getGeometryType(), "Polygon")) { drawPolygonShape(bbox, graphics, shape, poi, shapeStyle); } else if (StringUtils.equalsAny(shape.getGeometryType(), "LinearRing", "LineString", "MultiLineString")) { drawLineShape(bbox, graphics, shape, shapeStyle); } else if (StringUtils.equals(shape.getGeometryType(), "Point")) { drawPointShape(bbox, graphics, shape, poi, shapeStyle); } //如果选中 int rgb = image.getRGB(x, y); int alpha = (rgb >> 24) & 0xff; // 提取alpha值 if (alpha > 0) { // 像素被填充 clickSelectedDataList.add(poi); } }); return clickSelectedDataList; } public static void renderAndDetectClick(BufferedImage image, String bboxStr, List dataList, ShapeStyle shapeStyle) { String[] split = bboxStr.split(","); double[] bbox = new double[split.length]; for (int i = 0; i < split.length; i++) { bbox[i] = Double.parseDouble(split[i]); } Graphics2D graphics = image.createGraphics(); dataList.parallelStream().forEach(poi -> { Geometry shape = GeoUtils.getGeoUtils().createGeometry(poi.getShape()); // //判断数据点线面 if (StringUtils.equals(shape.getGeometryType(), "Polygon")) { drawPolygonShape(bbox, graphics, shape, poi, shapeStyle); } else if (StringUtils.equalsAny(shape.getGeometryType(), "LinearRing", "LineString", "MultiLineString")) { drawLineShape(bbox, graphics, shape, shapeStyle); } else if (StringUtils.equals(shape.getGeometryType(), "Point")) { drawPointShape(bbox, graphics, shape, poi, shapeStyle); } }); } }