Geoserver源码解读七 插件(二)扩展图层预览界面

系列文章目录

Geoserver源码解读一 环境搭建

Geoserver源码解读二 主入口

Geoserver源码解读三 GeoServerBasePage

Geoserver源码解读四 REST服务

Geoserver源码解读五 Catalog

Geoserver源码解读六 插件(怎么在开发模式下使用)

前言

看过前面几篇文章的朋友应该知道,Geoserver是使用Spring+Wicket的,本文就以图层预览界面添加属性表的例子,看下geoserver是怎么利用Spring和Wicket优雅的扩展新页面或者模块的

一、要实现什么功能

打开图层预览界面,默认的样子是这样的

联想到好多桌面端GIS软件(比如Arcgis、QGIS、超图)都有一个功能叫“打开属性表”,也就是说查看矢量图层的表数据,方便我们预览矢量图层里面到底有什么东西,于是乎突发奇想,想要再geoserver的图层预览界面添加一个查看属性表的功能

查看属性表的页面大概长这样

二、Geoserver的插件化开发模式

1.插件化开发模式的好处

安装过geoserver插件的朋友应该知道,只要吧插件的jar包放到geoserver安装目录的lib文件夹下就可以,不需要其它的任何操作,简单、

lib文件夹目录:安装目录\GeoServer\lib

2.怎么实现的

为什么它能做到那么优雅的安装插件呢,这就是spring这个大管家的功劳了,抽丝剥茧的看下geoserver的菜单目录加载模式

在GeoServerBasePage.html代码中看到起菜单的变量是category.links

org/geoserver/web/GeoServerBasePage.html

<li class="navigation-tab" wicket:id="category">
  <div class="navigation-tab-header">
    <span wicket:id="category.header">[Category Header]</span>
  </div>
  <ul class="navigation-tab-content plain">
    <li class="nav-administer-service" wicket:id="category.links">
      <a wicket:id="link">
        <img src="#" wicket:id="link.icon"/><span wicket:id="link.label"></span>
      </a>
    </li>
  </ul>
</li>

其对应的java代码是

List<MenuPageInfo<GeoServerBasePage>> infos =
                (List) filterByAuth(getGeoServerApplication().getBeansOfType(MenuPageInfo.class));
final Map<Category, List<MenuPageInfo<GeoServerBasePage>>> links = splitByCategory(infos);

List<MenuPageInfo<GeoServerBasePage>> standalone =
                links.containsKey(null) ? links.get(null) : new ArrayList<>();
links.remove(null);

List<Category> categories = new ArrayList<>(links.keySet());

重点在这行代码

(List) filterByAuth(getGeoServerApplication().getBeansOfType(MenuPageInfo.class))

它的作用是获取所有MenuPageInfo类型的java类

在 spring的 applicationContext.xml里面能看到各种MenuPageInfo类型的bean,比如这个

  <bean id="wmsServicePage" class="org.geoserver.web.services.ServiceMenuPageInfo">
    <property name="id" value="wms"/>
    <property name="titleKey" value="wms.title"/>
    <property name="descriptionKey" value="wms.description"/>
    <property name="componentClass" value="org.geoserver.wms.web.WMSAdminPage"/>
    <property name="icon" value="server_map.png"/>
    <property name="category" ref="servicesCategory"/>
    <property name="serviceClass" value="org.geoserver.wms.WMSInfo"/>
  </bean> 

也就是说只要在spring中注册了MenuPageInfo类型的java类,都会被获取到,不管是哪个包下面的

以此类推就明白了Geoserver是怎么优雅的加载插件或者扩展项目了

三、图层预览页面逻辑

打开图层预览页面的html代码

代码位置:org/geoserver/web/demo/MapPreviewPage.html

  <!-- The fragment for the common links -->
  <wicket:fragment wicket:id="commonLinks">
    <span style="white-space: nowrap;" wicket:id="commonFormat">
      <a target="_blank" href="#" wicket:id="theLink">theTitle</a>&nbsp;
    </span>
  </wicket:fragment>

再看它对应的java代码

