Как стать автором
Обновить

Автоматическое создание интерактивных карт

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров5.5K

Решение подобных задач предусмотрено в QGIS с помощью специального модуля "qgis2web" , можно создать страничку с интерактивной картой на одной из библиотек: OpenLayers, Leaflet или Mapbox. И полученный результат в полне себе годится для интеграции на веб страницу для последующего просмотра и использования.

Однако в данной статье будет рассмотрен подход к автоматизированному созданию карт с использованием следующего технологического стека: QGIS, PostgreSQL, Django, Leaflet. Идея родилась ввиду необходимости быстрого создания интерактивных карт из уже созданных проектов в QGIS. Хочу сразу оговорится что проекты создаются без использования локальных слоев, а исключительно с использование БД для их хранения.

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

Слои и окно отображения карты в программе
Слои и окно отображения карты в программе

Сам файл проекта формата QGZ является архивом содержащим в себе уже QGS, который фундаментально представляет структуру формата XML и годен к парсингу. Стоит также отметить, что помимо самого файла проекта можно получить и сам стиль одного слоя, для этого в программе предусмотрен экспорт стиля, который возвращает QML , который тоже подходит для реализации нашей задачи. Ниже представлен пример оформления карты численности населения в формате QML .

  <renderer-v2 symbollevels="0" attr="nasel_2000" referencescale="-1" graduatedMethod="GraduatedColor" enableorderby="0" forceraster="0" type="graduatedSymbol">
    <ranges>
      <range upper="13104200.000000000000000" label="8 590 000 - 13 100 000" render="true" lower="8591700.000000000000000" symbol="0"/>
      <range upper="8591700.000000000000000" label="4 240 000 - 8 590 000" render="true" lower="4239100.000000000000000" symbol="1"/>
      <range upper="4239100.000000000000000" label="3 410 000 - 4 240 000" render="true" lower="3407100.000000000000000" symbol="2"/>
      <range upper="3407100.000000000000000" label="2 570 000 - 3 410 000" render="true" lower="2568200.000000000000000" symbol="3"/>
      <range upper="2568200.000000000000000" label="2 020 000 - 2 570 000" render="true" lower="2023800.000000000000000" symbol="4"/>
      <range upper="2023800.000000000000000" label="1 530 000 - 2 020 000" render="true" lower="1533200.000000000000000" symbol="5"/>
      <range upper="1533200.000000000000000" label="1 150 000 - 1 530 000" render="true" lower="1152500.000000000000000" symbol="6"/>
      <range upper="1152500.000000000000000" label="770 000 - 1 150 000" render="true" lower="771400.000000000000000" symbol="7"/>
      <range upper="771400.000000000000000" label="340 000 - 770 000" render="true" lower="337300.000000000000000" symbol="8"/>
      <range upper="337300.000000000000000" label="0 - 340 000" render="true" lower="0.000000000000000" symbol="9"/>
    </ranges>
    <symbols>
      <symbol name="0" alpha="1" type="fill" clip_to_extent="1" force_rhr="0">
        <data_defined_properties>
          <Option type="Map">
            <Option name="name" type="QString" value=""/>
            <Option name="properties"/>
            <Option name="type" type="QString" value="collection"/>
          </Option>
        </data_defined_properties>
        <layer locked="0" pass="0" enabled="1" class="SimpleFill">
          <Option type="Map">
            <Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
            <Option name="color" type="QString" value="34,102,51,255"/>
            <Option name="joinstyle" type="QString" value="bevel"/>
            <Option name="offset" type="QString" value="0,0"/>
            <Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
            <Option name="offset_unit" type="QString" value="MM"/>
            <Option name="outline_color" type="QString" value="35,35,35,255"/>
            <Option name="outline_style" type="QString" value="solid"/>
            <Option name="outline_width" type="QString" value="0.26"/>
            <Option name="outline_width_unit" type="QString" value="MM"/>
            <Option name="style" type="QString" value="solid"/>
          </Option>

