На одном из текущих проектов мы строим геоинформационную систему. Работаем с геоданными через 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 секунд.
Загрузка кластеризированных маркеров

| 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 | 
Загрузка отдельных маркеров

| 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 | 
Так что простое объединение маркеров в кластеры, позволило повысить скорость загрузки страницы в сотню раз.
