前言
在很多的面向WebGIS的应用开发过程中,我们通常会将空间地理数据,比如Shapefile等矢量数据或者Tiff等栅格数据进行发布到GIS服务器中。为了能直观的展示这些空间矢量数据或者栅格数据的空间范围及大致的数据边界,我们需要将服务化之后的数据进行缩略图的预览展示。如果大家熟悉GeoServer或者ArcgisServer的话,对这个需求一定不陌生。如果我们是自己发布的GIS服务,怎么提供这种类似的空间数据的预览服务呢?在不引入其它的外部服务的情况下。这是个值得思考的问题。
本文即是在上述的需求场景下出现的。本文主要使用Java语言,讲解如何使用GeoTools这个组件来进行空间Shapefile数据转换成图片,从而实现服务缩略图的功能。文章通过实例的模式讲解预览图片的生成,对于在研究Java的服务预览图片生成的同学和朋友有一定的参考价值。
一、关于GeoTools的图片生成
关于GeoTools这个使用Java开发的地理开发组件,它提供了许多丰富的功能和生态来帮助我们实现图片的生成操作。关于GeoTools的整体架构和相关知识,我想在后面的章节中再慢慢介绍。今天先试用它的图形渲染功能来实现空间数据的可视化渲染。本节重点讲解Geotools当中的GTRenderer功能。
1、关于GtRenderer
关于在GeoTools中使用图像渲染和生成的功能,我们会使用到GtRenderer这个组件。它在GeoTools的官网原文中的介绍如下:
GTRenderer
renderer is the reason why you signed up for this whole GeoTools experience; you want to see a Map.
GTRenderer
is actually an interface; currently there are two implementations:
2、关于 图像生成架构
为了实现空间对象的生成,我们来看下它的后台使用类的生成关系。这个图可以参考管网给出的一个类关系图。
当然,上图中的相关接口和类中,尤其是在实现类中,实现类的方法和属性的定义是非常多的。但是在上图中做了一定的处理,没有展示那么多的方法。这里,我们会对StreamIngRenderer这个类比官网多一些的详细说明。 详细说明为什么使用流式计算,在实际的绘图过程中,有哪些方法是比较重要的
在面向对象的学习过程中,我们选择从接口或者抽象类,自顶向下的方式来研究相关的类。因此,在这里,我们首先对抽象的接口GTRenderer进行研究。GTRenderer的源码不多,主要定义的都是关于如何绘制的API,这里直接给出它的代码:
package org.geotools.renderer; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.util.Map; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.map.MapContent; import org.locationtech.jts.geom.Envelope; public interface GTRenderer { public void stopRendering(); public void addRenderListener(RenderListener listener); public void removeRenderListener(RenderListener listener); public void setJava2DHints(RenderingHints hints); public RenderingHints getJava2DHints(); public void setRendererHints(Map<?, ?> hints); public Map<Object, Object> getRendererHints(); public void setMapContent(MapContent mapContent); public MapContent getMapContent(); public void paint(Graphics2D graphics, Rectangle paintArea, AffineTransform worldToScreen); public void paint(Graphics2D graphics, Rectangle paintArea, Envelope mapArea); public void paint(Graphics2D graphics, Rectangle paintArea, ReferencedEnvelope mapArea); public void paint( Graphics2D graphics,Rectangle paintArea,Envelope mapArea,AffineTransform worldToScreen); public void paint(Graphics2D graphics, Rectangle paintArea,ReferencedEnvelope mapArea,AffineTransform worldToScreen); }
在上面的方法中,我已经把相关的注释全部删除,在这个渲染接口中其实比较重要的方法其实就那么几个,第一是设置RenderingHints,第二是设置MapContent对象,第三就是执行具体的绘制,即上面代码中的Paint方法,在上面的代码中,一共提供了5个重载的方法,在实际使用的时候,大家根据自己的需要来进行灵活的选择。
3、流式计算绘制
在定义了图片绘制的接口之后,最重要的是要实现图片的写入,这里需要来看一下GTRenderer对象的子类,即:
可以看到在StreamingRenderer中定义了非常多的属性以及方法来支撑图片的绘制。 接下来我们看下绘制的相关类,为什么渲染的速度这么快。
/** The thread pool used to submit the painter workers. */ private ExecutorService threadPool; private PainterThread painterThread;
在代码中可以看到以上代码片段,在进行绘制的时候,内部会使用线程池的方式来实现,所以这也是为什么处理效率比较高效的原因。
ExecutorService localThreadPool = threadPool; boolean localPool = false; if (localThreadPool == null) { localThreadPool = Executors.newSingleThreadExecutor(); localPool = true; } Future painterFuture = localThreadPool.submit(painterThread); List<CompositingGroup> compositingGroups = null;
从这个代码可以看出,这里的处理线程池,我们可以在外部传入一个指定的线程池来进行任务的处理。如果外部没有传入线程池,在内部也会创建出一个线程对象来,这样就保证了一定会有一个线程池来进行处理。可以看到,这里的线程池采用的是Future的方式,与我们常见的Thread的方式有所区别,请大家注意。
下面给出一个官网提供的将shp文件写出到图片文件的java实例代码。
public void saveImage(final MapContent map, final String file, final int imageWidth) { GTRenderer renderer = new StreamingRenderer(); renderer.setMapContent(map); Rectangle imageBounds = null; ReferencedEnvelope mapBounds = null; try { mapBounds = map.getMaxBounds(); double heightToWidth = mapBounds.getSpan(1) / mapBounds.getSpan(0); imageBounds = new Rectangle( 0, 0, imageWidth, (int) Math.round(imageWidth * heightToWidth)); } catch (Exception e) { // failed to access map layers throw new RuntimeException(e); } BufferedImage image = new BufferedImage(imageBounds.width, imageBounds.height, BufferedImage.TYPE_INT_RGB); Graphics2D gr = image.createGraphics(); gr.setPaint(Color.WHITE); gr.fill(imageBounds); try { renderer.paint(gr, imageBounds, mapBounds); File fileToSave = new File(file); ImageIO.write(image, "jpeg", fileToSave); } catch (IOException e) { throw new RuntimeException(e); } }
二、全球空间预览生成实战
在之前的博文中,我们讲解了如何使用Geotools来进行shapefile的空间数据的生成。这里还是以全球的矢量数据为例,结合sld空间样式控制。我们使用GeoTools来生成一张图片,使用上面讲到的GTRenderer对象来实现图片缩略图的生成。
1、pom.xml中关于图像生成依赖
要想实现在Geotools中进行图像生成,需要引入相关的依赖包。这里给出依赖的maven配置,请注意,这是基本的pom.xml,其它的geotools的依赖,请自行引入。
<dependency> <groupId>org.geotools</groupId> <artifactId>gt-referencing</artifactId> <version>${geotools.version}</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-epsg-hsql</artifactId> <version>${geotools.version}</version> </dependency> <dependency> <groupId>org.geotools</groupId> <artifactId>gt-epsg-extension</artifactId> <version>${geotools.version}</version> </dependency>
2、样式设置及地图资源绑定
为了让地图更好看,需要我们对地图进行样式的设置,关于SLD的解析和使用,我们不再赘述,直接提供代码。大家可以直接拷贝过去使用。
private Style createStyleFromSld(String uri) throws XPathExpressionException, IOException, SAXException, ParserConfigurationException { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document xmlDocument = db.parse(uri); XPath xPath = XPathFactory.newInstance().newXPath(); String version = xPath.compile("/StyledLayerDescriptor/@version").evaluate(xmlDocument); Configuration sldConf; if (version != null && version.startsWith("1.1")) { sldConf = new org.geotools.sld.v1_1.SLDConfiguration(); } else { sldConf = new org.geotools.sld.SLDConfiguration(); } StyledLayerDescriptor sld = (StyledLayerDescriptor) new org.geotools.xsd.DOMParser(sldConf, xmlDocument) .parse(); NamedLayer l = (NamedLayer) sld.getStyledLayers()[0]; Style style = l.getStyles()[0]; return style; }
加载样式之后,还需要将地图的样式和地图进行绑定。如下面的代码所示:
/** * 添加shp文件 * * @param shpPath */ @SuppressWarnings("deprecation") public void addShapeLayer(String shpPath, String sldPath) { try { File file = new File(shpPath); ShapefileDataStore shpDataStore = null; shpDataStore = new ShapefileDataStore(file.toURL()); // 设置编码 Charset charset = Charset.forName("GBK"); shpDataStore.setCharset(charset); String typeName = shpDataStore.getTypeNames()[0]; SimpleFeatureSource featureSource = null; featureSource = shpDataStore.getFeatureSource(typeName); // SLD的方式 Style style = null; try { style = createStyleFromSld(sldPath); } catch (Exception e) { } Layer layer = new FeatureLayer(featureSource, style); map.addLayer(layer); } catch (Exception e) { e.printStackTrace(); } }
上面两个方法都比较简单,而且在之前的博客中也曾经讲过,实现map的读取和样式的绑定。在介绍了上面的两个方法之后,我们来讲述如何进行图片的生成。
3、图片生成绘制
在讲解图片的绘制时,有几个小小的知识点是需要讲述的。首先是参考坐标,我们在进行绘图时,需要获取系统的参考坐标,然后根据参考坐标,将需要绘制的地图范围进行设置,也就是相当于生成ReferencedEnvelope。然后Graphics这个java的2d图像生成对象将Map对象绘制成图片,从而实现缩略图的生成功能。
/** * 根据四至、长、宽获取地图内容,并生成图片 * * @param paras * @param imgPath */ public void getMapContent(Map<String, Object> paras, String imgPath) { try { double[] bbox = (double[]) paras.get("bbox"); double x1 = bbox[0], y1 = bbox[1], x2 = bbox[2], y2 = bbox[3]; int width = (int) paras.get("width"), height = (int) paras.get("height"); // 设置输出范围 CoordinateReferenceSystem crs = DefaultGeographicCRS.WGS84; // CoordinateReferenceSystem crs = CRS.decode("EPSG:4326"); ReferencedEnvelope mapArea = new ReferencedEnvelope(x1, x2, y1, y2, crs); // 初始化渲染器 StreamingRenderer sr = new StreamingRenderer(); sr.setMapContent(map); // 初始化输出图像 BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics g = bi.getGraphics(); ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); Rectangle rect = new Rectangle(0, 0, width, height); // 绘制地图 sr.paint((Graphics2D) g, rect, mapArea); // 将BufferedImage变量写入文件中。 ImageIO.write(bi, "png", new File(imgPath)); } catch (Exception e) { e.printStackTrace(); } }
4、图片生成测试
下面我们进行实际的图片生成测试,测试的代码如下:
public static void main(String[] args) { long start = System.currentTimeMillis(); Shp2Image shp2img = new Shp2Image(); String shpPath = "F:/wzh_workspace_20210320/geotools-fx/src/main/resources/maps/countries.shp"; String sldPath = "F:/wzh_workspace_20210320/geotools-fx/src/main/resources/maps/countries1.1.0-2.sld"; String imgPath = "D:/countries0819-美国.png"; Map<String, Object> paras = new HashMap<String, Object>(); double[] bbox_usa = new double[] { -145.40999, 9.93976, -65.062300,81.12722 }; paras.put("bbox", bbox_usa); paras.put("width", 1020); paras.put("height", 800); shp2img.addShapeLayer(shpPath, sldPath); shp2img.getMapContent(paras, imgPath); System.out.println("缩略图生成完成,共耗时" + (System.currentTimeMillis() - start) + "ms"); }
以上就是完成将shp数据转为图片的生成代码,下面我们就可以运行相应的程序来进行调用测试。请注意,这里的bbox是想要生成的区域的空间区域的BBOX,也就是最大矩形外包框的坐标位置。还有需要生成图片的高度和宽度,在指定好这些参数之后就可以调用相应的应用程序来生成。
三、成果验证
下面来进行成果的验证,我们分别定义了以下的城市,首先是全世界的地图范围图片生成,然后是中国图片生成、日本及美国的地图范围生成。当然每个不同的城市的bbox是不一样的,还要传入不同的bbox值进行验证。
1、全球范围生成
全球的bbox范围值如下:
double[] bbox = new double[]
{-179.9999999999999716,-90.0000000188696276,180.0000000000000000,83.6274185352932022};
接下来看一下全球的生成视图:
2、我国的范围示意图
我国的经纬度位置范围大致是:
double[] bbox_china = new double[] { 73.409999999999716, -3.5300000188696276, 135.0623000000000000,53.6274185352932022 };
3、日本范围生成
日本的经纬度bbox范围如下:
double[] bbox_japan = new double[] { 122.409999999999716, 22.9300000188696276, 151.0623000000000000,47.1274185352932022 };
四、总结
以上就是本文的主要内容,本文主要使用Java语言,讲解如何使用GeoTools这个组件来进行空间Shapefile数据转换成图片,从而实现服务缩略图的功能。文章通过实例的模式讲解预览图片的生成,对于在研究Java的服务预览图片生成的同学和朋友有一定的参考价值。 阅读本文,不仅可以学习到如何将shp数据转换成图片缩略图,还可以进一步复习geotools包的具体使用。当前,关于图像生成的很多资源不足或者介绍不够深入,这里将比较详细的解释相关知识,同时给出具体的实例。行文仓促,定有不当之处,恳请各位专家学者博友在评论区留下宝贵的意见,万分感激。