Обрабатываем карту в GeoJSON и выводим прогноз погоды
Построим карту температур, осадков и погодных явлений
В этом примере мы хотим показать возможности работы с картой России и погодными сервисами.
Приступим к примеру после установки нескольких библиотек и настройки окружения.
]add Shapefile, GeoJSON
gr()
Загрузка данных с помощью API OpenMeteo
Первым делом скачаем данные для каждой точки координатной сетки, заданной широтой и долготой.
Сбор данных о погоде может занять довольно много времени. Из открытого и бесплатного источника OpenMeteo сбор данных о рельефе и погоде занимает около 4 минут для разрешения карты в 4 градуса (с задержкой 0.1 с между запросами) и 7 минут при разрешении 2 градуса. Для учебного примера это приемлемо, но для более серьезной работы лучше пользоваться более производительным API коммерческих сервисов.
# Раскомментируйте чтобы скачать свежие данные
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, можем построить следующее изображение:
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))
Фильтрация областей суши
Поскольку мы также загрузили информацию о том, куда попадает каждая точка матрицы — на водную или не земную поверхность, то мы можем вывести такой график:
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))
Небольшая функция для загрузки границ карты из файла f позволит нам спланировать дальнейший ход работы:
# Функция для загрузки полигонов из 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 ];
Теперь поверх матрицы можно нанести карту:
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)
Оказалось довольно сложно соотнести два графика, к тому же мы видим, что многие точки на побережье не попали в дискретизацию. Но мы планируем построить такой график, когда очень точный учет нам не нужен.
Сравним две функции определения принадлежности точки матрицы к карте, приведенной в файле:
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)
)
Карта справа нас устроит гораздо больше, тем более, что изменяя порог при построении маски для карты справа мы можем управлять отрисовкой более тонких деталей и построить именно такую карту, которая лучше всего отразит визуальный замысел.
И вот теперь мы наконец можем построить отличную карту погодных явлений:
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, получив интересную визуализацию в результате.



