Fork me on GitHub

Кластеризация маркеров в GeoServer May 10, 2017

На одном из текущих проектов мы строим геоинформационную систему. Работаем с геоданными через PostGIS и GeoServer. Объектов на карте достаточно много и в перспективе будет всё больше. Отрисовка всех маркеров на крупном масштабе заставляет геосервер нагружать систему на 100%. Для оптимизации работы системы, а также повышения наглядности для пользователя. Отдельные маркеры на карте необходимо группировать в кластеры.

Как оказалось, сделать это силами геосервeра очень просто, но пока этот способ был найден, пришлось перерыть всю документацию по геосерверу и весь Gis StackExchange. В результате группировка точек в кластеры осуществляется с помощью векторной трансформации vec:PointStacker.

Кластеризация точек

Для объединения объектов в один кластер необходимо в стиле слоя объявить векторную трансформацию vec:PointStacker.

Данная трансформация принимает следующие параметры:

  • cellSize — Размер ячейки в пределах которой точки будут объединяться в кластер.
  • outputBBOX — Координаты углов результирующего тайла.
  • outputWidth — Ширина результирующего тайла.
  • outputHeight — Высота результирующего тайла.

На выходе получаем кластеры с информацие о количестве объектов.

  • geom — геометрия кластера.
  • count — число объектов вошедших в кластер.
  • countUnique — число уникальных объектов в кластере.

Размер ячейки выбираем “на глаз”, чтобы объекты красиво группировались с точки зрения пользователя. Чем больше ячейка, тем меньше кластеров получается в результате.

Остальные параметры передаем из исходного тайла через функцию env входящего wms запроса.

<Transformation>
    <ogc:Function name="vec:PointStacker">
        <ogc:Function name="parameter">
            <ogc:Literal>data</ogc:Literal>
        </ogc:Function>
        <ogc:Function name="parameter">
            <ogc:Literal>cellSize</ogc:Literal>
            <ogc:Literal>99</ogc:Literal>
        </ogc:Function>
        <ogc:Function name="parameter">
            <ogc:Literal>outputBBOX</ogc:Literal>
            <ogc:Function name="env">
                <ogc:Literal>wms_bbox</ogc:Literal>
            </ogc:Function>
        </ogc:Function>
        <ogc:Function name="parameter">
            <ogc:Literal>outputWidth</ogc:Literal>
            <ogc:Function name="env">
                <ogc:Literal>wms_width</ogc:Literal>
            </ogc:Function>
        </ogc:Function>
        <ogc:Function name="parameter">
            <ogc:Literal>outputHeight</ogc:Literal>
            <ogc:Function name="env">
                <ogc:Literal>wms_height</ogc:Literal>
            </ogc:Function>
        </ogc:Function>
    </ogc:Function>
</Transformation>

Далее для кластеров необходимо задать стиль отображения. Будем отображать кластеры на карте в виде кругов с текстом соответствующим количеству объектов вошедших в кластер. Размер круга выбираем в зависимости от числа объектов вошедших в него, например по формуле:

25 + log (count) * 5

Стиль состоит из описания текста TextSymbolizer и точки PointSymbolizer.

<Rule>
    <Name>Point group cluster</Name>
    <Title>Realties group</Title>
    <TextSymbolizer>
        <Label>
            <ogc:PropertyName>count</ogc:PropertyName>
        </Label>
        <Font>
            <CssParameter name="font-family">Arial</CssParameter>
            <CssParameter name="font-size">12</CssParameter>
            <CssParameter name="font-weight">bold</CssParameter>
        </Font>
        <LabelPlacement>
            <PointPlacement>
                <AnchorPoint>
                    <AnchorPointX>0.5</AnchorPointX>
                    <AnchorPointY>0.8</AnchorPointY>
                </AnchorPoint>
            </PointPlacement>
        </LabelPlacement>
        <Fill>
            <CssParameter name="fill">#000</CssParameter>
            <CssParameter name="fill-opacity">1.0</CssParameter>
        </Fill>
        <VendorOption name="partials">true</VendorOption>
    </TextSymbolizer>
    <PointSymbolizer>
        <Graphic>
            <Mark>
                <WellKnownName>circle</WellKnownName>
                <Fill>
                    <CssParameter name="fill">#1E90FF</CssParameter>
                    <CssParameter name="fill-opacity">0.75</CssParameter>
                </Fill>
            </Mark>
            <Size>
                <ogc:Add>
                    <ogc:Literal>25</ogc:Literal>
                    <ogc:Mul>
                        <ogc:Function name="log">
                           <ogc:PropertyName>count</ogc:PropertyName>
                        </ogc:Function>
                        <ogc:Literal>5</ogc:Literal>
                    </ogc:Mul>
                </ogc:Add>
            </Size>
        </Graphic>
    </PointSymbolizer>
</Rule>

Для описания стиля текста кластера важно указать опцию partials, иначе любой текст на границе тайла не будет отображаться и, возможно, возникновение кластеров без подписи числа объектов.

<VendorOption name="partials">true</VendorOption>

В одном слое у нас отображаются объекты разных торговых сетей и стиль иконок должен быть различным для них. Но при кластеризации сохраняется только информация о количестве объектов, но никак не о качестве (индивидуальных особенностях). Даже для кластера из одного объекта нельзя получить свойства этого объекта. Поэтому будем запускать кластеризацию, начиная с некоторого масштаба. А для меньшего масштаба отображать все точки отдельно.

Разделить отображения можно с помощью задания минимального и максимального масштаба отображения.

<!-- Стиль отдельного объекта для масштабов меньше заданного максимальной величиной -->
<MaxScaleDenominator>70000</MaxScaleDenominator>

