Como escrever geometrias bem torneadas em shapefiles?

26

Alguém pode demonstrar uma maneira simples de escrever estruturas de dados de geometria de bem torneados em arquivos de forma? Estou particularmente interessado em polígonos com furos e cordas de linha. Também seria benéfico ficar longe do arcpy (então osgeo, pyshp etc. seriam melhores).

terra_matics
fonte

Respostas:

44

O binário conhecido é um bom formato de troca binária que pode ser trocado com muitos softwares GIS, incluindo Shapely e GDAL / OGR.

Este é um pequeno exemplo do fluxo de trabalho com osgeo.ogr:

from osgeo import ogr
from shapely.geometry import Polygon

# Here's an example Shapely geometry
poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)])

# Now convert it to a shapefile with OGR    
driver = ogr.GetDriverByName('Esri Shapefile')
ds = driver.CreateDataSource('my.shp')
layer = ds.CreateLayer('', None, ogr.wkbPolygon)
# Add one attribute
layer.CreateField(ogr.FieldDefn('id', ogr.OFTInteger))
defn = layer.GetLayerDefn()

## If there are multiple geometries, put the "for" loop here

# Create a new feature (attribute and geometry)
feat = ogr.Feature(defn)
feat.SetField('id', 123)

# Make a geometry, from Shapely object
geom = ogr.CreateGeometryFromWkb(poly.wkb)
feat.SetGeometry(geom)

layer.CreateFeature(feat)
feat = geom = None  # destroy these

# Save and close everything
ds = layer = feat = geom = None

Atualização : Embora o pôster tenha aceitado a resposta GDAL / OGR, aqui está um equivalente da Fiona :

from shapely.geometry import mapping, Polygon
import fiona

# Here's an example Shapely geometry
poly = Polygon([(0, 0), (0, 1), (1, 1), (0, 0)])

# Define a polygon feature geometry with one attribute
schema = {
    'geometry': 'Polygon',
    'properties': {'id': 'int'},
}

# Write a new Shapefile
with fiona.open('my_shp2.shp', 'w', 'ESRI Shapefile', schema) as c:
    ## If there are multiple geometries, put the "for" loop here
    c.write({
        'geometry': mapping(poly),
        'properties': {'id': 123},
    })

(Nota Usuários do Windows: você não tem desculpa )

Mike T
fonte
Interessado por que você escolheu esse método e não a biblioteca Fiona.
Nathan W
11
Bem, o cartaz estava procurando um exemplo de osgeo.ogr, e a comparação é interessante.
sgillies
Comparação explícita @sgillies adicionada
Mike T
3
Bem, para ser sincero, era principalmente pragmático. Apreciei o esforço de demonstrar o código em resposta à minha pergunta e já estava mexendo com o osgeo. Desde então, tentei os dois métodos e ambas são respostas suficientes. Agradeço o esforço dos respondentes de serem precisos e rápidos.
terra_matics
@ Mike T Quanto à abordagem osgeo.ogr, estou usando-a em um plugin Python para QGIS. O arquivo de forma considerado a ser gravado é uma Linha (LineString em Shapely). Onde você definiu a variável "poly", eu defini uma variável "line" com coordenadas de um Qgs.Rectangle. Eu usei o código exato, sem erros, mas ele não adiciona um recurso e me fornece um shapefile sem recursos.
Akhil
28

Eu projetei Fiona para funcionar bem com Shapely. Aqui está um exemplo muito simples de usá-los juntos para "limpar" os recursos do shapefile:

import logging
import sys

from shapely.geometry import mapping, shape

import fiona

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

with fiona.open('docs/data/test_uk.shp', 'r') as source:

    # **source.meta is a shortcut to get the crs, driver, and schema
    # keyword arguments from the source Collection.
    with fiona.open(
            'with-shapely.shp', 'w',
            **source.meta) as sink:

        for f in source:

            try:
                geom = shape(f['geometry'])
                if not geom.is_valid:
                    clean = geom.buffer(0.0)
                    assert clean.is_valid
                    assert clean.geom_type == 'Polygon'
                    geom = clean
                f['geometry'] = mapping(geom)
                sink.write(f)

            except Exception, e:
                # Writing uncleanable features to a different shapefile
                # is another option.
                logging.exception("Error cleaning feature %s:", f['id'])

Em https://github.com/Toblerity/Fiona/blob/master/examples/with-shapely.py .

sgillies
fonte
6

Você também pode escrever geometrias bem torneadas usando PyShp (já que o pôster original também perguntou sobre PyShp).

Uma maneira seria converter sua geometria bem torneada em geojson (com o método shapely.geometry.mapping) e depois usar meu fork modificado do PyShp, que fornece um método Writer que aceita dicionários de geometria geojson ao gravar em um shapefile.

Se você preferir confiar na versão principal do PyShp, também forneci uma função de conversão abaixo:

# THIS FUNCTION CONVERTS A GEOJSON GEOMETRY DICTIONARY TO A PYSHP SHAPE OBJECT
def shapely_to_pyshp(shapelygeom):
    # first convert shapely to geojson
    try:
        shapelytogeojson = shapely.geometry.mapping
    except:
        import shapely.geometry
        shapelytogeojson = shapely.geometry.mapping
    geoj = shapelytogeojson(shapelygeom)
    # create empty pyshp shape
    record = shapefile._Shape()
    # set shapetype
    if geoj["type"] == "Null":
        pyshptype = 0
    elif geoj["type"] == "Point":
        pyshptype = 1
    elif geoj["type"] == "LineString":
        pyshptype = 3
    elif geoj["type"] == "Polygon":
        pyshptype = 5
    elif geoj["type"] == "MultiPoint":
        pyshptype = 8
    elif geoj["type"] == "MultiLineString":
        pyshptype = 3
    elif geoj["type"] == "MultiPolygon":
        pyshptype = 5
    record.shapeType = pyshptype
    # set points and parts
    if geoj["type"] == "Point":
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("MultiPoint","Linestring"):
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("Polygon"):
        record.points = geoj["coordinates"][0]
        record.parts = [0]
    elif geoj["type"] in ("MultiPolygon","MultiLineString"):
        index = 0
        points = []
        parts = []
        for eachmulti in geoj["coordinates"]:
            points.extend(eachmulti[0])
            parts.append(index)
            index += len(eachmulti[0])
        record.points = points
        record.parts = parts
    return record

Simplesmente copie e cole a função em seu próprio script e convoque-a para converter qualquer uma de suas geometrias bem torneadas em uma forma compatível com pyshp. Para salvá-los, basta anexar cada forma pyshp resultante à lista ._shapes da instância shapefile.Writer (por exemplo, veja o script de teste na parte inferior desta postagem).

Observe no entanto: a função NÃO manipulará nenhum orifício interno de polígono, se houver algum, simplesmente os ignora. Certamente é possível adicionar essa funcionalidade à função, mas eu simplesmente ainda não me incomodei. Sugestões ou edições para melhorar a função são bem-vindas :)

Aqui está um script de teste independente completo:

### HOW TO SAVE SHAPEFILE FROM SHAPELY GEOMETRY USING PYSHP

# IMPORT STUFF
import shapefile
import shapely, shapely.geometry

# CREATE YOUR SHAPELY TEST INPUT
TEST_SHAPELYSHAPE = shapely.geometry.Polygon([(133,822),(422,644),(223,445),(921,154)])

#########################################################
################## END OF USER INPUT ####################
#########################################################

# DEFINE/COPY-PASTE THE SHAPELY-PYSHP CONVERSION FUNCTION
def shapely_to_pyshp(shapelygeom):
    # first convert shapely to geojson
    try:
        shapelytogeojson = shapely.geometry.mapping
    except:
        import shapely.geometry
        shapelytogeojson = shapely.geometry.mapping
    geoj = shapelytogeojson(shapelygeom)
    # create empty pyshp shape
    record = shapefile._Shape()
    # set shapetype
    if geoj["type"] == "Null":
        pyshptype = 0
    elif geoj["type"] == "Point":
        pyshptype = 1
    elif geoj["type"] == "LineString":
        pyshptype = 3
    elif geoj["type"] == "Polygon":
        pyshptype = 5
    elif geoj["type"] == "MultiPoint":
        pyshptype = 8
    elif geoj["type"] == "MultiLineString":
        pyshptype = 3
    elif geoj["type"] == "MultiPolygon":
        pyshptype = 5
    record.shapeType = pyshptype
    # set points and parts
    if geoj["type"] == "Point":
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("MultiPoint","Linestring"):
        record.points = geoj["coordinates"]
        record.parts = [0]
    elif geoj["type"] in ("Polygon"):
        record.points = geoj["coordinates"][0]
        record.parts = [0]
    elif geoj["type"] in ("MultiPolygon","MultiLineString"):
        index = 0
        points = []
        parts = []
        for eachmulti in geoj["coordinates"]:
            points.extend(eachmulti[0])
            parts.append(index)
            index += len(eachmulti[0])
        record.points = points
        record.parts = parts
    return record

# WRITE TO SHAPEFILE USING PYSHP
shapewriter = shapefile.Writer()
shapewriter.field("field1")
# step1: convert shapely to pyshp using the function above
converted_shape = shapely_to_pyshp(TEST_SHAPELYSHAPE)
# step2: tell the writer to add the converted shape
shapewriter._shapes.append(converted_shape)
# add a list of attributes to go along with the shape
shapewriter.record(["empty record"])
# save it
shapewriter.save("test_shapelytopyshp.shp")
Karim Bahgat
fonte
5

A resposta de Karim é bastante antiga, mas eu usei o código dele e queria agradecer a ele por isso. Uma pequena coisa que descobri usando o código dele: se o tipo de forma for Polígono ou Multipolígono, ele ainda poderá ter várias partes (orifícios no interior). Portanto, parte de seu código deve ser alterada para

elif geoj["type"] == "Polygon":
    index = 0
    points = []
    parts = []
    for eachmulti in geoj["coordinates"]:
        points.extend(eachmulti)
        parts.append(index)
        index += len(eachmulti)
    record.points = points
    record.parts = parts
elif geoj["type"] in ("MultiPolygon", "MultiLineString"):
    index = 0
    points = []
    parts = []
    for polygon in geoj["coordinates"]:
        for part in polygon:
            points.extend(part)
            parts.append(index)
            index += len(part)
    record.points = points
    record.parts = parts
lebenlechzer
fonte