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

Основной файлик main.py выглядит так
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from mainwindow import Ui_MainWindow
import folium
import io
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.show_map()
def show_map(self):
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
attributionControl=0,
)
m_data = io.BytesIO()
m.save(m_data, close_file=False)
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
файлик интерфейса mainwindow.ui вот такой:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>697</width>
<height>541</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QWebEngineView" name="web">
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
файл для генерации интерфейса generate.py
pyside6-uic -o mainwindow.py mainwindow.ui
его надо вызвать чтобы сгенерился файлик mainwindow.py
generate.bat
ну и сам файлик с зависимостями requirements.txt
PySide6
numpy
pandas
geopandas
folium
зависимости ставим через
pip install -r requirements.txt
если в политехе, то надо добавить прокси
pip install -r requirements.txt --proxy 172.27.100.5:4444
Вот пример кода, где мы добавляем разные типы маркеров. Так как мы никакие библиотечки не правили и файл интерфейса не изменяли, то можно смело брать код отсюда и вставлять в код из предыдущего задания
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from mainwindow import Ui_MainWindow
import folium
import io
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.show_map()
def show_map(self):
# создание объекты карты
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager"
)
###
folium.Marker(
location=[52.286387, 104.280660],
popup="Это Иркутск",
icon=folium.Icon(
icon="cow",
prefix="fa",
color="lightred",
icon_color="black"
)
).add_to(m)
folium.CircleMarker(
location=[52.226387, 104.220660],
popup="Это уже не Иркутск",
fill=True,
radius=30,
color="red",
fill_color="blue",
fill_opacity=0.9,
).add_to(m)
folium.Marker(
location=[52.226387, 104.220660],
popup="Это СНТ",
icon=folium.DivIcon(
html='<span style="font-size: 24px; background: yellow;">Привет</span>'
)
).add_to(m)
bounds = [
(52.2599889, 104.2616272),
(52.2615387, 104.2702103),
(52.2636006, 104.2692447),
(52.2649138, 104.2670131),
(52.2641127, 104.2594814),
(52.2599626, 104.2616057),
]
folium.Polygon(
bounds,
color="red",
fill=True,
popup="ИРНИТУ"
).add_to(m)
###
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Тут пример кода который позволит вывести города из файла
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from mainwindow import Ui_MainWindow
import folium
import io
import pandas as pd
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.df = pd.read_csv("./data.csv")
self.show_map()
def show_map(self):
# создание объекты карты
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0
)
###
items = self.df.loc[self.df['population'] >= 100000]
for item in items.itertuples():
icon = folium.Icon()
if item.population >= 1000000:
icon = folium.Icon(
color="red",
icon="atom",
prefix='fa',
)
folium.Marker(
location=[item.latitude_dd, item.longitude_dd],
popup=item.settlement,
icon=icon
).add_to(m)
folium.Marker(
location=[item.latitude_dd, item.longitude_dd],
icon=folium.DivIcon(
icon_size=[80, 35],
html=f'<div style="background: yellow; text-align: center">{item.settlement}<br>{item.population} чел.<span/>'
)
).add_to(m)
###
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
чтобы код заработал надо скачать файлик https://disk.yandex.ru/d/SuqMm1SgRviE6w и положить в папку с проектом:

Обработка кликов требует доабвления дополнительных классов
Класс для инициализации шины данных
class BridgeFolium(folium.MacroElement):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var mainWindow;
// Ждём загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
// Создаём WebChannel
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
// Ждём загрузки карты Leaflet
setTimeout(function() {
// Добавляем обработчик клика
{{this._parent.get_name()}}.on('click', function(e) {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.receive_coordinates(lat, lng);
}
});
}, 1000);
});
{% endmacro %}
"""
)
и класс для создания кликабельного маркера
class ClickableMarker(folium.Marker):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.marker(
{{ this.location|tojson }},
{{ this.options|tojavascript }}
).addTo({{ this._parent.get_name() }}).on('click', (e) => {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.marker_clicked(lat, lng, {{ this.options|tojavascript }}.id);
}
});
{% endmacro %}
"""
)
итоговый файл после добавления этих двух классов будет выглядеть вот так:
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import Slot
from mainwindow import Ui_MainWindow
import folium
from folium import template
import io
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class BridgeFolium(folium.MacroElement):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var mainWindow;
// Ждём загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
// Создаём WebChannel
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
// Ждём загрузки карты Leaflet
setTimeout(function() {
// Добавляем обработчик клика
{{this._parent.get_name()}}.on('click', function(e) {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.receive_coordinates(lat, lng);
}
});
}, 1000);
});
{% endmacro %}
"""
)
class ClickableMarker(folium.Marker):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.marker(
{{ this.location|tojson }},
{{ this.options|tojavascript }}
).addTo({{ this._parent.get_name() }}).on('click', (e) => {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.marker_clicked(lat, lng, {{ this.options|tojavascript }}.id);
}
});
{% endmacro %}
"""
)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.show_map()
@Slot(float, float)
def receive_coordinates(self, lat, lng):
print(lat, lng)
@Slot(float, float, int)
def marker_clicked(self, lat, lng, _id):
print("Marker:", lat, lng, _id)
def show_map(self):
# создание объекты карты
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0
)
m.add_child(BridgeFolium())
# Добавляем JavaScript в HTML-код карты
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
###
ClickableMarker(
location=[52.286387, 104.280660],
id=123,
icon=folium.Icon(icon="cat", prefix='fa')
).add_to(m)
###
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Код для реализации такого интерфейса.
Файл mainwindow.py
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import Slot
from mainwindow import Ui_MainWindow
import folium
from folium import template
import io
import pandas as pd
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class BridgeFolium(folium.MacroElement):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var mainWindow;
// Ждём загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
// Создаём WebChannel
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
// Ждём загрузки карты Leaflet
setTimeout(function() {
// Добавляем обработчик клика
{{this._parent.get_name()}}.on('click', function(e) {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.receive_coordinates(lat, lng);
}
});
}, 1000);
});
{% endmacro %}
"""
)
class ClickableMarker(folium.Marker):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.marker(
{{ this.location|tojson }},
{{ this.options|tojavascript }}
).addTo({{ this._parent.get_name() }}).on('click', (e) => {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.marker_clicked(lat, lng, {{ this.options|tojavascript }}.id);
}
});
{% endmacro %}
"""
)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.df = pd.read_csv("./data.csv")
self.ui.cmbPopulation.addItem('50000-100000', (50000, 100000))
self.ui.cmbPopulation.addItem('100000-500000', (100001, 500000))
self.ui.cmbPopulation.addItem('500000-1млн.', (500001, 1000000))
self.ui.cmbPopulation.addItem('от 1млн.', (1000001, 100000000))
self.ui.cmbPopulation.currentIndexChanged.connect(self.show_map)
self.show_map()
@Slot(float, float)
def receive_coordinates(self, lat, lng):
print(lat, lng)
@Slot(float, float, int)
def marker_clicked(self, lat, lng, _id):
city = self.df[self.df.id == _id].iloc[0]
self.ui.edtCity.setText(city.settlement)
self.ui.edtInfo.setText(f"""
Население: {city.population}<br>
Муниципалитет: {city.municipality}<br>
Регион: {city.region}
""".strip())
def show_map(self):
# создание объекты карты
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0
)
m.add_child(BridgeFolium())
# Добавляем JavaScript в HTML-код карты
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
###
left, right = self.ui.cmbPopulation.currentData()
cities = self.df.loc[(left <= self.df.population) & (self.df.population <=right)]
sw = cities[['latitude_dd', 'longitude_dd']].min().values.tolist()
ne = cities[['latitude_dd', 'longitude_dd']].max().values.tolist()
for city in cities.itertuples():
ClickableMarker(
location=[city.latitude_dd, city.longitude_dd],
id=city.id,
icon=folium.Icon(icon="cat", prefix='fa')
).add_to(m)
m.fit_bounds([sw, ne])
###
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
код файла интерфейса mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>827</width>
<height>541</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="cmbPopulation"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QWebEngineView" name="web">
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Количество людей</string>
</property>
</widget>
</item>
<item row="0" column="2" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Информация</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLineEdit" name="edtCity"/>
</item>
<item row="1" column="0">
<widget class="QTextEdit" name="edtInfo"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>QWebEngineView</class>
<extends>QWidget</extends>
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
Чтобы реализовать механизм выделения, нам достаточно править только файл main.py.
Итоговый файл в конце этого текста, а тут уточнения что имено мы делаем, по кусочкам.
class PolygonSelector(folium.MacroElement):
_template = template.Template("""
{% macro script(this, kwargs) %}
let lastSelectedPolygon = null;
{{this._parent.get_name()}}.on('draw:created', (e) => {
let coordinates = e.layer.getLatLngs()[0].map(p => [p.lat, p.lng]);
lastSelectedPolygon = e.layer;
{{ this._parent.get_name()}}.addLayer(lastSelectedPolygon);
if(mainWindow) {
mainWindow.select_bounds(JSON.stringify(coordinates));
}
})
{{this._parent.get_name()}}.on('draw:drawstart', (e) => {
if (lastSelectedPolygon) {
lastSelectedPolygon.remove();
lastSelectedPolygon = null;
}
})
{% endmacro %}
""")
внутри метода __init__ сразу после считывания датасета добавляем преобразование
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.df = pd.read_csv("./data.csv")
# ПРЕОБРАЗУЕМ ДАТАСЕТ в GeoDataFrame
self.df = gpd.GeoDataFrame(
self.df,
geometry=gpd.points_from_xy(self.df.latitude_dd, self.df.longitude_dd),
crs='EPSG:4326'
)
# КОНЕЦ ПРЕОБРАЗОВАНИЯ
# ...
# ОБРАБОТЧИК СОБЫТИЯ ВЫБОРА ОБЛАСТИ
# мы тут по сути просто список городов выводим на форму
@Slot(str)
def select_bounds(self, coordinates_str):
coordinates = json.loads(coordinates_str)
self.polygon = Polygon(coordinates)
cities = self.cities[self.cities.geometry.within(self.polygon)]
print(cities.shape[0])
cities_list = []
for c in cities.itertuples():
cities_list.append(f"{c.settlement}: {c.population}")
self.ui.edtInfo.setText(f"""
Население: {cities.population.sum()}<p>
Города: {"<br>".join(cities_list)}
""")
# КОНЕЦ ОБРАБОТЧИКА
def show_map(self):
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0,
drawControl=True
)
m.add_child(BridgeFolium())
# АУТИВИРУЕМ НАШ ПЛАГИН
m.add_child(PolygonSelector())
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
# ПОДКЛЮЧЕНИЕ js и css БИБЛИОТЕК
m.add_js_link('leaflet.draw.js', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js')
m.add_css_link('leaflet.draw.css', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw-src.css')
# КОНЕЦ ПОДКЛЮЧЕНИЯ
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
left, right = self.ui.cmbPopulation.currentData()
# А ТУТ cities на self.cities заменяем
self.cities = self.df.loc[(left <= self.df.population) & (self.df.population <=right)]
sw = self.cities[['latitude_dd', 'longitude_dd']].min().values.tolist()
ne = self.cities[['latitude_dd', 'longitude_dd']].max().values.tolist()
for city in self.cities.itertuples():
# ...
# дальше ничего не трогаем ...
Вот итоговый код main.py, который нужен чтобы заработала функция выделения:
import json
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import Slot
from shapely import Point, Polygon
from mainwindow import Ui_MainWindow
import folium
from folium import template
import io
import json
import pandas as pd
import geopandas as gpd
from shapely import Polygon
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class BridgeFolium(folium.MacroElement):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var mainWindow;
// Ждём загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
// Создаём WebChannel
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
// Ждём загрузки карты Leaflet
setTimeout(function() {
// Добавляем обработчик клика
{{this._parent.get_name()}}.on('click', function(e) {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.receive_coordinates(lat, lng);
}
});
}, 1000);
});
{% endmacro %}
"""
)
class ClickableMarker(folium.Marker):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.marker(
{{ this.location|tojson }},
{{ this.options|tojavascript }}
).addTo({{ this._parent.get_name() }}).on('click', (e) => {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.marker_clicked(lat, lng, {{ this.options|tojavascript }}.id);
}
});
{% endmacro %}
"""
)
# СОЗДАЕМ ПЛАГИН ДЛЯ ДОБАВЛЕНИЯ ВОЗМОЖНОСТИ ВЫБРАТЬ ОБЛАСТЬ
class PolygonSelector(folium.MacroElement):
_template = template.Template("""
{% macro script(this, kwargs) %}
let lastSelectedPolygon = null;
{{this._parent.get_name()}}.on('draw:created', (e) => {
let coordinates = e.layer.getLatLngs()[0].map(p => [p.lat, p.lng]);
lastSelectedPolygon = e.layer;
{{ this._parent.get_name()}}.addLayer(lastSelectedPolygon);
if(mainWindow) {
mainWindow.select_bounds(JSON.stringify(coordinates));
}
})
{{this._parent.get_name()}}.on('draw:drawstart', (e) => {
if (lastSelectedPolygon) {
lastSelectedPolygon.remove();
lastSelectedPolygon = null;
}
})
{% endmacro %}
""")
# КОНЕЦ ПЛАГИНА
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.polygon = None
self.df = pd.read_csv("./data.csv")
# ПРЕОБРАЗУЕМ ДАТАСЕТ в GeoDataFrame
self.df = gpd.GeoDataFrame(
self.df,
geometry=gpd.points_from_xy(self.df.latitude_dd, self.df.longitude_dd),
crs='EPSG:4326'
)
# КОНЕЦ ПРЕОБРАЗОВАНИЯ
self.ui.cmbPopulation.addItem('50000-100000', (50001, 100000))
self.ui.cmbPopulation.addItem('100000-500000', (100001, 500000))
self.ui.cmbPopulation.addItem('500000-1млн.', (500001, 1000000))
self.ui.cmbPopulation.addItem('от 1млн.', (1000001, 100000000))
self.ui.cmbPopulation.currentIndexChanged.connect(self.show_map)
self.show_map()
@Slot(float, float)
def receive_coordinates(self, lat, lng):
print(lat, lng)
# ОБРАБОТЧИК СОБЫТИЯ ВЫБОРА ОБЛАСТИ
# мы тут по сути просто список городов выводим на форму
@Slot(str)
def select_bounds(self, coordinates_str):
coordinates = json.loads(coordinates_str)
self.polygon = Polygon(coordinates)
cities = self.cities[self.cities.geometry.within(self.polygon)]
print(cities.shape[0])
cities_list = []
for c in cities.itertuples():
cities_list.append(f"{c.settlement}: {c.population}")
self.ui.edtInfo.setText(f"""
Население: {cities.population.sum()}<p>
Города: {"<br>".join(cities_list)}
""")
# КОНЕЦ ОБРАБОТЧИКА
@Slot(float, float, int)
def marker_clicked(self, lat, lng, _id):
city = self.df[self.df.id == _id].iloc[0]
self.ui.edtCity.setText(city.settlement)
self.ui.edtInfo.setText(f"""
Население: {city.population}<br>
Муниципалитет: {city.municipality}<br>
Регион: {city.region}
""".strip())
def show_map(self):
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0,
drawControl=True
)
m.add_child(BridgeFolium())
# АУТИВИРУЕМ НАШ ПЛАГИН
m.add_child(PolygonSelector())
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
# ПОДКЛЮЧЕНИЕ js и css БИБЛИОТЕК
m.add_js_link('leaflet.draw.js', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js')
m.add_css_link('leaflet.draw.css', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw-src.css')
# КОНЕЦ ПОДКЛЮЧЕНИЯ
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
left, right = self.ui.cmbPopulation.currentData()
# А ТУТ cities на self.cities заменяем
self.cities = self.df.loc[(left <= self.df.population) & (self.df.population <=right)]
sw = self.cities[['latitude_dd', 'longitude_dd']].min().values.tolist()
ne = self.cities[['latitude_dd', 'longitude_dd']].max().values.tolist()
for city in self.cities.itertuples():
color = 'blue'
ClickableMarker(
location=[city.latitude_dd, city.longitude_dd],
id=city.id,
icon=folium.Icon(icon="cat", prefix='fa', color=color)
).add_to(m)
m.fit_bounds([sw, ne])
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
with open('result.html', 'w') as f:
f.write(m_data.getvalue().decode())
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Чтобы зум не сбрасывался снова работаем с файлом main.py
Добавляем плагин сохранения границ
class FitBoundsPlugin(folium.MacroElement):
_template = template.Template("""
{% macro script(this, kwargs) %}
{{this._parent.get_name()}}.on('moveend', e => {
if (mainWindow) {
mainWindow.bounds_changed(JSON.stringify({{this._parent.get_name()}}.getBounds()))
}
})
{% endmacro %}
""")
добавляем в __init__ поля
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.polygon = None
self.df = pd.read_csv("./data.csv")
# ДОБАВИЛ
self.sw = None
self.ne = None
# ...
добавляем метод реакцию на изменения границ карты
class MainWindow(QMainWindow):
# ...
@Slot(str)
def bounds_changed(self, bounds_str):
print(bounds_str)
bounds = json.loads(bounds_str)
self.sw = [bounds['_southWest']['lat'], bounds['_southWest']['lng']]
self.ne = [bounds['_northEast']['lat'], bounds['_northEast']['lng']]
ну и подкручиваем show_map:
def show_map(self):
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0,
)
m.add_child(BridgeFolium())
m.add_child(PolygonSelector())
# АКТИВИРОВАЛИ ПЛАГИН-СЕЛЕКТОР
m.add_child(FitBoundsPlugin())
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
m.add_js_link('leaflet.draw.js', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js')
m.add_css_link('leaflet.draw.css', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw-src.css')
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
left, right = self.ui.cmbPopulation.currentData()
self.cities = self.df.loc[(left <= self.df.population) & (self.df.population <=right)]
# ВЫТАСКИВАЕМ ГРАНИЦУ ЮГО-ЗАПАДА
if not self.sw:
sw = self.cities[['latitude_dd', 'longitude_dd']].min().values.tolist()
else:
sw = self.sw
# ВЫТАСКИВАЕМ ГРАНИЦУ СЕВЕРО-ВОСТОК
if not self.ne:
ne = self.cities[['latitude_dd', 'longitude_dd']].max().values.tolist()
else:
ne = self.ne
for city in self.cities.itertuples():
# ...
# устанавливаем собственно границу
m.fit_bounds([sw, ne])
### дальше ничего не трогаем
итоговый файл получается такой:
import json
import sys
import os
import PySide6
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtWebChannel import QWebChannel
from PySide6.QtCore import Slot
from shapely import Point, Polygon
from mainwindow import Ui_MainWindow
import folium
from folium import template
import io
import json
import pandas as pd
import geopandas as gpd
from shapely import Polygon
dirname = os.path.dirname(PySide6.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
class BridgeFolium(folium.MacroElement):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var mainWindow;
// Ждём загрузки страницы
document.addEventListener('DOMContentLoaded', function() {
// Создаём WebChannel
new QWebChannel(qt.webChannelTransport, function(channel) {
mainWindow = channel.objects.mainWindow;
});
// Ждём загрузки карты Leaflet
setTimeout(function() {
// Добавляем обработчик клика
{{this._parent.get_name()}}.on('click', function(e) {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.receive_coordinates(lat, lng);
}
});
}, 1000);
});
{% endmacro %}
"""
)
class ClickableMarker(folium.Marker):
_template = template.Template(
"""
{% macro script(this, kwargs) %}
var {{ this.get_name() }} = L.marker(
{{ this.location|tojson }},
{{ this.options|tojavascript }}
).addTo({{ this._parent.get_name() }}).on('click', (e) => {
var lat = e.latlng.lat;
var lng = e.latlng.lng;
if (mainWindow) {
mainWindow.marker_clicked(lat, lng, {{ this.options|tojavascript }}.id);
}
});
{% endmacro %}
"""
)
class PolygonSelector(folium.MacroElement):
_template = template.Template("""
{% macro script(this, kwargs) %}
var drawControl = new L.Control.Draw({
draw: {
polygon: true,
polyline: false,
rectangle: false,
circle: false,
marker: false,
circlemarker: false,
}
})
{{this._parent.get_name()}}.addControl(drawControl);
let lastSelectedPolygon = null;
{{this._parent.get_name()}}.on('draw:created', (e) => {
let coordinates = e.layer.getLatLngs()[0].map(p => [p.lat, p.lng]);
lastSelectedPolygon = e.layer;
{{ this._parent.get_name()}}.addLayer(lastSelectedPolygon);
if(mainWindow) {
mainWindow.select_bounds(JSON.stringify(coordinates));
}
})
{{this._parent.get_name()}}.on('draw:drawstart', (e) => {
if (lastSelectedPolygon) {
lastSelectedPolygon.remove();
lastSelectedPolygon = null;
}
})
{% endmacro %}
""")
class FitBoundsPlugin(folium.MacroElement):
_template = template.Template("""
{% macro script(this, kwargs) %}
{{this._parent.get_name()}}.on('moveend', e => {
if (mainWindow) {
mainWindow.bounds_changed(JSON.stringify({{this._parent.get_name()}}.getBounds()))
}
})
{% endmacro %}
""")
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.polygon = None
self.df = pd.read_csv("./data.csv")
self.sw = None
self.ne = None
self.df = gpd.GeoDataFrame(
self.df,
geometry=gpd.points_from_xy(self.df.latitude_dd, self.df.longitude_dd),
crs='EPSG:4326'
)
self.ui.cmbPopulation.addItem('50000-100000', (50001, 100000))
self.ui.cmbPopulation.addItem('100000-500000', (100001, 500000))
self.ui.cmbPopulation.addItem('500000-1млн.', (500001, 1000000))
self.ui.cmbPopulation.addItem('от 1млн.', (1000001, 100000000))
self.ui.cmbPopulation.currentIndexChanged.connect(self.show_map)
self.show_map()
@Slot(float, float)
def receive_coordinates(self, lat, lng):
print(lat, lng)
@Slot(str)
def bounds_changed(self, bounds_str):
print(bounds_str)
bounds = json.loads(bounds_str)
self.sw = [bounds['_southWest']['lat'], bounds['_southWest']['lng']]
self.ne = [bounds['_northEast']['lat'], bounds['_northEast']['lng']]
@Slot(str)
def select_bounds(self, coordinates_str):
coordinates = json.loads(coordinates_str)
self.polygon = Polygon(coordinates)
cities = self.cities[self.cities.geometry.within(self.polygon)]
print(cities.shape[0])
cities_list = []
for c in cities.itertuples():
cities_list.append(f"{c.settlement}: {c.population}")
self.ui.edtInfo.setText(f"""
Население: {cities.population.sum()}<p>
Города: {"<br>".join(cities_list)}
""")
@Slot(float, float, int)
def marker_clicked(self, lat, lng, _id):
city = self.df[self.df.id == _id].iloc[0]
self.ui.edtCity.setText(city.settlement)
self.ui.edtInfo.setText(f"""
Население: {city.population}<br>
Муниципалитет: {city.municipality}<br>
Регион: {city.region}
""".strip())
def show_map(self):
# создание объекты карты
m = folium.Map(
location=[52.286387, 104.280660],
zoom_start=11,
tiles="CartoDB Voyager",
attributionControl=0,
)
m.add_child(BridgeFolium())
m.add_child(PolygonSelector())
m.add_child(FitBoundsPlugin())
# Добавляем JavaScript в HTML-код карты
m.get_root().html.add_child(folium.Element("""<script src="qrc:///qtwebchannel/qwebchannel.js"></script>"""))
m.add_js_link('leaflet.draw.js', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js')
m.add_css_link('leaflet.draw.css', 'https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw-src.css')
self.channel = QWebChannel()
self.channel.registerObject("mainWindow", self)
self.ui.web.page().setWebChannel(self.channel)
###
left, right = self.ui.cmbPopulation.currentData()
self.cities = self.df.loc[(left <= self.df.population) & (self.df.population <=right)]
if not self.sw:
sw = self.cities[['latitude_dd', 'longitude_dd']].min().values.tolist()
else:
sw = self.sw
if not self.ne:
ne = self.cities[['latitude_dd', 'longitude_dd']].max().values.tolist()
else:
ne = self.ne
for city in self.cities.itertuples():
color = 'blue'
ClickableMarker(
location=[city.latitude_dd, city.longitude_dd],
id=city.id,
icon=folium.Icon(icon="cat", prefix='fa', color=color)
).add_to(m)
m.fit_bounds([sw, ne])
###
# подключение карты к интерфейсу
m_data = io.BytesIO()
m.save(m_data, close_file=False)
with open('result.html', 'w') as f:
f.write(m_data.getvalue().decode())
self.ui.web.setHtml(m_data.getvalue().decode())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())