<!-- Стиль кластера для всех масштабов больше чем минимальная величина -->
<MinScaleDenominator>70000</MinScaleDenominator>

Результат

Весь стиль объектов

<?xml version="1.0" encoding="UTF-8"?>
<StyledLayerDescriptor version="1.0.0"
    xmlns="http://www.opengis.net/sld"
    xmlns:ogc="http://www.opengis.net/ogc"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd">
    <NamedLayer>
        <Name>Realties</Name>
        <UserStyle>
            <Name>Realties</Name>
            <Title>Realties objects icons</Title>
            <Abstract>SVG styles for realties objects</Abstract>

            <FeatureTypeStyle>
                <Rule>
                    <MaxScaleDenominator>70000</MaxScaleDenominator>
                    <Title>Realty</Title>
                    <PointSymbolizer>
                        <Graphic>
                            <ExternalGraphic>
                                <OnlineResource
                                    xlink:type="simple"
                                    xlink:href="./img/${logo_name}.svg" />
                                <Format>image/svg+xml</Format>
                            </ExternalGraphic>
                            <Size>
                                <ogc:Literal>35</ogc:Literal>
                            </Size>
                        </Graphic>
                    </PointSymbolizer>
                </Rule>
            </FeatureTypeStyle>

            <FeatureTypeStyle>
                <Transformation>
                    <ogc:Function name="vec:PointStacker">
                        <ogc:Function name="parameter">
                            <ogc:Literal>data</ogc:Literal>
                        </ogc:Function>
                        <ogc:Function name="parameter">
                            <ogc:Literal>cellSize</ogc:Literal>
                            <ogc:Literal>99</ogc:Literal>
                        </ogc:Function>
                        <ogc:Function name="parameter">
                            <ogc:Literal>outputBBOX</ogc:Literal>
                            <ogc:Function name="env">
                                <ogc:Literal>wms_bbox</ogc:Literal>
                            </ogc:Function>
                        </ogc:Function>
                        <ogc:Function name="parameter">
                            <ogc:Literal>outputWidth</ogc:Literal>
                            <ogc:Function name="env">
                                <ogc:Literal>wms_width</ogc:Literal>
                            </ogc:Function>
                        </ogc:Function>
                        <ogc:Function name="parameter">
                            <ogc:Literal>outputHeight</ogc:Literal>
                            <ogc:Function name="env">
                                <ogc:Literal>wms_height</ogc:Literal>
                            </ogc:Function>
                        </ogc:Function>
                    </ogc:Function>
                </Transformation>

                <Rule>
                    <MinScaleDenominator>70000</MinScaleDenominator>
                    <Name>Point group cluster</Name>
                    <Title>Realties group</Title>
                    <TextSymbolizer>
                        <Label>
                            <ogc:PropertyName>count</ogc:PropertyName>
                        </Label>
                        <Font>
                            <CssParameter name="font-family">Arial</CssParameter>
                            <CssParameter name="font-size">12</CssParameter>
                            <CssParameter name="font-weight">bold</CssParameter>
                        </Font>
                        <LabelPlacement>
                            <PointPlacement>
                                <AnchorPoint>
                                    <AnchorPointX>0.5</AnchorPointX>
                                    <AnchorPointY>0.8</AnchorPointY>
                                </AnchorPoint>
                            </PointPlacement>
                        </LabelPlacement>
                        <Fill>
                            <CssParameter name="fill">#000</CssParameter>
                            <CssParameter name="fill-opacity">1.0</CssParameter>
                        </Fill>
                        <VendorOption name="partials">true</VendorOption>
                    </TextSymbolizer>
                    <PointSymbolizer>
                        <Graphic>
                            <Mark>
                                <WellKnownName>circle</WellKnownName>
                                <Fill>
                                    <CssParameter name="fill">#1E90FF</CssParameter>
                                    <CssParameter name="fill-opacity">0.75</CssParameter>
                                </Fill>
                            </Mark>
                            <Size>
                                <ogc:Add>
                                    <ogc:Literal>25</ogc:Literal>
                                    <ogc:Mul>
                                        <ogc:Function name="log">
                                           <ogc:PropertyName>count</ogc:PropertyName>
                                        </ogc:Function>
                                        <ogc:Literal>5</ogc:Literal>
                                    </ogc:Mul>
                                </ogc:Add>
                            </Size>
                        </Graphic>
                    </PointSymbolizer>
                </Rule>
            </FeatureTypeStyle>

        </UserStyle>
    </NamedLayer>
</StyledLayerDescriptor>

Для сравнения представлены изображения с кластеризацией объектов и без неё.

Также кластеризация позволила значительно увеличить производительность системы. Рендер большого числа отдельных маркеров выполнялся медленно и существенно нагружал процессор. Часто соединение обрывалось по 504. Объединение в кластеры работает очень быстро без нагрузки на систему.

Кластеризованный результат (полная карта) загружается в среднем за 1.6 секунд. Тогда как отдельные маркеры грузились порядка 130 секунд.

Загрузка кластеризированных маркеров

waterfall cluster

Load Time First Byte Start Render Speed Index Interactive (beta) Time Requests Bytes In
1.647s 1.557s 3.718s 3718 > 3.763s 1.677s 2 9 KB

Загрузка отдельных маркеров

waterfall points

Load Time First Byte Start Render Speed Index Interactive (beta) Time Requests Bytes In
130.397s 8.268s 21.934s 57691 8.445s 130.397s 1 960 KB

Так что простое объединение маркеров в кластеры, позволило повысить скорость загрузки страницы в сотню раз.