private List<ExternalLink> commonFormatLinks(PreviewLayer layer) {
    List<ExternalLink> links = new ArrayList<>();
    List<CommonFormatLink> formats =
                getGeoServerApplication().getBeansOfType(CommonFormatLink.class);
    Collections.sort(formats);
    for (CommonFormatLink link : formats) {
        links.add(link.getFormatLink(layer));
    }
    return links;
}

能看出来,它和上面加载菜单的是一个套路,都用到了获取所有某类型的javaBean

getGeoServerApplication().getBeansOfType 

那么如果想要加一个属性表的常用格式链接,就需要创建一个CommonFormatLink类型的javaBean

四、属性表连接添加

方式一、直接在源码中添加

参照KMLFormatLink.java,我在这个位置建了个javaBean

org/geoserver/web/demo/PropertySheetFormatLink.java

public class PropertySheetFormatLink extends CommonFormatLink {

    @Override
    public ExternalLink getFormatLink(PreviewLayer layer) {

        ExternalLink olLink =
                new ExternalLink(
                        this.getComponentId(),
                        //这个位置临时写成百度的地址,后期再改
                        "http://www.baidu.com",
                        (new StringResourceModel(this.getTitleKey(), null, null)).getString());
        olLink.setVisible(layer.getType() == PreviewLayer.PreviewLayerType.Vector && layer.hasServiceSupport("WFS"));
        return olLink;
    }
}

 然后再配置文件applicationContext.xml中注册该javaBean

<bean id="propertySheetPreview" class="org.geoserver.web.demo.PropertySheetFormatLink">
    <property name="id" value="propertySheet"/>
    <property name="titleKey" value="propertySheet.title"/>
    <property name="order" value="40"/>
</bean>

能看到上面有个titleKey的属性,需要在配置文件中给赋下值(上篇文章的i18n有讲到这个)

在通用包GeoServerApplication.properties中添加

propertySheet.title=PropertySheet

在中文包GeoServerApplication_zh.properties中添加(中文需要改为ISO-8859编码)

propertySheet.title=\u5c5e\u6027\u8868

方式二、在新建模块中添加

虽说属性表的功能能在源码上直接改,但可扩展性就降低了,别人的geoserver想用你的功能就很难,所以更推荐新建一个模块,作为一个插件去开发。在上篇文章中讲到了geoserver插件的一些东西的存储位置在src下面的extension文件夹中,也可以说是extension模块中,我也按照它的思路去在这儿建一个模块

建好模块后在主模块【src/web/app】的pom文件中添加一个profile预设

<profile>
   <id>vector-plugin</id>
    <dependencies>
      <dependency>
       <groupId>org.geoserver.extension</groupId>
       <artifactId>vector-plugin</artifactId>
        <version>${project.version}</version>
      </dependency>
     </dependencies>
</profile>

添加预设后就能maven中看到了,勾选的操作就相当于把插件作为一个模块引入到主模块了

如果你那儿爆红或者没有出现这个复选框,就需要重新构建下你新建或者修改的模块

重起项目后就能看到这个属性表的超链接了

需要注意的是这个连接类型【ExternalLink】是定死的,意思是在新的窗口打开这个连接,相当于“window.open('https://www.example.com', '_blank');”。这点儿就很难受,本来想使用其他页面的【BookmarkablePageLink】类型的连接在当前页面显示,不过人家既然这样限制了,估计geoserver官方有它的理由吧。

五、属性表页面设计

看了geoserver源码后,发现它的页面大致分为三类

类型
静态页面geoserver文件目录的【www】位置,可以直接访问http://localhost:8080/geoserver/www/ol-demo.html
Wicket 大多数页面使用的方式,是 GeoServer Web 界面的核心,
FreeMarker一个模板引擎,多用于生成页面的动态内容,有点像接口

单纯的静态页面就没有学习的必要了,这里暂时选择用Wicket 去实现属性表的页面,如果要使用Wicket实现的话有个小问题需要注意下,ExternalLink 是专门用于外部链接的,因此直接在 ExternalLink 中引用 Wicket 页面可能会遇到一些问题。如果需要在 Wicket 页面中进行页面间的跳转,通常会使用 BookmarkablePageLink 或 PageLink 组件,但是此处是人家geoserver要求的让用ExternalLink  为了尽可能保持Geoserver优雅的风格,在上面第四章建的PropertySheetFormatLink.java中只能给转换下 

