Geometric polygon analysis to assess territorial changes
This article discusses the implementation of an algorithm for analyzing temporal changes in geometric polygons. The code loads up-to-date geodata, calculates the area of territories for different dates, and visualizes the results, demonstrating Engee's capabilities for geospatial information processing tasks.
The following packages are used in this example:
- Downloads: For easy file upload by URL.
- JSON3: A high-performance package for parsing JSON data.
- CodecZlib: Provides work with compressed data, in particular, GZIP decompression.
- Dates: Working with dates for filtering and formatting.
- Plots: A powerful and flexible library for plotting and visualizing data, used for rendering maps.
- GeoJSON(although added, it is not directly used in the code): A package for working with geodata. In this script, parsing is performed via- JSON3, which is also a valid approach.
Pkg.activate("MyGeoProject", shared=true)
Pkg.add(["JSON3", "CodecZlib", "GeoJSON"])
using Downloads, JSON3, CodecZlib, Dates, GeoJSON
The code starts with defining constants:
- DATE_START— a fixed date for the start of the analysis.
- DATE_CURRENT— the current date calculated at the time of the script launch. This makes the analysis always relevant.
- DSM_URL— The URL indicating the data source.
- REGIONS_DATA- contains background information on the regions of Ukraine: name, area and coordinates for the caption on the map.
- UKRAINE_TOTAL_AREA— the total area of the country.
DATE_START = Date("2022-02-24")
DATE_CURRENT = Date(Dates.now())
DSM_URL = "https://raw.githubusercontent.com/cyterat/deepstate-map-data/main/deepstate-map-data.geojson.gz"
REGIONS_DATA = [
    ("Винницкая", 26513, [28.47, 49.23]),
    ("Волынская", 20144, [25.33, 50.75]),
    ("Днепропетровская", 31914, [35.02, 48.45]),
    ("Донецкая", 26517, [37.80, 48.00]),
    ("Житомирская", 29832, [28.65, 50.25]),
    ("Закарпатская", 12777, [22.29, 48.62]),
    ("Запорожская", 27183, [35.18, 47.84]),
    ("Ивано-Франковская", 13928, [24.71, 48.92]),
    ("Киевская", 28131, [30.52, 50.45]),
    ("Кировоградская", 24588, [32.26, 48.51]),
    ("Луганская", 26684, [39.30, 48.57]),
    ("Львовская", 21833, [24.03, 49.84]),
    ("Николаевская", 24598, [31.98, 46.97]),
    ("Одесская", 33310, [30.73, 46.48]),
    ("Полтавская", 28748, [34.55, 49.59]),
    ("Ровненская", 20047, [26.25, 50.62]),
    ("Сумская", 23834, [34.80, 50.91]),
    ("Тернопольская", 13823, [25.60, 49.55]),
    ("Харьковская", 31415, [36.25, 50.00]),
    ("Херсонская", 28461, [32.62, 46.63]),
    ("Хмельницкая", 20629, [27.00, 49.42]),
    ("Черкасская", 20900, [32.06, 49.44]),
    ("Черниговская", 31865, [31.30, 51.50]),
    ("Черновицкая", 8097, [25.94, 48.29]),
    ("Крым", 26081, [34.10, 45.35]),
    ("Севастополь", 864, [33.52, 44.61])
]
UKRAINE_TOTAL_AREA = 603628
Function get_gzip_data:
- Downloads.download(url)uploads the file by URL to a temporary file and returns the path to it.
- open(temp_file) do fileopens this temporary file.
- GzipDecompressorStream(file)creates a decompression stream that "wraps" the file. When reading from this stream, the data is automatically decompressed.
- read(stream, String)reads the entire decompressed stream into a string, which is returned by the function.
It is a solution for working with compressed data directly in memory, without having to manage temporary files manually.
function get_gzip_data(url::String)
    temp_file = Downloads.download(url)
    open(temp_file) do file
        stream = GzipDecompressorStream(file)
        return read(stream, String)
    end
end
Function get_polygons_for_date — the filtration core:
- JSON3.readparses a string with GeoJSON.
- The cycle forpasses through all objects in the arrayfeatures.
- The presence of a property is checked for each object. "date".
- The date is converted to an object. Dateand is compared withtarget_date.
- If the dates match, the geometry is extracted. Depending on its type (PolygonorMultiPolygon) coordinates are added to the resulting array. It is important thatMultiPolygonit is split, and its components are added separately.
function get_polygons_for_date(geojson_data::String, target_date::Date)
    data = JSON3.read(geojson_data)
    features = get(data, "features", [])
    result_polygons = []
    
    for feature in features
        properties = get(feature, "properties", nothing)
        if properties !== nothing && haskey(properties, "date")
            feature_date = Date(properties["date"])
            if feature_date == target_date
                geometry = get(feature, "geometry", nothing)
                if geometry !== nothing
                    geom_type = get(geometry, "type", "")
                    coordinates = get(geometry, "coordinates", [])
                    if geom_type == "Polygon"
                        push!(result_polygons, coordinates)
                    elseif geom_type == "MultiPolygon"
                        append!(result_polygons, coordinates)
                    end
                end
            end
        end
    end
    return result_polygons
end
Function calculate_polygon_area implements the Gauss lacing algorithm (or the formula for the areas of barycentric coordinates).
- The formula goes through all the vertices of the polygon and summarizes the vector products of the coordinates. The result is twice the area of the polygon.
- Adaptation for the sphere: This is a planar algorithm. To account for the sphericity of the Earth, the longitude coordinates (ring[i][1]) and latitude (ring[i][2]) multiplied by the approximate length of degrees in kilometers (85.0and111.0respectively). This is a rough but working approximation.
function calculate_polygon_area(coordinates)
    total_area = 0.0
    for polygon in coordinates
        for ring in polygon
            if length(ring) >= 3
                area = 0.0
                n = length(ring)
                for i in 1:n
                    j = (i % n) + 1
                    area += (ring[i][1] * 85.0) * (ring[j][2] * 111.0) - 
                           (ring[j][1] * 85.0) * (ring[i][2] * 111.0)
                end
                total_area += abs(area) / 2.0
            end
        end
    end
    return total_area
end
Function draw_ukraine_map it is used for drawing geometry.
- seriestype=:shape— a key parameter indicating that we are drawing a closed shape (polygon) at the specified coordinates.
- Usage plot!andscatter!with an exclamation mark, it means that we are adding new elements to the existing schedule.
- annotate!adds text captions to the specified coordinates.
- Parameter aspect_ratio=:equalensures that the scale on the X and Y axes is the same, and the map will not be distorted.
function draw_ukraine_map(start_polygons, current_polygons)
    plt = plot()
    for polygon in start_polygons
        for ring in polygon
            x_coords = [point[1] for point in ring]
            y_coords = [point[2] for point in ring]
            plot!(x_coords, y_coords, 
                  seriestype=:shape, 
                  color=:lightblue, 
                  alpha=0.4, 
                  linecolor=:blue,
                  linewidth=1,
                  label="")
        end
    end 
    for polygon in current_polygons
        for ring in polygon
            x_coords = [point[1] for point in ring]
            y_coords = [point[2] for point in ring]
            plot!(x_coords, y_coords, 
                  seriestype=:shape, 
                  color=:red, 
                  alpha=0.4, 
                  linecolor=:darkred,
                  linewidth=1,
                  label="")
        end
    end
    
    for (name, area, center) in REGIONS_DATA
        scatter!([center[1]], [center[2]], 
                 markersize=4, 
                 markercolor=:black, 
                 markerstrokewidth=0.5,
                 label="")
        
        annotate!(center[1], center[2], text(name, 6, :left))
    end
    
    plot!(plt,
        legend=:bottomright,
        aspect_ratio=:equal, 
        size=(1000, 800),
        ylims=(44, 52),
        title="Территориальные изменения с $(Dates.format(DATE_START, "dd.mm.yyyy")) по $(Dates.format(DATE_CURRENT, "dd.mm.yyyy"))",
        xlabel="Долгота",
        ylabel="Широта",
        grid=true
    )
    return plt
end
Now we will run the entire algorithm, in which the data is loaded, filtered, analyzed, and the results are displayed as text and images.
geo_data = get_gzip_data(DSM_URL)
start_polygons = get_polygons_for_date(geo_data, DATE_START)
current_polygons = get_polygons_for_date(geo_data, DATE_CURRENT)
start_polygon_area = calculate_polygon_area(start_polygons)
current_polygon_area = calculate_polygon_area(current_polygons)
println("Период: с $(Dates.format(DATE_START, "dd.mm.yyyy")) по $(Dates.format(DATE_CURRENT, "dd.mm.yyyy"))")
println()
println("  Общая площадь Украины: $UKRAINE_TOTAL_AREA км²")
println("  Начальная площадь: $(round(start_polygon_area, digits=0)) км² ($(round((start_polygon_area/UKRAINE_TOTAL_AREA)*100, digits=2))%)")
println("  Текущая площадь:   $(round(current_polygon_area, digits=0)) км² ($(round((current_polygon_area/UKRAINE_TOTAL_AREA)*100, digits=2))%)")
draw_ukraine_map(start_polygons, current_polygons)
Conclusion
The presented code is an excellent example of how an effective tool for analyzing complex geospatial data can be quickly created in a modern high-level language.
This code provides a solid foundation for implementing these and many other features, demonstrating Engee's flexibility and performance in the field of Data Science.