geoformat 20250922
Geoformat is a 99 % python geospatial blacksmith
License: MIT

# Welcome to Geoformat

## Introduction

Geoformat is a geospatial library.
The library aims to simplify loading data, manipulations, and storage of geospatial data.
Currently, this library is in Alpha mode. This means that at the moment, the structure of this library is not fully object-oriented compatible.

* [Installation](#installation)
* [Geoformat Cookbook](#geoformat-cookbook)
* [Geoformat Technical Description [WIP]](#geoformat-technical-description)
* [Table of Contents](#table-of-contents)

------------------------------------------------------------------------

## Installation

``` sh
$ pip install geoformat
```

------------------------------------------------------------------------

## Geoformat Cookbook


* [Create geoformat objects](#create-geoformat-objects)
  * [Create a feature](#create-a-feature)
  * [Create a geolayer](#create-a-geolayer)
  * [Print data geolayer](#print-data-geolayer)
  * [Draw a Geometry](#draw-a-geometry)
* [Geometry usefully functions](#geometry-usefully-functions)
  * [Area, length, distance, bbox and centroid](#area-length-distance-bbox-and-centroid)
  * [Reproject](#reproject-)
  * [Geometry formatting](#geometry-formatting)
  * [Geometrics manipulations](#geometrics-manipulations)
  * [Low level geometrics objects](#low-level-geometrics-objects)
  * [Geometry Index and Matrix](#geometry-index-and-matrix)
* [Attributes usefully functions](#attributes-usefully-functions)
  * [Attributes index](#attributes-index)
  * [Join geolayer attributes](#join-geolayer-attributes)
  * [Clause functions](#clause-functions)
  * [Field statistics](#field-statistics)
* [Feature usefully functions](#feature-usefully-functions)
* [Geolayer usefully functions](#geolayer-usefully-functions)
* [Write geolayer in GIS file](#write-geolayer-in-gis-file)
  * [Geoforamt GIS drivers](#geoforamt-gis-drivers)
  * [Make a GIS format transormation (shapefile => geojson)](#make-a-gis-format-transormation-shapefile--geojson)

------------------------------------------------------------------------

### Create geoformat objects

When you use Geoformat, there are two basic objects:

* **feature**: the content. This object can store *attributes* and/or *geometry* data.
* **geolayer**: the container. It stores features. It is equivalent to a table in a database.


#### Create a feature

A feature contains attributes and geometry information. (For more technical information, see: [Geoformat Technical Description [WIP]](#geoformat-technical-description))

```python
# create a feature dict 
feature = {}

# add attributes data
feature['attributes'] = {'integer_field': 1704, 'string_field': 'hello world', 'real_list_field': [123.43, 65., 356.65]}

# add geometry data
feature['geometry'] = {'type': 'Point', 'coordinates': [2.34886039, 48.85332408]}

print(feature)

# >>> {'attributes': {'integer_field': 1704, 'string_field': 'hello world', 'real_list_field': [123.43, 65.0, 356.65]}, 'geometry': {'type': 'Point', 'coordinates': [2.34886039, 48.85332408]}}
```

#### Create a geolayer

A geolayer is an equivalent to a file or a table in database containing one or several features with attibutes and/or
geometry. (For more technical information, see: [Geoformat Technical Description [WIP]](#geoformat-technical-description))

##### handmade

```python
# first create geolayer dict with 'metadata' keys and features 'keys'
geolayer = {"metadata": {"name": "handmade geolayer"}, "features": {}}

# create fields metadata
metadata_fields = {
    "id": {"type": "Integer", "index": 0}, 
    "foo": {"type": "String", "width": 50, "index": 1}
}

# create geometry metadata
metadata_geometry = {"type": {"Point", "LineString"}, "crs": 4326}

# rattach fields metadata and geometry metadata to geolayer metadata
geolayer["metadata"]["fields"] = metadata_fields
geolayer["metadata"]["geometry_ref"] = metadata_geometry

# create features
feature_a = {"attributes": {"id": 8754, "foo": "bar"}, "geometry": {"type": "Point", "coordinates": [2.3488, 48.8533]}}
feature_b = {"attributes": {"id": 764, "foo": "baz"}, "geometry": {"type": "LineString", "coordinates": [[-2.1368, 47.2829], [-1.5655, 47.1971],  [3.9097, 44.8938]]}}

# rattach features to geolayer
geolayer['features'][0] = feature_a
geolayer['features'][1] = feature_b

print(geolayer)

# >>> {'metadata': {'name': 'handmade geolayer', 'fields': {'id': {'type': 'Integer', 'index': 0}, 'foo': {'type': 'String', 'width': 50, 'index': 1}}, 'geometry_ref': {'type': {'LineString', 'Point'}, 'crs': 4326}}, 'features': {0: {'attributes': {'id': 8754, 'foo': 'bar'}, 'geometry': {'type': 'Point', 'coordinates': [2.3488, 48.8533]}}, 1: {'attributes': {'id': 764, 'foo': 'baz'}, 'geometry': {'type': 'LineString', 'coordinates': [[-2.1368, 47.2829], [-1.5655, 47.1971], [3.9097, 44.8938]]}}}}
```

##### Using a driver

```python
import geoformat

departement_path = 'data/FRANCE_IGN/DEPARTEMENT_2016_L93.shp'

geolayer = geoformat.shapefile_to_geolayer(departement_path)

print(len(geolayer['features']))

# >>> 96
```

#### Print data geolayer

Sometimes it can be useful to print in terminal geolayer's attributes.

##### Print fields metadata

```python
import geoformat

region_path = 'data/FRANCE_IGN/REGION_2016_L93.shp'

geolayer = geoformat.shapefile_to_geolayer(region_path)

print(geoformat.print_metadata_field_table(geolayer))

# >>>
# +------------+---------+-------+-----------+-------+
# | field name | type    | width | precision | index |
# +============+=========+=======+===========+=======+
# | CODE_REG   | String  | 2     | None      | 0     |
# | NOM_REG    | String  | 35    | None      | 1     |
# | POPULATION | Integer | None  | None      | 2     |
# | SUPERFICIE | Integer | None  | None      | 3     |
# +------------+---------+-------+-----------+-------+
```


##### Print features data

```python
import geoformat

region_path = 'data/FRANCE_IGN/REGION_2016_L93.shp'

geolayer = geoformat.shapefile_to_geolayer(region_path)

print(geoformat.print_features_data_table(geolayer))

# ### >>>
# +--------+----------+-------------------------------------+------------+------------+--------------+--------------------------------+
# | i_feat | CODE_REG | NOM_REG                             | POPULATION | SUPERFICIE | type         | coordinates                    |
# +========+==========+=====================================+============+============+==============+================================+
# | 0      | 76       | LANGUEDOC-ROUSSILLON-MIDI-PYRENEES  | 5683878    | 7243041    | MultiPolygon | [[[[449862.6000001011, ...]]]] |
# | 1      | 75       | AQUITAINE-LIMOUSIN-POITOU-CHARENTES | 5844177    | 8466821    | MultiPolygon | [[[[547193.100000056,  ...]]]] |
# | 2      | 84       | AUVERGNE-RHONE-ALPES                | 7757595    | 7014795    | Polygon      | [[[831613.6999999505, 6 ...]]] |
# | 3      | 32       | NORD-PAS-DE-CALAIS-PICARDIE         | 5987883    | 3187435    | Polygon      | [[[608820.0000000052, 7 ...]]] |
# | 4      | 44       | ALSACE-CHAMPAGNE-ARDENNE-LORRAINE   | 5552388    | 5732928    | Polygon      | [[[776081.0999999898, 6 ...]]] |
# | 5      | 93       | PROVENCE-ALPES-COTE D'AZUR          | 4953675    | 3155736    | MultiPolygon | [[[[1009696.399999884, ...]]]] |
# | 6      | 27       | BOURGOGNE-FRANCHE-COMTE             | 2819783    | 4746283    | Polygon      | [[[826122.1999999635, 6 ...]]] |
# | 7      | 52       | PAYS DE LA LOIRE                    | 3660852    | 2997777    | MultiPolygon | [[[[396406.200000058,  ...]]]] |
# | 8      | 28       | NORMANDIE                           | 3328364    | 2728511    | MultiPolygon | [[[[414934.20000005106 ...]]]] |
# | 9      | 11       | ILE-DE-FRANCE                       | 11959807   | 1205191    | Polygon      | [[[607657.2000000161, 6 ...]]] |
# | 10     | 24       | CENTRE-VAL DE LOIRE                 | 2570548    | 3905914    | Polygon      | [[[690565.4000000022, 6 ...]]] |
# | 11     | 53       | BRETAGNE                            | 3258707    | 2702269    | MultiPolygon | [[[[263628.30000009853 ...]]]] |
# | 12     | 94       | CORSE                               | 320208     | 875982     | MultiPolygon | [[[[1232225.1999997576 ...]]]] |
# +--------+----------+-------------------------------------+------------+------------+--------------+--------------------------------+
```

#### Draw a Geometry

Functions let you quickly **visualize** the geometries you are
manipulating or **export** them to an image file.\
You can display:

-   a standalone **geometry**,
-   a geometry contained in a **Feature**, or
-   all geometries inside a **Geolayer**.


##### Display a geometry in a window

Example with a polygon.\
Set `graticule=True` to add major/minor grid lines (default is `False`):

``` python
import geoformat
from tests.data.geometries import polygon_square_with_holes

geoformat.draw_geometry(polygon_square_with_holes, graticule=True)
```

If a graphical backend is available, a Matplotlib window opens and
shows:

![](https://framagit.org/Guilhain/Geoformat/-/raw/master/images/polygon_square_with_holes.png)


##### Save the figure directly to a file

In headless environments (servers, CI, SSH sessions) there may be **no
interactive display**.\
Pass a file path to `path` to export the figure without opening a
window.\
Optionally set `dpi` to control resolution:

``` python
import geoformat
from tests.data.geometries import polygon_square_with_holes

# Save as a 300 dpi PNG without showing a window
geoformat.draw_geometry(
    polygon_square_with_holes,
    graticule=True,
    path="polygon_output.png",
    dpi=300
)
```

When `path` is provided, **no interactive window is created**---the
image is written directly.


##### Draw a geometry in a Feature

To render a geometry stored inside a GeoJSON-like feature:

``` python
import geoformat
from tests.data.features import feature_katsuragawa

# Show interactively
geoformat.draw_feature(feature_katsuragawa, graticule=True)

# Or export only
geoformat.draw_feature(feature_katsuragawa, path="katsuragawa.png", dpi=200)
```

![](https://framagit.org/Guilhain/Geoformat/-/raw/master/images/katsuragawa.png)


##### Draw an entire Geolayer

To view or export all geometries of a geolayer:

``` python
import geoformat
from tests.data.geolayers import geolayer_idf_reseau_ferre

# Interactive window
geoformat.draw_geolayer(geolayer_idf_reseau_ferre, graticule=True)

# Save to file instead
geoformat.draw_geolayer(geolayer_idf_reseau_ferre,
                        graticule=True,
                        path="idf_reseau_ferre.png",
                        dpi=250)
```

![](https://framagit.org/Guilhain/Geoformat/-/raw/master/images/idf_reseau_ferre.png)

------------------------------------------------------------------------

**Summary of new parameters**

  -------------------------------------------------------------------------------------
  Parameter   Type                   Default   Purpose
  ----------- ---------------------- --------- ----------------------------------------
  `path`      `str | PathLike`       `None`    If set, save the figure to this file and
                                               skip `plt.show()`.

  `dpi`       `int | float | None`   `None`    Resolution (dots per inch) when saving
                                               the figure.
  -------------------------------------------------------------------------------------

> **Tip:**\
> If you are running on a server or inside a CI pipeline and see\
> `RuntimeError: Interactive display is not available`, simply supply
> `path="output.png"` to save the image instead of trying to open a
> window.


------------------------------------------------------------------------

### Geometry usefully functions

#### Area, length, distance, bbox and centroid

* [Area](#area)
* [Length](#length)
* [Distance](#distance)
* [Bbox](#bbox)
* [Centroid](#centroid)
  * [Euclidean distance](#euclidean-distance)
  * [Manhattan distance](#manhattan-distance)

##### Area

```python
import geoformat

geometry = {
    "type": "Polygon",
    "coordinates": [[[273950.31, 6643295.0], [190786.82, 6599267.27], [190786.82, 6467184.09], [44027.73, 6408480.45], [4891.97, 6364452.72], [39135.76, 6335100.9], [-122299.25, 6315533.03], [-141867.12, 6388912.57], [-200570.76, 6388912.57], [-176110.91, 6315533.03], [-151651.06, 6207909.69], [-288626.22, 6183449.84], [-337545.92, 6237261.51], [-513656.83, 6217693.63], [-528332.74, 6163881.96], [-489196.98, 6168773.93], [-469629.1, 6134530.14], [-513656.83, 6119854.23], [-484305.01, 6061150.59], [-450061.22, 6105178.32], [-313086.07, 6056258.63], [-225030.61, 5953527.26], [-249490.46, 5929067.41], [-136975.15, 5826336.04], [-112515.31, 5743172.56], [-73379.55, 5640441.19], [-127191.22, 5699144.83], [-127191.22, 5523033.92], [-195678.79, 5356706.94], [63595.61, 5258867.55], [73379.55, 5293111.33], [210354.7, 5229515.73], [362005.77, 5214839.82], [327761.98, 5307787.24], [464737.13, 5390950.73], [547900.62, 5386058.76], [665307.89, 5327355.12], [719119.56, 5342031.03], [841418.81, 5420302.55], [846310.78, 5479006.19], [782715.17, 5488790.13], [748471.38, 5625765.28], [777823.2, 5718712.71], [768039.26, 5836119.98], [684875.77, 5782308.32], [679983.8, 5860579.83], [758255.32, 5938851.35], [841418.81, 6031798.78], [851202.75, 6114962.26], [914798.35, 6271505.3], [748471.38, 6291073.18], [714227.59, 6354668.78], [635956.08, 6344884.84], [513656.83, 6447616.21], [322870.01, 6555239.55], [273950.31, 6643295.0]]]
}

geom_area = geoformat.geometry_area(geometry)

print(geom_area)

# >>> 1126425592118.5842
```

##### Length

```python
import geoformat

geometry = {
    "type": "Polygon",
    "coordinates": [[[273950.31, 6643295.0], [190786.82, 6599267.27], [190786.82, 6467184.09], [44027.73, 6408480.45], [4891.97, 6364452.72], [39135.76, 6335100.9], [-122299.25, 6315533.03], [-141867.12, 6388912.57], [-200570.76, 6388912.57], [-176110.91, 6315533.03], [-151651.06, 6207909.69], [-288626.22, 6183449.84], [-337545.92, 6237261.51], [-513656.83, 6217693.63], [-528332.74, 6163881.96], [-489196.98, 6168773.93], [-469629.1, 6134530.14], [-513656.83, 6119854.23], [-484305.01, 6061150.59], [-450061.22, 6105178.32], [-313086.07, 6056258.63], [-225030.61, 5953527.26], [-249490.46, 5929067.41], [-136975.15, 5826336.04], [-112515.31, 5743172.56], [-73379.55, 5640441.19], [-127191.22, 5699144.83], [-127191.22, 5523033.92], [-195678.79, 5356706.94], [63595.61, 5258867.55], [73379.55, 5293111.33], [210354.7, 5229515.73], [362005.77, 5214839.82], [327761.98, 5307787.24], [464737.13, 5390950.73], [547900.62, 5386058.76], [665307.89, 5327355.12], [719119.56, 5342031.03], [841418.81, 5420302.55], [846310.78, 5479006.19], [782715.17, 5488790.13], [748471.38, 5625765.28], [777823.2, 5718712.71], [768039.26, 5836119.98], [684875.77, 5782308.32], [679983.8, 5860579.83], [758255.32, 5938851.35], [841418.81, 6031798.78], [851202.75, 6114962.26], [914798.35, 6271505.3], [748471.38, 6291073.18], [714227.59, 6354668.78], [635956.08, 6344884.84], [513656.83, 6447616.21], [322870.01, 6555239.55], [273950.31, 6643295.0]]]
}

geom_length = geoformat.geometry_length(geometry)

print(geom_length)
# >>> 5999094.432367001
```
##### Distance

You can compute the distance between two points. There are two types of distance allowed:
- Euclidean
- Manhattan

##### Bbox

Each geometry has an associated bbox. The bbox represents the minimum bounding rectangle in which the geometry is embedded.

```python
import geoformat

polygon_geometry = {"type": "Polygon", "coordinates": [
    [[2.38, 57.322], [23.194, -20.28], [-120.43, 19.15], [2.38, 57.322]],
    [[-5.21, 23.51], [15.21, -10.81], [-20.51, 1.51], [-5.21, 23.51]],
]}

polygon_bbox = geoformat.geometry_to_bbox(polygon_geometry)

print(polygon_bbox)

# >>> (-120.43, -20.28, 23.194, 57.322)
```
##### Centroid

You can get the centroid coordinates, which is the mean of all coordinates of a geometry.

```python
import geoformat

multi_point_coordinates  = [[-155.52, 19.61], [-156.22, 20.74], [-157.97, 21.46]]

multi_point_centroid = geoformat.coordinates_to_centroid(multi_point_coordinates)

print(multi_point_centroid)

# >>> [-156.57000000000002, 20.60333333333333]
```


###### Euclidean distance

```python
import geoformat

point_a = (0, 0)
point_b = (1, 1)

print(geoformat.euclidean_distance(point_a, point_b))

# >>> 1.4142135623730951
```


###### Manhattan distance

```python
import geoformat

point_a = (0, 0)
point_b = (1, 1)

print(geoformat.manhattan_distance(point_a, point_b))

# >>> 2.0
```

#### Reproject 

You can reproject an entire Geolayer or a simple geometry. To do that, indicate the input EPSG and the desired EPSG for the output.

* [Reproject Geometry](#reproject-geometry)
* [Reproject Geolayer](#reproject-geolayer)

##### Reproject Geometry

```python
import geoformat

point_geometry_4326 = {"type": "Point", "coordinates": [-115.81, 37.24]}

point_geometry_3857 = geoformat.reproject_geometry(point_geometry_4326, 4326, 3857)

print(point_geometry_3857)

# >>> {'type': 'Point', 'coordinates': [-12891910.228769012, 4472612.698469004]}
```

##### Reproject Geolayer

In this example, we will transform a geolayer from the Lambert93 projection [EPSG:2154] to the WGS84 coordinate system [EPSG:4326].

```python
import geoformat

region_path = 'data/FRANCE_IGN/REGION_2016_L93.shp'

geolayer = geoformat.shapefile_to_geolayer(region_path)

geolayer = geoformat.reproject_geolayer(geolayer, in_crs=2154, out_crs=4326)

print(geolayer['metadata']['geometry_ref']['crs'])

# >>>4326
```

#### Geometry formatting

* [wkb_to_geometry](#wkb_to_geometry)
* [geometry_to_wkb](#geometry_to_wkb)
* [wkt_to_geometry](#wkt_to_geometry)
* [geometry_to_wkt](#geometry_to_wkt)

##### wkb_to_geometry

You can convert geometry in WKB format to Geoformat geometry.

```python
import geoformat

geoformat_geometry = geoformat.wkb_to_geometry(b"\x00\x00\x00\x00\x01\xc0\\\xf3\xd7\n=p\xa4@B\x9e\xb8Q\xeb\x85\x1f")

print(geoformat_geometry)

# >>>  {"type": "Point", "coordinates": [-115.81, 37.24], "bbox": (-115.81, 37.24, -115.81, 37.24)}
```

##### geometry_to_wkb

You can convert Geoformat geometry to WKB format.

```python
import geoformat

wkb_geometry = geoformat.geometry_to_wkb({"type": "Point", "coordinates": [-115.81, 37.24]})

print(wkb_geometry)

# >>> b"\x00\x00\x00\x00\x01\xc0\\\xf3\xd7\n=p\xa4@B\x9e\xb8Q\xeb\x85\x1f"
```

##### wkt_to_geometry

From WKT geometry, you can make a conversion to Geoformat geometry.

```python
import geoformat

geoformat_geometry = geoformat.wkt_to_geometry("POINT (-115.81 37.24)")

print(geoformat_geometry)

# >>>  {"type": "Point", "coordinates": [-115.81, 37.24], "bbox": (-115.81, 37.24, -115.81, 37.24)}
```

##### geometry_to_wkt

Convert a geometry to WKT.

```python
import geoformat

wkt_geometry = geoformat.geometry_to_wkt({"type": "Point", "coordinates": [-115.81, 37.24]})

print(wkt_geometry)

# >>> "POINT (-115.81 37.24)"
```

#### Geometrics manipulations

In this section, we will present the functions for performing geometric operations.

* [Generalization](#generalization)
  * [Ramer Douglas Peucker](#ramer-douglas-peucker)
  * [Visvalimgam Whyatt](#visvalimgam-whyatt)
* [Merging geometries](#merging-geometries)
  * [LineString merging](#linestring-merging)
  * [Geometry merging](#geometry-merging)
* [Point on Linestring](#point-on-linestring)
* [Force rhr](#force-rhr)
* [Multi and single geometry](#multi-and-single-geometry)
  * [single_geometry_to_multi_geometry](#single_geometry_to_multi_geometry)
  * [multi_geometry_to_single_geometry](#multi_geometry_to_single_geometry)

##### Generalization

Geoformat allows cartographic generalization via the _simplify_ function by allowing the user to choose between two algorithms: 
* Ramer-Douglas-Peucker (RDP)
* Visvalingam-Whyatt (VW)

###### Ramer-Douglas-Peucker

The Ramer-Douglas-Peucker algorithm is a geometric simplification technique that reduces the number of points in a geometry while maintaining its overall shape.
It recursively subdivides the polyline, retaining points that exceed a specified distance tolerance.

Advantages:

* Preserves overall shape.
* Efficient for simple or mildly complex geometry.
* Easy to implement.

Disadvantages:

* Can produce suboptimal results for densely packed points.
* Slower for highly complex geometries.
* Fixed tolerance might not suit varying data densities.

```python
import geoformat

loire_geometry = {"type": "LineString", "coordinates": [[-237872.03, 5988382.54], [-209743.21, 5987159.55], [-198124.78, 5976152.62], [-174276.42, 5974318.13], [-154097.05, 5986548.06], [-145536.1, 5998777.98], [-119241.76, 6002446.96], [-104565.85, 6003669.95], [-88055.46, 6004281.45], [-78271.52, 6004281.45], [-66653.09, 6009173.42], [-52588.68, 6009173.42], [-37301.27, 6009173.42], [-32409.3, 6008561.92], [-23236.86, 5996331.99], [-4891.97, 5983490.57], [9172.44, 5977987.11], [18344.89, 5980433.09], [48919.7, 5996943.49], [67264.58, 6001223.96], [91724.43, 6009784.91], [116184.28, 6010396.41], [149205.08, 6032410.27], [155320.04, 6045863.19], [181002.88, 6060539.1], [182225.88, 6071546.03], [194455.8, 6079495.48], [204851.24, 6083775.96], [218915.65, 6089890.92], [245209.99, 6084387.45], [257439.91, 6077049.5], [295352.68, 6051978.15], [307582.6, 6038525.23], [328984.97, 6018957.36], [317366.54, 5998166.48], [342437.89, 5969426.16], [344883.87, 5941297.33], [389523.1, 5912557.01], [409702.47, 5892377.64], [412759.95, 5881982.2], [426824.37, 5861802.83], [447615.24, 5848961.4], [451284.21, 5812271.63], [452507.21, 5782919.81], [448838.23, 5766409.41], [457399.18, 5755402.48], [469017.61, 5738280.59], [465348.63, 5710151.76], [473298.08, 5695475.85], [468406.11, 5678965.45], [461068.15, 5656951.59], [448838.23, 5654811.35], [437066.93, 5651753.87], [436608.31, 5643498.67], [435385.31, 5636772.21], [440583.03, 5624695.16], [434468.07, 5621943.43], [435538.19, 5617815.83], [437831.3, 5613229.61], [435232.44, 5604821.54]]}
loire_simplify_geometry = geoformat.simplify(
    geometry=loire_geometry,
    tolerance=5000, 
    algo='RDP'
)

print(loire_simplify_geometry)

# >>> {'type': 'LineString', 'coordinates': [[-237872.03, 5988382.54], [-174276.42, 5974318.13], [-145536.1, 5998777.98], [-37301.27, 6009173.42], [9172.44, 5977987.11], [48919.7, 5996943.49], [116184.28, 6010396.41], [149205.08, 6032410.27], [194455.8, 6079495.48], [218915.65, 6089890.92], [245209.99, 6084387.45], [295352.68, 6051978.15], [328984.97, 6018957.36], [317366.54, 5998166.48], [342437.89, 5969426.16], [344883.87, 5941297.33], [389523.1, 5912557.01], [426824.37, 5861802.83], [447615.24, 5848961.4], [448838.23, 5766409.41], [469017.61, 5738280.59], [465348.63, 5710151.76], [473298.08, 5695475.85], [461068.15, 5656951.59], [437066.93, 5651753.87], [435232.44, 5604821.54]]}
```

###### Visvalimgam Whyatt

The Visvalingam-Whyatt algorithm is a geometry simplification method that reduces the number of points while preserving shape. 
It iteratively removes points based on their "effective area," which is the triangular area formed by a point and its two neighbors.
The algorithm removes points with the smallest areas until a desired level of simplification is achieved.

Advantages:

* Better preservation of local features.
* More intuitive elimination based on effective area.
* Adaptive to varying data densities.

Disadvantages:

* Can be slower for large datasets.
* More complex to implement.
* Requires pre-sorting or priority queue for best performance.

```python
import geoformat

france_geometry = {
    "type": "Polygon",
    "coordinates":  [[[273950.31, 6643295.0], [190786.82, 6599267.27], [190786.82, 6467184.09], [44027.73, 6408480.45], [4891.97, 6364452.72], [39135.76, 6335100.9], [-122299.25, 6315533.03], [-141867.12, 6388912.57], [-200570.76, 6388912.57], [-176110.91, 6315533.03], [-151651.06, 6207909.69], [-288626.22, 6183449.84], [-337545.92, 6237261.51], [-513656.83, 6217693.63], [-528332.74, 6163881.96], [-489196.98, 6168773.93], [-469629.1, 6134530.14], [-513656.83, 6119854.23], [-484305.01, 6061150.59], [-450061.22, 6105178.32], [-313086.07, 6056258.63], [-225030.61, 5953527.26], [-249490.46, 5929067.41], [-136975.15, 5826336.04], [-112515.31, 5743172.56], [-73379.55, 5640441.19], [-127191.22, 5699144.83], [-127191.22, 5523033.92], [-195678.79, 5356706.94], [63595.61, 5258867.55], [73379.55, 5293111.33], [210354.7, 5229515.73], [362005.77, 5214839.82], [327761.98, 5307787.24], [464737.13, 5390950.73], [547900.62, 5386058.76], [665307.89, 5327355.12], [719119.56, 5342031.03], [841418.81, 5420302.55], [846310.78, 5479006.19], [782715.17, 5488790.13], [748471.38, 5625765.28], [777823.2, 5718712.71], [768039.26, 5836119.98], [684875.77, 5782308.32], [679983.8, 5860579.83], [758255.32, 5938851.35], [841418.81, 6031798.78], [851202.75, 6114962.26], [914798.35, 6271505.3], [748471.38, 6291073.18], [714227.59, 6354668.78], [635956.08, 6344884.84], [513656.83, 6447616.21], [322870.01, 6555239.55], [273950.31, 6643295.0]]],
}
france_simplify_geometry = geoformat.simplify(
    geometry=france_geometry,
    tolerance=3000000000, 
    algo='VW'
)

print(france_simplify_geometry)

# >>> {'type': 'Polygon', 'coordinates': [[[273950.31, 6643295.0], [190786.82, 6599267.27], [190786.82, 6467184.09], [44027.73, 6408480.45], [39135.76, 6335100.9], [-122299.25, 6315533.03], [-151651.06, 6207909.69], [-288626.22, 6183449.84], [-337545.92, 6237261.51], [-513656.83, 6217693.63], [-469629.1, 6134530.14], [-313086.07, 6056258.63], [-225030.61, 5953527.26], [-136975.15, 5826336.04], [-127191.22, 5699144.83], [-127191.22, 5523033.92], [-195678.79, 5356706.94], [63595.61, 5258867.55], [210354.7, 5229515.73], [362005.77, 5214839.82], [327761.98, 5307787.24], [464737.13, 5390950.73], [547900.62, 5386058.76], [665307.89, 5327355.12], [719119.56, 5342031.03], [841418.81, 5420302.55], [782715.17, 5488790.13], [748471.38, 5625765.28], [777823.2, 5718712.71], [768039.26, 5836119.98], [684875.77, 5782308.32], [679983.8, 5860579.83], [758255.32, 5938851.35], [841418.81, 6031798.78], [851202.75, 6114962.26], [914798.35, 6271505.3], [748471.38, 6291073.18], [635956.08, 6344884.84], [513656.83, 6447616.21], [322870.01, 6555239.55], [273950.31, 6643295.0]]]}
```


##### Merging geometries

###### LineString merging

This function is similar than [ST_LineMerge](https://postgis.net/docs/ST_LineMerge.html) from Postgis. 
You can optionaly choose to not allow reverse geometry when setting `directed` to True.  This is useful when dealing 
with a road network where the direction in which the coordinates are encoded is important for defining the direction of 
traffic.)

```python
import geoformat 

multi_line_geometry = {
            "type": "MultiLineString",
            "coordinates": [
                [[2, 3], [5, 2], [8, 4]],
                [[5, 9], [3, 6]],
                [[3, 6], [2, 3]],
                [[8, 4], [9, 7], [5, 9]],
            ],
        }

line_merged_geometry = geoformat.line_merge(geometry=multi_line_geometry)

print(line_merged_geometry)

# >>> {"type": "LineString", "coordinates": [[8, 4], [9, 7], [5, 9], [3, 6], [2, 3], [5, 2], [8, 4]]}
```

Example with directed linestring.
```python
import geoformat 

multi_line_geometry = {
  "type": "MultiLineString",
  "coordinates": [
    [[0, 0], [0, 10],],
    [[0, 10], [0, 20]],
    [[0, 30], [0, 20]]
    ]
}

line_merged_geometry = geoformat.line_merge(geometry=multi_line_geometry, directed=True)

print(line_merged_geometry)

# >>> {'type': 'MultiLineString', 'coordinates': [[[0, 0], [0, 10], [0, 20]], [[0, 30], [0, 20]]]}
```

###### Geometry merging

Return the result of merging two different geometries by adding them together. Please note that this function does not "union" two geometries that intersect. It simply adds one geometry to another.

Here is an example of merging combinations:

- Single AND Single
  * `Point` + `Point` = `MultiPoint`
  * `LineString` + `LineString` = `MultiLineString`
  * `Polygon` + `Polygon` = `MultiPolygon` 

- Single AND Multi 
  * `Point` + `MultiPoint` = `MultiPoint` 
  * `LineString`  + `MultiLineString` = `MultiLineString`
  * `Polygon` + `MultiPolygon` = `MultiPolygon`

- Mixed Geometries Types and GeometryCollection
  * `Point` + `Polygon` = `GeometryCollection(Point, Polygon)`
  * `GeometryCollection(Polygon + LineString)` + `LineSting` = `GeometryCollection(Polygon + MultiLineString)`
  * `GeometryCollection(MultiPolygon, LineString), GeometryCollection(MultiPoint, LineString)` = `GeometryCollection(MultiPolygon, MultiLineString, MultiPoint)`

```python
import geoformat

geometry_a = {"type": "Point", "coordinates": [-115.81, 37.24]}
geometry_b = {"type": "Polygon", "coordinates": [
    [[2.38, 57.322], [23.194, -20.28], [-120.43, 19.15], [2.38, 57.322]],
    [[-5.21, 23.51], [15.21, -10.81], [-20.51, 1.51], [-5.21, 23.51]],
]}

merged_geometry = geoformat.merge_geometries(geometry_a, geometry_b, bbox=False)

print(merged_geometry)

# >>> {'type': 'GeometryCollection', 'geometries': [{'type': 'Point', 'coordinates': [-115.81, 37.24]}, {'type': 'Polygon', 'coordinates': [[[2.38, 57.322], [23.194, -20.28], [-120.43, 19.15], [2.38, 57.322]], [[-5.21, 23.51], [15.21, -10.81], [-20.51, 1.51], [-5.21, 23.51]]]}]}
```


##### Point on Linestring


If you want to add point(s) on a linestring at a given distance, points_on_linestring_distance is for you. 
You can optionally add an offset that will shift each point created to the left (positive offset) or right (negative offset) of the line depending on the direction of the line.

```python
import geoformat

linestring_geometry =  {
    "type": "LineString",
    "coordinates": [[-10, -10], [-10, 10], [10, 10]],
}

point_on_linestring_gen = geoformat.points_on_linestring_distance(linestring_geometry, 5)

for point in point_on_linestring_gen:
    print(point)

# >>> {'type': 'Point', 'coordinates': [-10, -10]}
# >>> {'type': 'Point', 'coordinates': [-10, -5]}
# >>> {'type': 'Point', 'coordinates': [-10, 0]}
# >>> {'type': 'Point', 'coordinates': [-10, 5]}
# >>> {'type': 'Point', 'coordinates': [-10, 10]}
# >>> {'type': 'Point', 'coordinates': [-5.0, 10.0]}
# >>> {'type': 'Point', 'coordinates': [0.0, 10.0]}
# >>> {'type': 'Point', 'coordinates': [5.0, 10.0]}
# >>> {'type': 'Point', 'coordinates': [10.0, 10.0]}
```


Same with offset:

```python
import geoformat

linestring_geometry =  {
    "type": "LineString",
    "coordinates": [[-10, -10], [-10, 10], [10, 10]],
}

point_on_linestring_offset_gen = geoformat.points_on_linestring_distance(linestring_geometry, 5, -10)

for point in point_on_linestring_offset_gen:
    print(point)

# >>> {'type': 'Point', 'coordinates': [-20.0, -10.0]}
# >>> {'type': 'Point', 'coordinates': [-20.0, -5.0]}
# >>> {'type': 'Point', 'coordinates': [-20.0, 0.0]}
# >>> {'type': 'Point', 'coordinates': [-20.0, 5.0]}
# >>> {'type': 'Point', 'coordinates': [-20.0, 10.0]}
# >>> {'type': 'Point', 'coordinates': [-5.0, 0.0]}
# >>> {'type': 'Point', 'coordinates': [0.0, 0.0]}
# >>> {'type': 'Point', 'coordinates': [5.0, 0.0]}
# >>> {'type': 'Point', 'coordinates': [10.0, 0.0]}
```

##### Force rhr


Force the orientation of the vertices in a polygon to follow the Right-Hand-Rule, in which the area bounded by the polygon is to the right of the boundary. Specifically, the exterior ring is oriented in a clockwise direction, and the interior rings are oriented in a counter-clockwise direction.

``` python
import geoformat

polygon = {'type': 'Polygon',
           'coordinates': [[[0, 0], [5, 0], [0, 5], [0, 0]],
                           [[1, 1], [1, 3], [3, 1], [1, 1]]]}
                                             
                                             
polygon_rhr = geoformat.force_rhr(polygon)

print(polygon_rhr)

# >>> {'type': 'Polygon', 'coordinates': [[[0, 0], [0, 5], [5, 0], [0, 0]], [[1, 1], [3, 1], [1, 3], [1, 1]]]}
```

##### Multi and single geometry

As you know, there are single and multi geometries:

**Single geometry:**
- `Point`
- `LineString`
- `Polygon`

**Multi geometry:**
- `MultiPoint`
- `MultiLineString`
- `MultiPolygon`

There are two functions for switching from one to the other: `single_geometry_to_multi_geometry` and `multi_geometry_to_single_geometry`.


###### single_geometry_to_multi_geometry

```python
import geoformat

polygon = {"type": "Polygon", "coordinates": [
        [[2.38, 57.322], [23.194, -20.28], [-120.43, 19.15], [2.38, 57.322]],
        [[-5.21, 23.51], [15.21, -10.81], [-20.51, 1.51], [-5.21, 23.51]],
    ]
}

multi_polygon = geoformat.single_geometry_to_multi_geometry(polygon)

print(multi_polygon)

# >>> {'type': 'MultiPolygon', 'coordinates': [[[[2.38, 57.322], [23.194, -20.28], [-120.43, 19.15], [2.38, 57.322]], [[-5.21, 23.51], [15.21, -10.81], [-20.51, 1.51], [-5.21, 23.51]]]]}
```

###### multi_geometry_to_single_geometry

This function returns a generator with single geometries of multi geometry in each iteration.

```python
import geoformat

multi_geometry = {
    "type": "MultiLineString",
    "coordinates": [
        [[3.75, 9.25], [-130.95, 1.52]],
        [[23.15, -34.25], [-1.35, -4.65], [3.45, 77.95]],
    ],
}

for geometry in geoformat.multi_geometry_to_single_geometry(multi_geometry):
    print(geometry)

# >>> {'type': 'LineString', 'coordinates': [[3.75, 9.25], [-130.95, 1.52]]}
# >>> {'type': 'LineString', 'coordinates': [[23.15, -34.25], [-1.35, -4.65], [3.45, 77.95]]}
```

#### Low level geometrics objects


There are low-level geometric objects that compose or describe the geometries generally used in GIS (point, linestring, polygon, etc.).
It is with these low-level geometric objects that we calculate the existing interactions between the GIS geometries.
For more details, refer to the technical documentation : [Geoformat Technical Description [WIP]](#geoformat-technical-description).

* [point, segment and bbox](#point-segment-and-bbox)
  * [point_intersects_point](#point_intersects_point)
  * [point_intersects_segment](#point_intersects_segment)
  * [point_intersects_bbox](#point_intersects_bbox)
  * [segment_intersects_segment](#segment_intersects_segment)
  * [segment_intersects_bbox](#segment_intersects_bbox)
  * [bbox_intersects_bbox](#bbox_intersects_bbox)
  * [ccw_or_cw_segments](#ccw_or_cw_segments)
  * [point_position_segment](#point_position_segment)
  * [extent_bbox](#extent_bbox)
  * [bbox_union](#bbox_union)
  * [point_bbox_position](#point_bbox_position)
  * [random](#random)
* [Line](#line)
  * [line_parameters](#line_parameters)
  * [perpendicular_line_parameters_at_point](#perpendicular_line_parameters_at_point)
  * [point_at_distance_with_line_parameters](#point_at_distance_with_line_parameters)
  * [crossing_point_from_lines_parameters](#crossing_point_from_lines_parameters)

##### point, segment and bbox

###### point_intersects_point

Check if two points are similar.

```python
import geoformat

point_a = (10, 10)
point_b = (10, 10)
pt_a_int_pt_b = geoformat.point_intersects_point(point_a, point_b)

print(pt_a_int_pt_b)

# >>> True
```

###### point_intersects_segment

Check if a point intersects a segment.

```python
import geoformat

point =  (0, 0)
segment = ((-10, -10), (10, 10))

pt_int_seg = geoformat.point_intersects_segment(point, segment)

print(pt_int_seg)

# >>> True
```


###### point_intersects_bbox

Check if a point is in a bounding box (bbox).

```python
import geoformat

point =  (0, 0)
bbox = (-10, -10, 10, 10)

pt_int_bbx = geoformat.point_intersects_bbox(point, bbox)

print(pt_int_bbx)

# >>> True
```

###### segment_intersects_segment

Check if a segment intersects another segment.

```python
import geoformat

segment_a = ((10, 10), (-10, -10))
segment_b = ((-10, 10), (10, -10))

seg_int_seg = geoformat.segment_intersects_segment(segment_a, segment_b)

print(seg_int_seg)

# >>> True
```

###### segment_intersects_bbox

Check if a segment intersects a bounding box (bbox).

```python
import geoformat

segment = ((-2, 0), (2, 0))
bbox = (-10, -10, 10, 10)

seg_int_bbx = geoformat.segment_intersects_bbox(segment, bbox)

print(seg_int_bbx)

# >>> True
```

###### bbox_intersects_bbox

Check if two bounding boxes (bbox) intersect.

```python
import geoformat

bbox_a = (-10, -10, 10, 10)
bbox_b = (-10, -10, 10, 10)

bbx_int_bbx = geoformat.bbox_intersects_bbox(bbox_a, bbox_b)

print(bbx_int_bbx)

# >>> True
```

###### ccw_or_cw_segments

Check if two continuous segments are clockwise ('CW') or counterclockwise ('CCW').

Counter-clockwise example:

```python
import geoformat

segment_a = ((-5, -5), (5, 5))
segment_b =  ((5, 5), (-1, 2))

seg_a_and_seg_b_clock_wise = geoformat.ccw_or_cw_segments(segment_a, segment_b)

print(seg_a_and_seg_b_clock_wise)

# >>> 'CCW'
```

Clockwise example:

```python
import geoformat

segment_a = ((-5, -5), (5, 5))
segment_b = ((5, 5), (5, -5))

seg_a_and_seg_b_clock_wise = geoformat.ccw_or_cw_segments(segment_a, segment_b)

print(seg_a_and_seg_b_clock_wise)

# >>> 'CCW'
```

###### point_position_segment

Give the position of a point with respect to the direction of a segment. There are three possible positions: 
- `LEFT`
- `RIGHT` 
- `NEITHER`

Left example : 
```python
import geoformat

segment = ((-5, -5), (5, 5))
point =  (-1, 2)

pt_vs_seg_position = geoformat.point_position_segment(point, segment)

print(pt_vs_seg_position)

# >>> 'LEFT'
```

right example :
```python
import geoformat

segment = ((-5, -5), (5, 5))
point = (5, -5)

pt_vs_seg_position = geoformat.point_position_segment(point, segment)

print(pt_vs_seg_position)

# >>> 'RIGHT'
```

neither example :
```python
import geoformat

segment = ((-5, -5), (5, 5))
point = (10, 10)

pt_vs_seg_position = geoformat.point_position_segment(point, segment)

print(pt_vs_seg_position)

# >>> 'NEITHER'
```

###### bbox_expand

Expand a bounding box (bbox).

```python
import geoformat

bbox = (-1, -1, 1, 1)
bbox_expanded = geoformat.bbox_expand(bbox, 10)

print(bbox_expanded)

# >>> (-11, -11, 11, 11)
```
###### bbox_union

Union two bounding boxes (bbox).

```python
import geoformat

bbox_a = (-10, -10, 0, 0)
bbox_b = (0, 0, 10, 10)

bbox_unioned = geoformat.bbox_union(bbox_a, bbox_b)

print(bbox_unioned)

# >>> (-10, -10, 10, 10)
```

###### point_bbox_position

Gives the position of a point relative to a bounding box (bbox).
This function returns 2 main pieces of information:
1. If the point is localized in the Exterior, Interior, or on the bbox Boundary
2. The place of the frame where the point is located (NW, N, NE, W, E, SW, S, SE)

```ascii 
   NW  |   N  |  NE
-------+------+-------
    W  | bbox |   E
-------+------+-------
   SW  |   S  |  SE
```

Point localized on the bbox North-West (NW) corner.

```python
import geoformat

point = (-10, 10)
bbox = (-10, -10, 10, 10)

point_position = geoformat.point_bbox_position(point, bbox)

print(point_position)

# >>> ('Boundary', 'NW')
```

Point localised in bounding boxes (bbox).
```python
import geoformat

point = (0, 0)
bbox = (-10, -10, 10, 10)

point_position = geoformat.point_bbox_position(point, bbox)

print(point_position)

# >>> ('Interior', None)
```


Point localised in South (S) of bounding boxes (bbox).
```python
import geoformat
        
point = (0, -11)
bbox = (-10, -10, 10, 10)

point_position = geoformat.point_bbox_position(point, bbox)

print(point_position)

# >>> ('Exterior', 'S')
```

###### random


These three low-level geometric objects can be created randomly. You must specify a bounding box (bbox) within which these objects will be created. 
Optionally, you can specify the number of digits after the decimal point for each coordinate.

####### random_point

```python
import geoformat

bbox = (-180, -90, 180, 90)
random_point = geoformat.random_point(bbox)

print(random_point)

# >>> (59.89, 1.04)
```

####### random_segment


```python
import geoformat

bbox = (-180, -90, 180, 90)
random_segment = geoformat.random_segment(bbox)

print(random_segment)

# >>> ((-155.78, 82.55), (99.11, -17.46))
```

####### random_