// 生成目标页面的 URL
String targetPageUrl = getRequestCycle().urlFor(PropertySheetPage.class, null).toString();
        // 创建 ExternalLink
ExternalLink externalLink = new ExternalLink(this.getComponentId(), targetPageUrl, (new StringResourceModel(this.getTitleKey(), null, null)).getString());

上面代码的PropertySheetPage.class 就是要即将创建的Wicket 页面类

1.html页面设计

代码位置:src/extension/vector-plugin/src/main/java/org/geoserver/vector/preview/PropertySheetPage.html

<html xmlns:wicket="http://wicket.apache.org/">
<head>
<title><wicket:message key="propertySheet.title"></wicket:message></title>
  <wicket:head>
    <meta http-equiv="X-UA-Compatible" content="IE=10" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <wicket:link>
      <link rel="stylesheet" href="css/geoserver.css" type="text/css" media="screen, projection" />
    </wicket:link>
  </wicket:head>
</head>
<body>
<wicket>
<!--   the table component -->
  <div wicket:id="table"></div>
  <!-- The fragment for the icon -->
  <wicket:fragment wicket:id="iconFragment">
    <img wicket:id="layerIcon"/>
  </wicket:fragment>
</wicket>
</body>
</html>

需要注意的是因为这个页面是独立存在的,所以默认没有和GeoServerBasePage共享css,所以我重新拷贝了一个geoserver.css,这样就能尽可能一样的和原页面风格保持一致

2.对应java类代码

首相要简单讲一下Wicket 页面的两个核心概念和设计模式,从而更好的理解Geoserver烦人源码

2.1 Wicket 两大核心概念

  • 数据提供者(DataProvider)

数据提供者是一个接口,它定义了如何获取数据集合。数据提供者通常用于复杂的数据操作,如分页、排序和过滤。Wicket 提供了多种数据提供者的实现,如 ListDataProviderSortableDataProviderFilterableDataProvider 等。

数据提供者不直接与 Wicket 组件绑定,而是通过模型(Model)来提供数据。数据提供者通常用于复杂的数据操作,而模型用于简单地绑定数据到组件。

  • 模型(Model)

模型是一个接口,它定义了如何获取和设置数据。Wicket 提供了多种模型实现,如 CompoundPropertyModelMapModelLoadableDetachableModel 等。

模型通常与 Wicket 组件绑定,提供组件的数据。模型可以是简单的,也可以是复杂的,取决于你的应用需求。

2.2 Wicket 设式计模

在设计 Wicket 应用程序时,通常会使用以下模式:

  1. 单数据源:每个页面或组件通常有一个数据源,它可以是一个数据提供者或一个模型。

  2. 模型和数据提供者的分离:通常情况下,模型负责绑定数据到组件,而数据提供者负责提供数据。模型可以是一个简单的 Model,而数据提供者可以是更复杂的 SortableDataProvider

  3. 数据绑定:使用 Wicket 的数据绑定特性,你可以轻松地将模型或数据提供者绑定到组件,如表格、表单等。

  4. 状态管理:在 Wicket 中,状态通常由模型和数据提供者来管理。你可以使用 LoadableDetachableModel 来延迟加载数据,或者使用 SortableDataProvider 来提供排序的数据。

2.3 设计java类

代码位置:src/extension/vector-plugin/src/main/java/org/geoserver/vector/preview/PropertySheetPage.java

我帮大家看了下geoserver那一坨坨枯燥的代码,结合上面的两个概念和设计模式,总结出Geoserver主要是使用上面的第2中设计模式模型和数据提供者的分离,也就是说创建一个Provider,然后在Provider中创建一个Model,然后此处也使用这种方案,现在需要创建的有四个类

