Skip to content

Instantly share code, notes, and snippets.

@pigreco
Created April 27, 2025 09:43
Show Gist options
  • Save pigreco/4f55d34207c4433a09a2d3105f1407c8 to your computer and use it in GitHub Desktop.
Save pigreco/4f55d34207c4433a09a2d3105f1407c8 to your computer and use it in GitHub Desktop.
from qgis.gui import QgsMapToolEmitPoint, QgsMapCanvas, QgsRubberBand#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Script PyQGIS per generare un frattale tipo "fiocco di neve di Koch" a partire
da una linea o poligono disegnato dall'utente. Il processo divide ogni lato in tre parti
e trasforma la parte centrale in un triangolo equilatero.
Il frattale può essere generato verso l'interno o verso l'esterno della geometria.
Supporta sia linee che poligoni e può visualizzare tutte le iterazioni insieme.
"""
from qgis.core import (
QgsProject, QgsGeometry, QgsFeature, QgsVectorLayer,
QgsPointXY, QgsWkbTypes, QgsApplication, QgsFields,
QgsField, QgsGraduatedSymbolRenderer, QgsClassificationEqualInterval,
QgsStyle, QgsGradientColorRamp, QgsSymbol
)
from qgis.PyQt.QtWidgets import QMessageBox, QInputDialog, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QSpinBox, QPushButton, QCheckBox
from qgis.PyQt.QtCore import Qt, QPoint, QVariant
from qgis.PyQt.QtGui import QColor
import math
import processing
class FractalOptionsDialog(QDialog):
"""Dialog for setting fractal generation options"""
def __init__(self, parent=None):
super(FractalOptionsDialog, self).__init__(parent)
self.setWindowTitle("Opzioni Frattale")
self.iterations = 1
self.direction = "esterno" # Default: esterno
self.geometry_type = "linea" # Default: linea
self.show_all_iterations = True # Default: mostra tutte le iterazioni
# Create layout
layout = QVBoxLayout()
# Geometry type selection
geom_layout = QHBoxLayout()
geom_label = QLabel("Tipo di geometria:")
self.geom_combo = QComboBox()
self.geom_combo.addItem("Linea", "linea")
self.geom_combo.addItem("Poligono", "poligono")
geom_layout.addWidget(geom_label)
geom_layout.addWidget(self.geom_combo)
# Iterations selection
iter_layout = QHBoxLayout()
iter_label = QLabel("Numero di iterazioni:")
self.iter_spin = QSpinBox()
self.iter_spin.setMinimum(1)
self.iter_spin.setMaximum(10)
self.iter_spin.setValue(1)
iter_layout.addWidget(iter_label)
iter_layout.addWidget(self.iter_spin)
# Show all iterations checkbox
all_iter_layout = QHBoxLayout()
self.all_iter_check = QCheckBox("Mostra tutte le iterazioni")
self.all_iter_check.setChecked(True)
all_iter_layout.addWidget(self.all_iter_check)
# Direction selection
dir_layout = QHBoxLayout()
dir_label = QLabel("Direzione del frattale:")
self.dir_combo = QComboBox()
self.dir_combo.addItem("Esterno", "esterno")
self.dir_combo.addItem("Interno", "interno")
dir_layout.addWidget(dir_label)
dir_layout.addWidget(self.dir_combo)
# OK/Cancel buttons
btn_layout = QHBoxLayout()
ok_button = QPushButton("OK")
cancel_button = QPushButton("Annulla")
ok_button.clicked.connect(self.accept)
cancel_button.clicked.connect(self.reject)
btn_layout.addWidget(ok_button)
btn_layout.addWidget(cancel_button)
# Add layouts to main layout
layout.addLayout(geom_layout)
layout.addLayout(iter_layout)
layout.addLayout(all_iter_layout)
layout.addLayout(dir_layout)
layout.addLayout(btn_layout)
self.setLayout(layout)
def get_values(self):
self.iterations = self.iter_spin.value()
self.direction = self.dir_combo.currentData()
self.geometry_type = self.geom_combo.currentData()
self.show_all_iterations = self.all_iter_check.isChecked()
return self.iterations, self.direction, self.geometry_type, self.show_all_iterations
class FrattaleKochTool:
def __init__(self, iface):
self.iface = iface
self.canvas = iface.mapCanvas()
self.points = []
self.rubber_band = None
self.map_tool = QgsMapToolEmitPoint(self.canvas)
self.map_tool.canvasClicked.connect(self.on_canvas_clicked)
self.iterations = 1
self.direction = "esterno" # Default: esterno
self.geometry_type = "linea" # Default: linea
self.show_all_iterations = True # Default: mostra tutte le iterazioni
self.result_layer = None
self.drawing_layer = None
def activate(self):
# Crea un layer temporaneo per il disegno se non esiste già
if not self.drawing_layer:
# Il tipo di layer dipenderà dalla scelta dell'utente
self.drawing_layer = QgsVectorLayer("LineString", "Input_Geometria", "memory")
QgsProject.instance().addMapLayer(self.drawing_layer)
# Resetta lo strumento di disegno
self.points = []
self.reset_rubber_band()
self.canvas.setMapTool(self.map_tool)
QMessageBox.information(None, "Istruzioni",
"Clicca sulla mappa per aggiungere vertici alla geometria.\n"
"Fai clic con il tasto destro per completare il disegno.")
def reset_rubber_band(self):
# Rimuovi il rubber band esistente se c'è
if self.rubber_band:
self.rubber_band.reset()
# Crea un nuovo rubber band
self.rubber_band = QgsRubberBand(
self.canvas,
QgsWkbTypes.LineGeometry # Inizia sempre come linestring
)
self.rubber_band.setColor(QColor(255, 0, 0, 100))
self.rubber_band.setWidth(2)
def on_canvas_clicked(self, point, button):
if button == Qt.LeftButton:
self.points.append(QgsPointXY(point))
# Aggiorna il rubber band per mostrare la geometria corrente
if len(self.points) > 1:
self.rubber_band.reset(QgsWkbTypes.LineGeometry)
for pt in self.points:
self.rubber_band.addPoint(pt)
elif button == Qt.RightButton and len(self.points) >= 2: # Almeno 2 punti per una linea
# Apri la finestra di opzioni
dlg = FractalOptionsDialog()
if dlg.exec_():
self.iterations, self.direction, self.geometry_type, self.show_all_iterations = dlg.get_values()
# Se è stato selezionato il tipo poligono, chiudi la geometria
if self.geometry_type == "poligono":
# Aggiungi il punto iniziale alla fine per chiudere il poligono
if self.points[0] != self.points[-1]:
self.points.append(self.points[0])
self.create_koch_fractal()
else:
# L'utente ha annullato, cancella il rubber band
self.reset_rubber_band()
self.points = []
def iterate_koch_fractal(self, geometry):
"""
Esegue un'iterazione del frattale di Koch sulla geometria data.
Divide ogni segmento in tre parti e sostituisce la parte centrale con un triangolo equilatero.
Funziona sia con linee che con poligoni.
"""
if not geometry:
return None
if geometry.type() == QgsWkbTypes.PolygonGeometry:
# Caso poligono
polygon = geometry.asPolygon()[0]
points = polygon
is_polygon = True
else:
# Caso linestring
linestring = geometry.asPolyline()
points = linestring
is_polygon = False
# Lista per i nuovi punti
new_points = []
# Processa ogni segmento della geometria
for i in range(len(points) - 1):
p1 = points[i]
p2 = points[i + 1]
# Aggiungi il primo punto
new_points.append(p1)
# Calcola i punti per dividere il lato in tre parti
dx = (p2.x() - p1.x()) / 3.0
dy = (p2.y() - p1.y()) / 3.0
# Primo terzo
p_1_3 = QgsPointXY(p1.x() + dx, p1.y() + dy)
new_points.append(p_1_3)
# Punto per il triangolo equilatero
# Calcola l'angolo del lato
angle = math.atan2(dy, dx)
# Decidi l'orientamento del triangolo in base alla direzione specificata
if self.direction == "esterno":
# Ruota di 60 gradi (pi/3 radianti) per il triangolo equilatero esterno
triangle_angle = angle + math.pi/3
else: # interno
# Ruota di -60 gradi (-pi/3 radianti) per il triangolo equilatero interno
triangle_angle = angle - math.pi/3
# Lunghezza di un terzo del lato
length = math.sqrt(dx*dx + dy*dy)
# Punto per il triangolo equilatero
p_triangle = QgsPointXY(
p_1_3.x() + length * math.cos(triangle_angle),
p_1_3.y() + length * math.sin(triangle_angle)
)
new_points.append(p_triangle)
# Secondo terzo
p_2_3 = QgsPointXY(p1.x() + 2*dx, p1.y() + 2*dy)
new_points.append(p_2_3)
# Per il caso linea, aggiungi l'ultimo punto
if not is_polygon:
new_points.append(points[-1])
# Crea la geometria appropriata
if is_polygon:
# Chiudi il poligono
if new_points[0] != new_points[-1]:
new_points.append(new_points[0])
return QgsGeometry.fromPolygonXY([new_points])
else:
return QgsGeometry.fromPolylineXY(new_points)
def create_koch_fractal(self):
# Crea la geometria iniziale in base al tipo scelto
if self.geometry_type == "poligono":
initial_geom = QgsGeometry.fromPolygonXY([self.points])
layer_type = "Polygon"
else: # linea
initial_geom = QgsGeometry.fromPolylineXY(self.points)
layer_type = "LineString"
# Salva la geometria iniziale sul layer di disegno
self.drawing_layer.dataProvider().truncate() # Rimuovi eventuali geometrie esistenti
init_feature = QgsFeature()
init_feature.setGeometry(initial_geom)
self.drawing_layer.dataProvider().addFeature(init_feature)
self.drawing_layer.updateExtents()
self.drawing_layer.triggerRepaint()
# Crea un nuovo layer per il risultato
direction_str = "Esterno" if self.direction == "esterno" else "Interno"
geom_str = "Poligono" if self.geometry_type == "poligono" else "Linea"
iterations_str = "Tutte" if self.show_all_iterations else str(self.iterations)
self.result_layer = QgsVectorLayer(layer_type,
f"Frattale_Koch_{geom_str}_Iter{iterations_str}_{direction_str}",
"memory")
# Aggiungi un campo per mostrare il numero di iterazioni
provider = self.result_layer.dataProvider()
provider.addAttributes([QgsField("iterazione", QVariant.Int)])
self.result_layer.updateFields()
# Se l'utente ha scelto di mostrare tutte le iterazioni
if self.show_all_iterations:
# Aggiungi la geometria iniziale (iterazione 0)
feature = QgsFeature(self.result_layer.fields())
feature.setGeometry(initial_geom)
feature.setAttribute("iterazione", 0)
provider.addFeature(feature)
# Elabora ogni iterazione e aggiungi il risultato al layer
current_geom = initial_geom
for i in range(1, self.iterations + 1):
current_geom = self.iterate_koch_fractal(current_geom)
feature = QgsFeature(self.result_layer.fields())
feature.setGeometry(current_geom)
feature.setAttribute("iterazione", i)
provider.addFeature(feature)
else:
# Elabora solo l'iterazione finale richiesta
current_geom = initial_geom
for i in range(self.iterations):
current_geom = self.iterate_koch_fractal(current_geom)
# Aggiungi solo il risultato finale
feature = QgsFeature(self.result_layer.fields())
feature.setGeometry(current_geom)
feature.setAttribute("iterazione", self.iterations)
provider.addFeature(feature)
# Aggiungi il layer al progetto
QgsProject.instance().addMapLayer(self.result_layer)
# Imposta la simbologia per visualizzare diverse iterazioni con colori diversi
if self.show_all_iterations:
self.apply_graduated_symbology()
# Pulisci il rubber band
self.reset_rubber_band()
self.points = []
self.canvas.unsetMapTool(self.map_tool)
# Zoom al risultato
self.canvas.setExtent(self.result_layer.extent())
self.canvas.refresh()
# Messaggio di completamento
iterations_msg = "tutte le iterazioni da 0 a" if self.show_all_iterations else "l'iterazione"
QMessageBox.information(None, "Completato",
f"Frattale di Koch creato con successo!\n"
f"- Tipo: {geom_str}\n"
f"- Creato {iterations_msg} {self.iterations}\n"
f"- Direzione: {direction_str.lower()}")
def apply_graduated_symbology(self):
"""Applica una simbologia graduata basata sul campo iterazione"""
if not self.result_layer:
return
# Configura il renderer per utilizzare colori graduati basati sul valore di iterazione
field_name = "iterazione"
# Crea una rampa di colori
color_ramp = QgsStyle.defaultStyle().colorRamp("Spectral")
if not color_ramp: # Fallback su una rampa predefinita se Spectral non è disponibile
color_ramp = QgsGradientColorRamp(QColor(255, 0, 0), QColor(0, 0, 255))
# Crea il renderer graduato
renderer = QgsGraduatedSymbolRenderer(field_name, [])
renderer.setClassAttribute(field_name)
renderer.setSourceColorRamp(color_ramp)
# Imposta il metodo di classificazione e genera le classi
renderer.setClassificationMethod(QgsClassificationEqualInterval())
renderer.updateClasses(self.result_layer, self.iterations + 1) # +1 per includere l'iterazione 0
# Applica il renderer al layer
self.result_layer.setRenderer(renderer)
self.result_layer.triggerRepaint()
# Variabile globale per mantenere un riferimento attivo allo strumento
koch_fractal_tool = None
def run_script():
global koch_fractal_tool
# Se lo strumento è già attivo, resettalo
if koch_fractal_tool is not None:
# Rimuovi eventuali rubber band esistenti
if koch_fractal_tool.rubber_band:
koch_fractal_tool.rubber_band.reset()
# Crea una nuova istanza dello strumento
koch_fractal_tool = FrattaleKochTool(iface)
koch_fractal_tool.activate()
# Notifica all'utente che lo strumento è pronto
iface.messageBar().pushMessage(
"Strumento Frattale di Koch",
"Lo strumento è attivo! Disegna una linea o un poligono sulla mappa.",
level=0, duration=5
)
# Esegui lo script se viene eseguito direttamente
if __name__ == "__main__":
run_script()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment