Sono interessato alla larghezza massima di un poligono, ad esempio un lago, in direzione est-ovest. Le scatole di delimitazione aiuteranno solo in poligoni semplici ma non in poligoni concavi complessi.
Sono interessato alla larghezza massima di un poligono, ad esempio un lago, in direzione est-ovest. Le scatole di delimitazione aiuteranno solo in poligoni semplici ma non in poligoni concavi complessi.
Risposte:
Ciò probabilmente richiede alcuni script in qualsiasi piattaforma GIS.
Il metodo più efficiente (asintoticamente) è uno sweep di linea verticale: richiede l'ordinamento dei bordi in base alle loro coordinate y minime e quindi l'elaborazione dei bordi dal basso (minimo y) all'inizio (massimo y), per un O (e * log ( e)) algoritmo quando sono coinvolti i bordi elettronici .
La procedura, sebbene semplice, è sorprendentemente difficile da ottenere in tutti i casi. I poligoni possono essere cattivi: possono avere penzoloni, schegge, buchi, essere disconnessi, avere vertici duplicati, corse di vertici lungo linee rette e avere confini non risolti tra due componenti adiacenti. Ecco un esempio che mostra molte di queste caratteristiche (e altro):
Cercheremo specificamente i segmenti orizzontali di lunghezza massima che si trovano interamente all'interno della chiusura del poligono. Ad esempio, questo elimina il ciondolo tra x = 20 e x = 40 proveniente dal foro tra x = 10 e x = 25. È quindi semplice mostrare che almeno uno dei segmenti orizzontali di massima lunghezza interseca almeno un vertice. (Se ci sono soluzioni che si intersecano senza vertici, si trovano nella parte interna di alcuni parallelogramma delimitata in alto e in basso da soluzioni che fanno si intersecano almeno un vertice. Questo ci dà un mezzo per trovare tutte le soluzioni.)
Di conseguenza, lo sweep di linea deve iniziare con i vertici più bassi e quindi spostarsi verso l'alto (cioè verso valori y più alti) per fermarsi ad ogni vertice. Ad ogni fermata, troviamo eventuali nuovi bordi che emanano verso l'alto da quella elevazione; eliminare eventuali spigoli che terminano dal basso a tale elevazione (questa è una delle idee chiave: semplifica l'algoritmo ed elimina la metà della potenziale elaborazione); ed elaborare con cura tutti i bordi che giacciono interamente ad un'elevazione costante (i bordi orizzontali).
Ad esempio, considerare lo stato quando viene raggiunto un livello di y = 10. Da sinistra a destra, troviamo i seguenti bordi:
x.min x.max y.min y.max
[1,] 10 0 0 30
[2,] 10 24 10 20
[3,] 20 24 10 20
[4,] 20 40 10 10
[5,] 40 20 10 10
[6,] 60 0 5 30
[7,] 60 60 5 30
[8,] 60 70 5 20
[9,] 60 70 5 15
[10,] 90 100 10 40
In questa tabella, (x.min, y.min) sono le coordinate dell'endpoint inferiore del bordo e (x.max, y.max) sono le coordinate del suo endpoint superiore. A questo livello (y = 10), il primo bordo viene intercettato al suo interno, il secondo viene intercettato nella parte inferiore e così via. Alcuni bordi che terminano a questo livello, come da (10,0) a (10,10), non sono inclusi nell'elenco.
Per determinare dove si trovano i punti interni e quelli esterni, immagina di partire dall'estrema sinistra - che ovviamente si trova all'esterno del poligono - e di spostarti orizzontalmente verso destra. Ogni volta che attraversiamo un bordo che non è orizzontale , alterniamo alternativamente da esterno a interno e posteriore. (Questa è un'altra idea chiave.) Tuttavia, tutti i punti all'interno di qualsiasi bordo orizzontale sono determinati ad essere all'interno del poligono, non importa quale. (La chiusura di un poligono include sempre i suoi bordi.)
Continuando l'esempio, ecco l'elenco ordinato di coordinate x in cui i bordi non orizzontali iniziano o attraversano la linea y = 10:
x.array 6.7 10 20 48 60 63.3 65 90
interior 1 0 1 0 1 0 1 0
(Si noti che x = 40 non è in questo elenco.) I valori interior
dell'array indicano gli endpoint di sinistra dei segmenti interni: 1 indica un intervallo interno, 0 un intervallo esterno. Pertanto, il primo 1 indica che l'intervallo da x = 6,7 a x = 10 è all'interno del poligono. Il prossimo 0 indica che l'intervallo da x = 10 a x = 20 è esterno al poligono. E così procede: l'array identifica quattro intervalli separati come all'interno del poligono.
Alcuni di questi intervalli, come quello da x = 60 a x = 63.3, non intersecano alcun vertice: un rapido controllo contro le coordinate x di tutti i vertici con y = 10 elimina tali intervalli.
Durante la scansione possiamo monitorare le lunghezze di questi intervalli, conservando i dati relativi agli intervalli di lunghezza massima trovati finora.
Nota alcune delle implicazioni di questo approccio. Un vertice a forma di "v", quando incontrato, è l'origine di due bordi. Pertanto quando si incrociano si verificano due interruttori. Questi interruttori si annullano. Qualsiasi "v" capovolta non viene nemmeno elaborata, poiché entrambi i suoi bordi vengono eliminati prima di iniziare la scansione da sinistra a destra. In entrambi i casi, tale vertice non blocca un segmento orizzontale.
Più di due spigoli possono condividere un vertice: questo è illustrato in (10,0), (60,5), (25, 20) e - sebbene sia difficile da dire - in (20,10) e (40 , 10). (Questo perché il ciondolo va (20,10) -> (40,10) -> (40,0) -> (40, -50) -> (40, 10) -> (20, 10) Notate come il vertice in (40,0) sia anche all'interno di un altro bordo ... è brutto.) Questo algoritmo gestisce bene quelle situazioni.
Una situazione difficile è illustrata in fondo: ci sono le coordinate x dei segmenti non orizzontali
30, 50
Questo fa sì che tutto a sinistra di x = 30 sia considerato esterno, tutto tra 30 e 50 sia interno e tutto dopo 50 sia di nuovo esterno. Il vertice in x = 40 non è mai nemmeno considerato in questo algoritmo.
Ecco come appare il poligono alla fine della scansione. Mostro tutti gli intervalli interni contenenti vertici in grigio scuro, tutti gli intervalli di lunghezza massima in rosso e coloro i vertici secondo le loro coordinate y. L'intervallo massimo è di 64 unità.
Gli unici calcoli geometrici coinvolti sono calcolare dove i bordi si intersecano con le linee orizzontali: questa è una semplice interpolazione lineare. I calcoli sono anche necessari per determinare quali segmenti interni contengono vertici: si tratta di determinazioni di intermittenza , facilmente calcolabili con un paio di disuguaglianze. Questa semplicità rende l'algoritmo solido e appropriato sia per le rappresentazioni di coordinate in virgola mobile che in virgola mobile.
Se le coordinate sono geografiche , le linee orizzontali si trovano realmente su cerchi di latitudine. Le loro lunghezze non sono difficili da calcolare: basta moltiplicare le loro lunghezze euclidee per il coseno della loro latitudine (in un modello sferico). Pertanto questo algoritmo si adatta perfettamente alle coordinate geografiche. (Per gestire l'avvolgimento attorno al pozzo del meridiano + -180, potrebbe essere necessario prima trovare una curva dal polo sud al polo nord che non passi attraverso il poligono. Dopo aver riespresso tutte le coordinate x come spostamenti orizzontali rispetto a quello curva, questo algoritmo troverà correttamente il segmento orizzontale massimo.)
Di seguito è riportato il R
codice implementato per eseguire i calcoli e creare le illustrazioni.
#
# Plotting functions.
#
points.polygon <- function(p, ...) {
points(p$v, ...)
}
plot.polygon <- function(p, ...) {
apply(p$e, 1, function(e) lines(matrix(e[c("x.min", "x.max", "y.min", "y.max")], ncol=2), ...))
}
expand <- function(bb, e=1) {
a <- matrix(c(e, 0, 0, e), ncol=2)
origin <- apply(bb, 2, mean)
delta <- origin %*% a - origin
t(apply(bb %*% a, 1, function(x) x - delta))
}
#
# Convert polygon to a better data structure.
#
# A polygon class has three attributes:
# v is an array of vertex coordinates "x" and "y" sorted by increasing y;
# e is an array of edges from (x.min, y.min) to (x.max, y.max) with y.max >= y.min, sorted by y.min;
# bb is its rectangular extent (x0,y0), (x1,y1).
#
as.polygon <- function(p) {
#
# p is a list of linestrings, each represented as a sequence of 2-vectors
# with coordinates in columns "x" and "y".
#
f <- function(p) {
g <- function(i) {
v <- p[(i-1):i, ]
v[order(v[, "y"]), ]
}
sapply(2:nrow(p), g)
}
vertices <- do.call(rbind, p)
edges <- t(do.call(cbind, lapply(p, f)))
colnames(edges) <- c("x.min", "x.max", "y.min", "y.max")
#
# Sort by y.min.
#
vertices <- vertices[order(vertices[, "y"]), ]
vertices <- vertices[!duplicated(vertices), ]
edges <- edges[order(edges[, "y.min"]), ]
# Maintaining an extent is useful.
bb <- apply(vertices <- vertices[, c("x","y")], 2, function(z) c(min(z), max(z)))
# Package the output.
l <- list(v=vertices, e=edges, bb=bb); class(l) <- "polygon"
l
}
#
# Compute the maximal horizontal interior segments of a polygon.
#
fetch.x <- function(p) {
#
# Update moves the line from the previous level to a new, higher level, changing the
# state to represent all edges originating or strictly passing through level `y`.
#
update <- function(y) {
if (y > state$level) {
state$level <<- y
#
# Remove edges below the new level from state$current.
#
current <- state$current
current <- current[current[, "y.max"] > y, ]
#
# Adjoin edges at this level.
#
i <- state$i
while (i <= nrow(p$e) && p$e[i, "y.min"] <= y) {
current <- rbind(current, p$e[i, ])
i <- i+1
}
state$i <<- i
#
# Sort the current edges by x-coordinate.
#
x.coord <- function(e, y) {
if (e["y.max"] > e["y.min"]) {
((y - e["y.min"]) * e["x.max"] + (e["y.max"] - y) * e["x.min"]) / (e["y.max"] - e["y.min"])
} else {
min(e["x.min"], e["x.max"])
}
}
if (length(current) > 0) {
x.array <- apply(current, 1, function(e) x.coord(e, y))
i.x <- order(x.array)
current <- current[i.x, ]
x.array <- x.array[i.x]
#
# Scan and mark each interval as interior or exterior.
#
status <- FALSE
interior <- numeric(length(x.array))
for (i in 1:length(x.array)) {
if (current[i, "y.max"] == y) {
interior[i] <- TRUE
} else {
status <- !status
interior[i] <- status
}
}
#
# Simplify the data structure by retaining the last value of `interior`
# within each group of common values of `x.array`.
#
interior <- sapply(split(interior, x.array), function(i) rev(i)[1])
x.array <- sapply(split(x.array, x.array), function(i) i[1])
print(y)
print(current)
print(rbind(x.array, interior))
markers <- c(1, diff(interior))
intervals <- x.array[markers != 0]
#
# Break into a list structure.
#
if (length(intervals) > 1) {
if (length(intervals) %% 2 == 1)
intervals <- intervals[-length(intervals)]
blocks <- 1:length(intervals) - 1
blocks <- blocks - (blocks %% 2)
intervals <- split(intervals, blocks)
} else {
intervals <- list()
}
} else {
intervals <- list()
}
#
# Update the state.
#
state$current <<- current
}
list(y=y, x=intervals)
} # Update()
process <- function(intervals, x, y) {
# intervals is a list of 2-vectors. Each represents the endpoints of
# an interior interval of a polygon.
# x is an array of x-coordinates of vertices.
#
# Retains only the intervals containing at least one vertex.
between <- function(i) {
1 == max(mapply(function(a,b) a && b, i[1] <= x, x <= i[2]))
}
is.good <- lapply(intervals$x, between)
list(y=y, x=intervals$x[unlist(is.good)])
#intervals
}
#
# Group the vertices by common y-coordinate.
#
vertices.x <- split(p$v[, "x"], p$v[, "y"])
vertices.y <- lapply(split(p$v[, "y"], p$v[, "y"]), max)
#
# The "state" is a collection of segments and an index into edges.
# It will updated during the vertical line sweep.
#
state <- list(level=-Inf, current=c(), i=1, x=c(), interior=c())
#
# Sweep vertically from bottom to top, processing the intersection
# as we go.
#
mapply(function(x,y) process(update(y), x, y), vertices.x, vertices.y)
}
scale <- 10
p.raw = list(scale * cbind(x=c(0:10,7,6,0), y=c(3,0,0,-1,-1,-1,0,-0.5,0.75,1,4,1.5,0.5,3)),
scale *cbind(x=c(1,1,2.4,2,4,4,4,4,2,1), y=c(0,1,2,1,1,0,-0.5,1,1,0)),
scale *cbind(x=c(6,7,6,6), y=c(.5,2,3,.5)))
#p.raw = list(cbind(x=c(0,2,1,1/2,0), y=c(0,0,2,1,0)))
#p.raw = list(cbind(x=c(0, 35, 100, 65, 0), y=c(0, 50, 100, 50, 0)))
p <- as.polygon(p.raw)
results <- fetch.x(p)
#
# Find the longest.
#
dx <- matrix(unlist(results["x", ]), nrow=2)
length.max <- max(dx[2,] - dx[1,])
#
# Draw pictures.
#
segment.plot <- function(s, length.max, colors, ...) {
lapply(s$x, function(x) {
col <- ifelse (diff(x) >= length.max, colors[1], colors[2])
lines(x, rep(s$y,2), col=col, ...)
})
}
gray <- "#f0f0f0"
grayer <- "#d0d0d0"
plot(expand(p$bb, 1.1), type="n", xlab="x", ylab="y", main="After the Scan")
sapply(1:length(p.raw), function(i) polygon(p.raw[[i]], col=c(gray, "White", grayer)[i]))
apply(results, 2, function(s) segment.plot(s, length.max, colors=c("Red", "#b8b8a8"), lwd=4))
plot(p, col="Black", lty=3)
points(p, pch=19, col=round(2 + 2*p$v[, "y"]/scale, 0))
points(p, cex=1.25)
Ecco una soluzione basata su raster. È veloce (ho fatto tutto il lavoro dall'inizio alla fine in 14 minuti), non richiede script, richiede solo poche operazioni ed è ragionevolmente preciso.
Inizia con una rappresentazione raster del poligono. Questo utilizza una griglia di 550 righe e 1200 colonne:
In questa rappresentazione, le celle grigie (interne) hanno il valore 1 e tutte le altre celle sono NoData.
Calcola l'accumulo di flusso nella direzione da ovest a est utilizzando i valori della cella unitaria per la griglia del peso (quantità di "pioggia"):
Il basso accumulo è scuro, aumentando ai massimi accumuli nel giallo brillante.
Un massimo zonale (usando il poligono per la griglia e l'accumulo di flusso per i valori) identifica le celle in cui il flusso è maggiore. Per mostrarli, ho dovuto ingrandire verso l'angolo in basso a destra:
I globuli rossi segnano le estremità dei più alti accumuli di flusso: sono i punti finali più a destra dei segmenti interni di lunghezza massima del poligono.
Per trovare questi segmenti, posiziona tutto il peso sui globuli rossi ed esegui il flusso all'indietro!
La striscia rossa vicino al fondo segna due file di celle: al suo interno si trova il segmento orizzontale di massima lunghezza. Utilizzare questa rappresentazione così com'è per ulteriori analisi o convertirla in una forma polilinea (o poligono).
Si è verificato un errore di discretizzazione con una rappresentazione raster. Può essere ridotto aumentando la risoluzione, a un certo costo nei tempi di calcolo.
Un aspetto davvero interessante di questo approccio è che in genere troviamo valori estremi delle cose come parte di un flusso di lavoro più ampio in cui è necessario raggiungere alcuni obiettivi: ubicazione di un oleodotto o di un campo di calcio, creazione di riserve ecologiche e così via. Il processo prevede compromessi. Pertanto, la linea orizzontale più lunga potrebbe non far parte di una soluzione ottimale. Potremmo cura invece di sapere dove quasi linee più lunghe sarebbe mentire. Questo è semplice: anziché selezionare il flusso massimo zonale, selezionare tutte le celle vicine al massimo zonale. In questo esempio, il massimo zonale equivale a 744 (il numero di colonne attraversate dal segmento interno più lungo). Invece, selezioniamo tutte le celle entro il 5% di quel massimo:
Il flusso da est a ovest produce questa raccolta di segmenti orizzontali:
Questa è una mappa dei luoghi in cui l'estensione est-ovest ininterrotta è del 95% o maggiore dell'estensione massima est-ovest in qualsiasi punto del poligono.
Ok. Ho un'altra (migliore) idea ( idea-№2 ). Ma suppongo che sia meglio essere realizzato come uno script Python, non come un SQL Querry. Anche in questo caso è il caso comune, non solo EW.
Avrai bisogno di un rettangolo di selezione per il poligono e un azimut (A) come direzione di misurazione. Supponiamo che la lunghezza dei bordi BBox sia LA e LB. Massima distanza possibile (MD) all'interno di un poligono è: MB = (LA^2 * LB^2)^(1/2)
, quindi alla ricerca di valore (V) non è più grande di MB: V <= MB
.
Non sono sicuro che la risposta di Fetzer sia ciò che vuoi fare, ma è così che st_box2d potrebbe fare il lavoro.
L'idea di SS_Rebelious N ° 1 funzionerà in molti casi, ma non per alcuni poligoni concavi.
Penso che sia necessario creare linee lw artificiali i cui punti seguono i bordi quando le linee create dai vertici attraversano i bordi del poligono se esiste una possibilità di linea est-ovest.
Per questo potresti provare a creare un poligono a 4 nodi in cui la lunghezza della linea è alta, fare il poligono P * che è la precedente sovrapposizione con il poligono originale e vedere se min (y1) e max (y2) lasciano una linea x possibilità. (dove y1 è l'insieme di punti tra la cornetta in alto a sinistra e l'angolo in alto a destra e y2 l'insieme di y tra gli angoli in basso a sinistra e in basso a destra del poligono a 4 nodi). Non è così facile, spero che troverai strumenti psql per aiutarti!
Ho un'idea-№1 ( Modifica: per il caso comune, non solo la direzione EW, e con alcune limitazioni che sono descritte nei commenti). Non fornirò il codice, solo un concetto. La "direzione x" è in realtà un azimut, che viene calcolato da ST_Azimuth. I passaggi proposti sono:
Dai un'occhiata alla mia domanda e alla risposta di Evil Genius.
Spero che il tuo poligono del lago abbia un numero di punti, puoi creare linee su questi punti con un azimut (aspetto, direzione geografica). Scegli la lunghezza delle linee abbastanza grande (parte ST_MakePoint), in modo da poter calcolare la linea più corta tra le due linee più distanti.
Ecco un esempio:
L'esempio mostra la larghezza massima del poligono. Ho scelto ST_ShortestLine (linea rossa) per questo approccio. ST_MakeLine aumenterebbe il valore (linea blu) e il punto finale della linea (in basso a sinistra) colpirebbe la linea blu del poligono. Devi calcolare la distanza con i centroidi delle linee create (di aiuto).
Un'idea per poligoni irregolari o concavi per questo approccio. Potrebbe essere che devi intersecare il poligono con un raster.