FeatureAttributionModel.javaModel
PropertySheetProvider.javaProvider
FeatureAttributionInfoImpl.java可序列化的Feature实体类
PropertySheetPage.java主页面类
2.3.1 FeatureAttributionInfoImpl

这是一个可序列化的要素实体类

// interface 
public interface FeatureAttributionInfo extends Serializable {
    SimpleFeature getSimpleFeature();
}

// 实现类(通常和上面的interface 分两个文件存储)
public class FeatureAttributionInfoImpl implements FeatureAttributionInfo {
    protected transient SimpleFeature simpleFeature;
    public FeatureAttributionInfoImpl(SimpleFeature simpleFeature){
        this.simpleFeature = simpleFeature;
    }

    @Override
    public SimpleFeature getSimpleFeature() {
        return this.simpleFeature;
    }
}

没来没有打算建这个类,创建表格组件的时候想着直接用SimpleFeature就行

// 初步方案
GeoServerTablePanel<SimpleFeature> table;
table = new GeoServerTablePanel<SimpleFeature>("table", provider){}

// 后期优化
GeoServerTablePanel<FeatureAttributionInfo > table;
table = new GeoServerTablePanel<FeatureAttributionInfo >("table", provider){}

但是使用之后就一直报错,说是wiket的数据一般是用于交互的所以必须实现Serializable接口,也就是说必须可序列化的,而SimpleFeature不实现Serializable接口,跟了下源码,发现是这个地方做了限制(GeoServerDataProvider.java)

public interface Property<T> extends Serializable

所以就只能再定义一个可序列化的中间类

2.3.2 FeatureAttributionModel

这是个Model类,实际上没有太多东西只用于传值

class FeatureAttributionModel extends LoadableDetachableModel<FeatureAttributionInfo> {
    FeatureAttributionInfo featureAttributionInfo;


    public FeatureAttributionModel(FeatureAttributionInfo pl) {
        super(pl);
        featureAttributionInfo = pl;
    }

    @Override
    protected FeatureAttributionInfo load() {
        return featureAttributionInfo;
    }
}
2.3.3 PropertySheetProvider

继承GeoServerDataProvider<FeatureAttributionInfo>,用于通用的表格

public class PropertySheetProvider extends GeoServerDataProvider<FeatureAttributionInfo> {
    public PropertySheetProvider(String layerName) {
        super();
        this.layerName = layerName;
        CacheBuilder<Object, Object> builder = CacheBuilder.newBuilder();

        cache = builder.expireAfterWrite(DEFAULT_CACHE_TIME, TimeUnit.SECONDS).build();
        // Callable which internally calls the size method
        sizeCaller = new SizeCallable();
        // Callable which internally calls the fullSize() method
        fullSizeCaller = new FullSizeCallable();
        initPropertyList();
    }
}

里面有一些关键的代码我列了出来

1.初始化属性表的,用于处理表格的列和属性表字段的对应关系

   /**
     * 初始化属性字段列表
     */
    protected void initPropertyList(){
        final Catalog catalog = getCatalog();
        this.propertyList.clear();
        LayerInfo currentLayerInfo = catalog.getLayerByName(this.layerName);
        ResourceInfo resourceInfo = currentLayerInfo.getResource();
        List<AttributeDescriptor> attributeDescriptors = null;
        // 从代理模式中获取到原值
        if (Proxy.isProxyClass(resourceInfo.getClass())) {
            if(Proxy.getInvocationHandler(resourceInfo) instanceof ModificationProxy){
                FeatureTypeInfoImpl featureTypeInfo = (FeatureTypeInfoImpl)ModificationProxy.handler(resourceInfo).getProxyObject();
                try {
                    attributeDescriptors = ((SimpleFeatureType) featureTypeInfo.getFeatureType()).getAttributeDescriptors();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }else {
            SimpleFeatureType simpleFeatureType = null;
            try {
                simpleFeatureType = (SimpleFeatureType) ((SecuredFeatureTypeInfo) resourceInfo).getFeatureType();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            attributeDescriptors = simpleFeatureType.getAttributeDescriptors();
        }
        // 遍历属性描述符数组,为每个属性创建列并添加到列集合中
        for (AttributeDescriptor descriptor : attributeDescriptors) {
            if(!(descriptor instanceof GeometryDescriptorImpl)){
                // 获取属性名称及其类型
                String name = descriptor.getLocalName();

                this.propertyList.add(new AbstractProperty<FeatureAttributionInfo>(name) {
                    @Override
                    public Object getPropertyValue(FeatureAttributionInfo item) {
                        return item.getSimpleFeature().getProperties(name).iterator().next().getValue();
                    }
                });
            }
        }
    }

2.查询迭代器

    @Override
    public Iterator<FeatureAttributionInfo> iterator(final long first, final long count) {
        SimpleFeatureCollection simpleFeatureIterator = null;
        try {
            simpleFeatureIterator = filteredItems(first, count);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        SimpleFeatureCollection finalSimpleFeatureIterator = simpleFeatureIterator;
        return new Iterator<FeatureAttributionInfo>() {
            @Override
            public boolean hasNext() {
                return finalSimpleFeatureIterator.features().hasNext();
            }

            @Override
            public FeatureAttributionInfo next() {

                SimpleFeature simpleFeature =  finalSimpleFeatureIterator.features().next();
                FeatureAttributionInfo featureAttributionInfo = new FeatureAttributionInfoImpl(simpleFeature);
                return featureAttributionInfo;
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException("Remove operation is not supported");
            }
        };
    }
2.3.4 PropertySheetPage

主类使用Provider从而实现功能

    PropertySheetProvider provider;

    GeoServerTablePanel<FeatureAttributionInfo> table;

    // private transient List<String> availableWFSFormats;

    public PropertySheetPage(final PageParameters parameters) {
        // 从PageParameters中获取layerName
        String layerName = parameters.get("layerName").toString();
        provider = new PropertySheetProvider(layerName);
        // build the table
        table =
                new GeoServerTablePanel<FeatureAttributionInfo>("table", provider) {

                    private static final long serialVersionUID = 1L;

                    @Override
                    protected Component getComponentForProperty(
                            String id,
                            IModel<FeatureAttributionInfo> itemModel,
                            Property<FeatureAttributionInfo> property) {
                        return new Label(id, property.getModel(itemModel));
                    }
                };
        table.setOutputMarkupId(true);
        add(table);
    }

总结

其实总体就是参照图层预览页面,然后做的一些改造,最终的成果虽说实现了功能,但还是有点儿小问题,页面初始加载的可慢,我暂时还没发现问题出在哪个地方,后期发现了我再回来补充,如果哪个大佬知道什么原因,也欢迎在评论区留言

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答您的问题。首先需要了解一下GeoServer的架构和工作原理,以及shapefile图层的相关知识。 GeoServer是一个基于Java的开源地理信息系统软件,它可以将地理空间数据发布为Web服务。它的工作原理是将地理空间数据存储在数据库中,然后将数据通过WMS、WFS等协议发布为Web服务。 shapefile是一种常见的地理信息数据格式,它由三个文件组成:.shp、.dbf和.shx文件。其中.shp文件包含了地理要素的几何形状信息,.dbf文件包含了地理要素的属性信息,.shx文件是用来提高访问速度的索引文件。 为了自动发布shapefile图层,可以考虑编写一个插件来实现。具体步骤如下: 1. 创建一个新的GeoServer插件项目,并添加依赖项。 2. 实现一个自定义的发布工具,用来将shapefile文件上传到GeoServer中,并创建对应的图层。 3. 实现一个自定义的数据存储,用来管理shapefile图层的数据。 4. 实现一个自定义的样式管理器,用来管理shapefile图层的样式。 5. 扩展GeoServer的REST API,以便我们能够在UI中访问我们的自定义插件。 6. 编写一些测试用例,确保我们的插件能够正常工作。 以上是大致的步骤,具体实现方法需要根据您的需求进行调整。如果您需要更详细的信息,可以参考GeoServer官方文档中的扩展开发部分,或者在CSDN上搜索相关的教程和例子。 希望能对您有所帮助,如果您还有其他问题,可以随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值