Сообщество Engee

Обрабатываем карту в GeoJSON и выводим прогноз погоды

Автор
avatar-nkapyrinnkapyrin
Notebook

Построим карту температур, осадков и погодных явлений

В этом примере мы хотим показать возможности работы с картой России и погодными сервисами.

Приступим к примеру после установки нескольких библиотек и настройки окружения.

In [ ]:
]add Shapefile, GeoJSON
In [ ]:
gr()

Загрузка данных с помощью API OpenMeteo

Первым делом скачаем данные для каждой точки координатной сетки, заданной широтой и долготой.

Сбор данных о погоде может занять довольно много времени. Из открытого и бесплатного источника OpenMeteo сбор данных о рельефе и погоде занимает около 4 минут для разрешения карты в 4 градуса (с задержкой 0.1 с между запросами) и 7 минут при разрешении 2 градуса. Для учебного примера это приемлемо, но для более серьезной работы лучше пользоваться более производительным API коммерческих сервисов.

In [ ]:
# Раскомментируйте чтобы скачать свежие данные
using Dates
target_date = Dates.Date(2026, 03, 08);
target_precision = 4;
target_hour = 12;

# include("get_meteo_data.jl")
# df_final = get_meteo_data( target_date, target_hour, target_precision );
# first( df_final, 5 )

Если открыть загруженные данные и построить простую "тепловую карту" при помощи heatmap, можем построить следующее изображение:

In [ ]:
using CSV, DataFrames

df = CSV.read("погода_россия_полная.csv", DataFrame, types=[Int64, Int64, Float32, String, Float32, Float32, Float32, Float32], missingstring=["NA", "N/A", ""])
df = filter(r->!ismissing(r.температура), df)
m = Matrix(permutedims(unstack(df, :долгота, :широта, :температура)[!, 2:end]))
heatmap(sort(unique(df.долгота)), sort(unique(df.широта)), m, size=(1200,400))
Out[0]:
No description has been provided for this image

Фильтрация областей суши

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

In [ ]:
m_temp = Matrix(permutedims(unstack(df, :долгота, :широта, :температура)[!, 2:end]))
m_land = Matrix(permutedims(unstack(df, :долгота, :широта, :тип_поверхности)[!, 2:end]))
m_filtered = ifelse.(m_land .== "суша", m_temp, NaN)
heatmap(sort(unique(df.долгота)), sort(unique(df.широта)), m_filtered, size=(1200,400))
Out[0]:
No description has been provided for this image

Небольшая функция для загрузки границ карты из файла f позволит нам спланировать дальнейший ход работы:

In [ ]:
# Функция для загрузки полигонов из GeoJSON файла
load_borders(f) = [ [(p[1],p[2]) for p in poly] for f in JSON.parsefile(f)["features"] for geom in [f["geometry"]] for g in (geom["type"]=="MultiPolygon" ? geom["coordinates"] : [geom["coordinates"]]) for poly in g ];

Теперь поверх матрицы можно нанести карту:

In [ ]:
borders = load_borders("Россия_0.01.geojson")

lons = sort(unique(df.долгота)); lats = sort(unique(df.широта));
# Создаем heatmap
p = heatmap(lons, lats, m_filtered[end:-1:1, :],
             yflip=true, size=(1200,400), xlims=(18,190), ylims=(35,85), xlabel="Долгота", ylabel="Широта",
             color=:thermal, clims=(-40,30), colorbar_title="Температура (°C)")
# Добавляем границы
for poly in borders
    xs = [point[1] for point in poly]; ys = [point[2] for point in poly]
    push!(xs, xs[1]); push!(ys, ys[1]) # Замыкаем полигон
    ys_flipped = [minimum(lats) + maximum(lats) - y .+ 9 for y in ys] # Учитываем yflip
    plot!(p, xs .- 3, ys_flipped, linecolor=:red, linewidth=1.5, alpha=0.7, label="")
end

display(p)
No description has been provided for this image

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

Сравним две функции определения принадлежности точки матрицы к карте, приведенной в файле:

In [ ]:
include("geo_poly_functions.jl")

lats = 36:target_precision:87
lons = 14:target_precision:200

crude_matrix = [point_in_russia(lon, lat, borders) for lat in lats, lon in lons]
smooth_matrix = [land_weight(lon, lat, borders, 1.0) for lat in lats, lon in lons]

plot(
    heatmap(lons, lats, crude_matrix[end:-1:1, :], yflip = true, aspect_ratio=1.5),
    heatmap(lons, lats, smooth_matrix[end:-1:1, :] .> 0.001, yflip = true, aspect_ratio=1.5),
    size=(1200,250)
)
Out[0]:
No description has been provided for this image

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

И вот теперь мы наконец можем построить отличную карту погодных явлений:

In [ ]:
using CSV, DataFrames

# Функция для преобразования кода погоды WMO в эмодзи
function weathercode_to_emoji(code)
    if code == 0                 return "☀️"  # Ясно
    elseif code in [1, 2, 3]     return "☁️"  # Облачно
    elseif code in [45, 48]      return "🌫️"  # Туман
    elseif code in [51, 53, 55]  return "🌧️"  # Морось
    elseif code in [56, 57]      return "🌨️"  # Ледяная морось
    elseif code in [61, 63, 65]  return "💧"  # Дождь
    elseif code in [66, 67]      return "🌨️"  # Ледяной дождь
    elseif code in [71, 73, 75]  return "❄️"  # Снег
    elseif code == 77            return "🌨️"  # Снежная крупа
    elseif code in [80, 81, 82]  return "💧"  # Ливень
    elseif code in [85, 86]      return "☃️"  # Снегопад
    elseif code == 95            return "⛈️"  # Гроза
    elseif code in [96, 99]
        return "⛈️"  # Гроза с градом
    else
        return "❓"
    end