Для построения портала с генерируемыми страницами для новых карт, был выбран фреймворк Django, который в свою очередь позволяет генерировать код на JS и HTML. Остановимся на этом поподробнее. Развертываем Django, и формируем нашу шаблонную страницу для будущих карт. Она будет состоять из самой карты, менеджера выбора карт и года на который, мы хотим увидеть, в данном примере, население с Росстата по регионам страны, плюс добавим окно для условных обозначений и менеджер слоев с масштабной линейкой. Все это создается на HTML без каких либо особенностей.

После чего с помощью языка шаблонов Jinja мы будем интегрировать основные стилистики из файла стилей QML в код на JS для Leaflet. Я не буду подробно останавливаться на данных технологиях ввиду большого количества инструкций в интернете, а заострю внимание на том как произвести необходимую конвертацию информации.

Для конвертации буду использовать библиотеку XML для python и вытяну необходимую информацию из файла стилистики. Мне соответственно необходимо получить для полигонов толщины линий, цвета заливки и обводки, и прозрачность, для каждого промежутка данных. Для точечных объектов, необходимо также получить радиус значка если это пунсон. Соответственно наша задача сформировать словарь, который будет хранить в себе информацию об выбранных промежутках (с уточнением их названия и сокращенной записи) для отображения стиля и о самом стиле, называться он будет будет style_params. dict_out_style необходим для передачи общего пула настроек в интерактивную карту.

def layer_style_js(layer_name):
    parser = ET.iterparse(layer_name)
    style_params = {}
    field_style = None
    count = 0
    max_diameter = 0
    dict_out_style = {}
    for event, element in parser:
        if element.tag == 'renderer-v2':
            field_style = element.get('attr')
        style_param = {}
        if element.tag == 'range':
            style_param['symbol'] = element.get('symbol')
            style_param['lower'] = element.get('lower')
            style_param['label'] = element.get('label')
            style_param['render'] = element.get('render')
            style_param['upper'] = element.get('upper')
            style_params[style_param['symbol']] = style_param

        symbols = element.findall('.//symbols/symbol')
        if symbols:
            while count != len(symbols):
                symbol = symbols[count]
                count += 1
                options = symbol.findall('.//Option')
                style_params[symbol.get('name')]['radius'] = ''
                dict_out_style['type_symbol'] = ''
                for option in options:
                    if option.get('name') == 'color':
                        style_params[symbol.get('name')]['color'] = option.get('value')
                    elif option.get('name') == 'outline_color':
                        style_params[symbol.get('name')]['outline_color'] = option.get('value')
                    elif option.get('name') == 'outline_width':
                        style_params[symbol.get('name')]['outline_width'] = str(round(float(option.get('value')) * 3.794, 2))
                    elif option.get('name') == 'size':
                        style_params[symbol.get('name')]['radius'] = 'radius: ' + str(float(option.get('value')) * 2) + ', '
                        style_params[symbol.get('name')]['diameter_text'] = str(float(option.get('value')) * 4)
                        if float(option.get('value')) > float(max_diameter):
                            max_diameter = str(float(option.get('value')) * 4)
                    elif option.get('name') == 'name' and option.get('value') == 'circle':
                        dict_out_style['type_symbol'] = option.get('value')

После формирования словаря стиля нам необходимо перевести его в JS для того что бы он мог быть интегрирован в объект карты на leaflet.

js_style_list = ['if (polygonValue == null) { return { fillOpacity: 0, opacity: 0};}']
    js_symbol_list = []
    for style in style_params:
        fillcolor = ', '.join(style_params[style]["color"].split(',')[:-1])
        outline_color = ', '.join(style_params[style]["outline_color"].split(',')[:-1])
        opacity = float(style_params[style]["outline_color"].split(',')[-1]) / 255 # можно выбрать устанавливать прозрачность в ручную или следовать за qml
        opacity = 0.8
        js_style = f'else if (polygonValue >= {style_params[style]["lower"]} && polygonValue <= {style_params[style]["upper"]}) {{return {{ fillColor: \'rgb({fillcolor})\', color: \'rgb({outline_color})\', weight: {style_params[style]["outline_width"]}, {style_params[style]["radius"]} fillOpacity: {opacity}, opacity: {opacity}}};}}'
        js_style_list.append(js_style)

