Created
April 27, 2025 09:43
-
-
Save pigreco/4f55d34207c4433a09a2d3105f1407c8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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