end

# Загружаем данные
df = CSV.read("погода_россия_полная.csv", DataFrame, types=[Int64, Int64, Float32, String, Float32, Float32, Float32, Float32], missingstring=["NA", "N/A", ""])
df1 = filter(r->!ismissing(r.код_погоды), df1)
df1 = filter([:широта, :долгота] => (lat,lon) -> land_weight(lon, lat, borders, 1.0) .> 0.001, df1)
lats, lons = sort(unique(df1.широта)), sort(unique(df1.долгота))
emoji_matrix = fill("🔵", length(lats), length(lons))
emoji_dict = Dict((r.широта, r.долгота) => weathercode_to_emoji(round(Int32, r.код_погоды)) for r in eachrow(df1))
emoji_matrix = [get(emoji_dict, (lat, lon), "🔵") for lat in lats, lon in lons]

# Выводим матрицу построчно
for i in length(lats):-1:1
    println( join(emoji_matrix[i, :]) )
    println( join(emoji_matrix[i, :]) ) # Повторим каждую строчку дважды
end
println("Легенда: ☀️ ясно | ☁️ облачно | 🌧️ дождь | ❄️ снег | ☃️ Снегопад | ⛈️ гроза | 🌫️ туман | 🌊 вода | ❓ нет данных")
🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵☁️☁️☁️☁️☁️☁️❄️❄️❄️☁️🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵☁️☁️☁️☁️☁️☁️❄️❄️❄️☁️🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵☁️☁️☁️☀️☀️☀️☀️☀️☀️☁️☀️☀️☀️☁️❄️☁️☁️☀️☀️☀️☀️☁️☀️☁️☁️🔵☀️🔵
🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️🔵☁️☁️☁️☀️☀️☀️☀️☀️☀️☁️☀️☀️☀️☁️❄️☁️☁️☀️☀️☀️☀️☁️☀️☁️☁️🔵☀️🔵
🔵❄️❄️☁️☁️☁️☁️☁️☀️☁️☀️☁️☁️☁️❄️❄️☁️☁️☁️☁️☁️☁️☁️☀️☀️❄️☁️☁️☁️☀️☀️☀️☁️☁️☁️☁️☀️☀️☀️
🔵❄️❄️☁️☁️☁️☁️☁️☀️☁️☀️☁️☁️☁️❄️❄️☁️☁️☁️☁️☁️☁️☁️☀️☀️❄️☁️☁️☁️☀️☀️☀️☁️☁️☁️☁️☀️☀️☀️
🔵☁️❄️☁️☀️☀️☀️☀️☁️☁️❄️☁️☁️☁️☁️☁️☁️☁️❄️❄️☁️☁️☁️☁️☀️☀️☁️☁️☁️☀️☀️☀️☀️☀️☁️☀️☁️☀️🔵
🔵☁️❄️☁️☀️☀️☀️☀️☁️☁️❄️☁️☁️☁️☁️☁️☁️☁️❄️❄️☁️☁️☁️☁️☀️☀️☁️☁️☁️☀️☀️☀️☀️☀️☁️☀️☁️☀️🔵
🔵☁️☁️❄️☁️☁️☁️☁️❄️☁️❄️❄️☁️☁️☁️☁️☁️❄️❄️❄️☁️☁️☁️☁️☀️☁️☁️☁️☁️🔵🔵🔵🔵☁️☁️🔵🔵🔵🔵
🔵☁️☁️❄️☁️☁️☁️☁️❄️☁️❄️❄️☁️☁️☁️☁️☁️❄️❄️❄️☁️☁️☁️☁️☀️☁️☁️☁️☁️🔵🔵🔵🔵☁️☁️🔵🔵🔵🔵
☀️🔵☁️☁️❄️☁️☁️❄️❄️☁️🔵🔵❄️❄️❄️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵
☀️🔵☁️☁️❄️☁️☁️❄️❄️☁️🔵🔵❄️❄️❄️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️☁️🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵
🔵🔵🔵☁️☁️☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️☁️🔵☁️🔵☀️☀️☀️☀️🔵🔵☁️☁️☁️❄️🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵☁️☁️☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵☁️☁️🔵☁️🔵☀️☀️☀️☀️🔵🔵☁️☁️☁️❄️🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵☁️☁️☁️☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵☀️☀️❄️🔵☁️🔵🔵🔵🔵🔵🔵🔵
🔵🔵☁️☁️☁️☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵☀️☀️❄️🔵☁️🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵
🔵🔵🔵🔵🔵☁️🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵🔵
Легенда: ☀️ ясно | ☁️ облачно | 🌧️ дождь | ❄️ снег | ☃️ Снегопад | ⛈️ гроза | 🌫️ туман | 🌊 вода | ❓ нет данных

Заключение

Мы с вами выполнили небольшое упражнение по картографии, загрузили файл в формате GeoJSON и поработали со скалярными величинами из таблиц CSV, получив интересную визуализацию в результате.