js_style_list полученный список будет состоять из частей кода на JS для последующей интеграции в шаблон и соответствующего оформления на интерактивной карте. То есть по своей сути мы превращаем список в единую строку на JS с помощью простых команд.

    dict_out_style['style'] = ''.join(js_style_list)
    dict_out_style['field_style'] = field_style.lower()
    dict_out_style['symbol_style'] = ''.join(js_symbol_list)

Для формирования условных обозначений карты мы так-же можем воспользоваться этим же набором стилей. Для этого сформируем контейнеры HTML с помощью возможностей JS и Django.

if dict_out_style['type_symbol'] == '':
    js_symbol = f''' 
                var container_content{var_name} = L.DomUtil.create('div', 'container_content{var_name}');
                container_content{var_name}.style.display = 'flex';
                container_content{var_name}.style.alignItems = 'left';
                container_content{var_name}.style.justifyContent = 'left';
                container_content{var_name}.style.flexDirection = 'row';
                container.appendChild(container_content{var_name});
    
                var symbol{var_name}  = L.DomUtil.create(\'div\', \'symbol_{var_name}\');
                symbol{var_name}.style.border = \'{style_params[style]["outline_width"]}px solid rgb({outline_color})\';
                symbol{var_name}.style.borderWidth = '{style_params[style]["outline_width"]}px';
                symbol{var_name}.style.backgroundColor = \'rgb({fillcolor})\';
                symbol{var_name}.style.width = '50px';
                symbol{var_name}.style.height = '20px';
                symbol{var_name}.style.margin = '5px';
                container_content{var_name}.appendChild(symbol{var_name});
    '''
    js_text_symbol = f''' var label{var_name} = L.DomUtil.create('p', \'label_{var_name}\');
                label{var_name}.style.textAlign = 'left';
                label{var_name}.style.display = 'inline-block';
                label{var_name}.style.alignItems = 'left';
                label{var_name}.style.justifyContent = 'left';
                label{var_name}.style.margin = '5px';
                label{var_name}.style.height = '20px';
                label{var_name}.innerHTML = '     —     {style_params[style]["label"]}';
                container_content{var_name}.appendChild(label{var_name});
    '''

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

В результате всех мероприятий у нас формируется интерактивная карта следующего вида.

Карта населения РФ по Росстату
Карта населения РФ по Росстату

Я не буду подробно останавливаться на создании элементов управления там все банально. Следующим вопросом будет то как сделать автоматическое создание подобных карт? Его реализация заключается в наполнении определенной директории файлами формата QML с определенными названиями, которые будут содержать название карты и название таблицы в БД. В данном случае название будет звучать как Численность населения_2000(russia_map).qml, что в своб очередь позволяет вместить туда и название карты и год на который получена статистика и название слоя в БД. Данный подход вызван спецификой поставленной задачи и гибок в своем исполнении.

Данный подход хорошо подходит если все картматериалы подготавливаются в виде проектов QGIS с хранением информации в БД, позволяют быстро реализовать на основании проектов интерактивные карты с актуальной информации подтягиваемой из БД. Стоит также отметить что подписи условных обозначений также были получены из QML файла, который по своей структуре повторяет QGS.

Спасибо за прочтение данной статьи! Надеюсь такой подход упростит работу тех, кто создает интерактивные карты основываясь на ручном переносе стилистики в JS.

Теги:
Хабы:
Всего голосов 6: ↑6 и ↓0+6
Комментарии4

Публикации

Истории

Работа

React разработчик
48 вакансий
Data Scientist
71 вакансия
Python разработчик
103 вакансии

Ближайшие события

22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань