Created
February 6, 2026 20:03
-
-
Save dinizime/838af78536ed5965a3a2877c98a7c3bd to your computer and use it in GitHub Desktop.
Cria linha passando pelos rótulos curvados de forma precisa
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
| """ | |
| ============================================================================ | |
| Label Line Extractor para QGIS | |
| --------------------------------------------------------------------------- | |
| Itera cada polígono de uma camada de referência, renderiza off-screen, | |
| e captura todos os rótulos de LINHA. Gera uma linha por rótulo com | |
| comprimento exato do texto, seguindo a geometria da feição original. | |
| - Labels "on line": linha coincide com a feição | |
| - Labels "above/below line": linha é deslocada (offset) à distância | |
| do centro do rótulo | |
| Saída: | |
| • Camada "Linhas_Rotulos" – uma linha por rótulo | |
| Uso: Console Python do QGIS (Ctrl+Alt+P) ou Editor de Scripts | |
| Requisitos: QGIS 3.28+ | |
| ============================================================================ | |
| """ | |
| from qgis.core import ( | |
| QgsProject, QgsVectorLayer, QgsFeature, QgsField, | |
| QgsGeometry, QgsPointXY, QgsRectangle, | |
| QgsMapSettings, QgsMapRendererSequentialJob, | |
| QgsCoordinateTransform, QgsUnitTypes, | |
| QgsLineSymbol, QgsSingleSymbolRenderer, | |
| QgsWkbTypes, Qgis, | |
| ) | |
| from qgis.PyQt.QtCore import QVariant, QSize | |
| from qgis.PyQt.QtGui import QFont, QFontMetricsF, QColor | |
| import math | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # ★ CONFIGURAÇÃO ★ | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| POLYGON_LAYER_NAME = "aux_moldura_a" | |
| OUTPUT_LAYER_NAME = "Linhas_Rotulos" | |
| RENDER_SCALE = 25000 # None = escala do canvas atual | |
| BBOX_MARGIN = 0.0 | |
| MAX_PX_PER_SIDE = 16384 | |
| # Tolerância (em map units) para considerar label "on line" vs "above/below" | |
| ON_LINE_TOLERANCE_FACTOR = 0.5 # fração da altura do label | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Extração das propriedades de fonte de uma camada | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| class LabelFontInfo: | |
| """Armazena todas as propriedades relevantes do texto de uma camada.""" | |
| def __init__(self, layer, mu_per_px, dpi): | |
| self.valid = False | |
| self.font = QFont("Sans Serif", 10) | |
| self.letter_spacing = 0.0 | |
| self.word_spacing = 0.0 | |
| self.buffer_size_mu = 0.0 | |
| self.mu_per_px = mu_per_px | |
| self.dpi = dpi | |
| # Placement flags: on line vs above/below | |
| self.is_on_line = True | |
| self.is_above_line = False | |
| self.is_below_line = False | |
| if layer is None or not layer.labelsEnabled(): | |
| return | |
| labeling = layer.labeling() | |
| if labeling is None: | |
| return | |
| try: | |
| settings = labeling.settings() | |
| except Exception: | |
| return | |
| text_format = settings.format() | |
| # ── Placement flags (on line / above / below) ── | |
| try: | |
| # QGIS 3.16+: lineSettings().placementFlags() | |
| line_settings = settings.lineSettings() | |
| flags = line_settings.placementFlags() | |
| # Flags são bit flags: OnLine=1, AboveLine=2, BelowLine=4 | |
| try: | |
| from qgis.core import QgsLabeling | |
| self.is_on_line = bool(flags & QgsLabeling.LinePlacementFlag.OnLine) | |
| self.is_above_line = bool(flags & QgsLabeling.LinePlacementFlag.AboveLine) | |
| self.is_below_line = bool(flags & QgsLabeling.LinePlacementFlag.BelowLine) | |
| except (ImportError, AttributeError): | |
| # Fallback: raw int flags | |
| self.is_on_line = bool(flags & 1) | |
| self.is_above_line = bool(flags & 2) | |
| self.is_below_line = bool(flags & 4) | |
| except AttributeError: | |
| try: | |
| # QGIS < 3.16 fallback | |
| flags = settings.placementFlags | |
| self.is_on_line = bool(flags & 1) | |
| self.is_above_line = bool(flags & 2) | |
| self.is_below_line = bool(flags & 4) | |
| except Exception: | |
| self.is_on_line = True | |
| # ── Construir QFont completo ── | |
| base_font = text_format.font() | |
| font_size = text_format.size() | |
| size_unit = text_format.sizeUnit() | |
| if size_unit == QgsUnitTypes.RenderPoints: | |
| font_size_px = font_size * dpi / 72.0 | |
| elif size_unit == QgsUnitTypes.RenderMillimeters: | |
| font_size_px = font_size * dpi / 25.4 | |
| elif size_unit == QgsUnitTypes.RenderPixels: | |
| font_size_px = font_size | |
| elif size_unit == QgsUnitTypes.RenderMapUnits: | |
| font_size_px = font_size / mu_per_px if mu_per_px > 0 else font_size | |
| else: | |
| font_size_px = font_size * dpi / 72.0 | |
| f = QFont(base_font.family()) | |
| f.setPixelSize(max(1, int(round(font_size_px)))) | |
| f.setBold(base_font.bold()) | |
| f.setItalic(base_font.italic()) | |
| f.setWeight(base_font.weight()) | |
| if hasattr(base_font, 'styleName') and base_font.styleName(): | |
| f.setStyleName(base_font.styleName()) | |
| stretch = base_font.stretch() | |
| if stretch > 0: | |
| f.setStretch(stretch) | |
| f.setCapitalization(base_font.capitalization()) | |
| f.setStrikeOut(base_font.strikeOut()) | |
| f.setUnderline(base_font.underline()) | |
| # ── Letter spacing ── | |
| letter_sp = base_font.letterSpacing() | |
| ls_type = base_font.letterSpacingType() | |
| if ls_type == QFont.PercentageSpacing: | |
| fm_temp = QFontMetricsF(f) | |
| avg_w = fm_temp.averageCharWidth() | |
| letter_sp_abs = avg_w * (letter_sp - 100.0) / 100.0 | |
| else: | |
| letter_sp_abs = letter_sp | |
| word_sp = base_font.wordSpacing() | |
| f.setLetterSpacing(QFont.AbsoluteSpacing, letter_sp_abs) | |
| f.setWordSpacing(word_sp) | |
| self.font = f | |
| self.letter_spacing = letter_sp_abs | |
| self.word_spacing = word_sp | |
| # ── Buffer (halo) ── | |
| buf = text_format.buffer() | |
| if buf.enabled(): | |
| buf_size = buf.size() | |
| buf_unit = buf.sizeUnit() | |
| if buf_unit == QgsUnitTypes.RenderMillimeters: | |
| buf_px = buf_size * dpi / 25.4 | |
| elif buf_unit == QgsUnitTypes.RenderPoints: | |
| buf_px = buf_size * dpi / 72.0 | |
| elif buf_unit == QgsUnitTypes.RenderPixels: | |
| buf_px = buf_size | |
| elif buf_unit == QgsUnitTypes.RenderMapUnits: | |
| buf_px = buf_size / mu_per_px if mu_per_px > 0 else buf_size | |
| else: | |
| buf_px = buf_size * dpi / 25.4 | |
| self.buffer_size_mu = buf_px * mu_per_px | |
| else: | |
| self.buffer_size_mu = 0.0 | |
| self.valid = True | |
| unit_names = ['Pontos', 'MM', 'MapUnits', 'Pixels', '%', 'Pol'] | |
| placement_str = [] | |
| if self.is_on_line: placement_str.append('OnLine') | |
| if self.is_above_line: placement_str.append('AboveLine') | |
| if self.is_below_line: placement_str.append('BelowLine') | |
| print(f" Fonte: {f.family()}, tamanho={font_size:.1f} " | |
| f"({unit_names[min(size_unit, 5)]})" | |
| f" → {font_size_px:.1f}px, " | |
| f"bold={f.bold()}, italic={f.italic()}, " | |
| f"letter_sp={letter_sp_abs:.2f}, word_sp={word_sp:.2f}, " | |
| f"buffer={self.buffer_size_mu:.4f} u.m., " | |
| f"placement=[{', '.join(placement_str)}]") | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Cálculo do comprimento do label em map units | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def compute_label_length_mu(text, fi): | |
| """ | |
| Calcula o comprimento do texto do rótulo em unidades de mapa. | |
| Usa fm.horizontalAdvance(string_completa) que já incorpora | |
| letter_spacing, word_spacing e kerning configurados na QFont. | |
| """ | |
| if not fi.valid or not text: | |
| return 0.0 | |
| # Aplicar capitalização | |
| cap = fi.font.capitalization() | |
| if cap == QFont.AllUppercase: | |
| display_text = text.upper() | |
| elif cap == QFont.AllLowercase: | |
| display_text = text.lower() | |
| elif cap == QFont.Capitalize: | |
| display_text = text.title() | |
| else: | |
| display_text = text | |
| fm = QFontMetricsF(fi.font) | |
| # horizontalAdvance da string completa: inclui kerning, letter spacing, | |
| # word spacing — tudo que está configurado na QFont | |
| try: | |
| total_advance_px = fm.horizontalAdvance(display_text) | |
| except AttributeError: | |
| total_advance_px = fm.width(display_text) | |
| if total_advance_px <= 0: | |
| return 0.0 | |
| # Converter pixels de fonte → unidades de mapa | |
| label_length_mu = total_advance_px * fi.mu_per_px | |
| return label_length_mu | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Cálculo da altura do label em map units (para tolerância on/above) | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def compute_label_height_mu(fi): | |
| """Altura da linha de texto em map units.""" | |
| if not fi.valid: | |
| return 0.0 | |
| fm = QFontMetricsF(fi.font) | |
| return fm.height() * fi.mu_per_px | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Renderização off-screen | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def render_and_collect_labels(extent, render_layers, project_crs, mu_per_px, dpi): | |
| ext_w = extent.width() | |
| ext_h = extent.height() | |
| if ext_w <= 0 or ext_h <= 0: | |
| return [] | |
| px_w = max(64, int(math.ceil(ext_w / mu_per_px))) | |
| px_h = max(64, int(math.ceil(ext_h / mu_per_px))) | |
| if px_w > MAX_PX_PER_SIDE or px_h > MAX_PX_PER_SIDE: | |
| ratio = min(MAX_PX_PER_SIDE / px_w, MAX_PX_PER_SIDE / px_h) | |
| px_w = max(64, int(px_w * ratio)) | |
| px_h = max(64, int(px_h * ratio)) | |
| settings = QgsMapSettings() | |
| settings.setOutputSize(QSize(px_w, px_h)) | |
| settings.setOutputDpi(dpi) | |
| settings.setDestinationCrs(project_crs) | |
| settings.setExtent(extent) | |
| settings.setLayers(render_layers) | |
| settings.setFlag(QgsMapSettings.UseAdvancedEffects, True) | |
| settings.setFlag(QgsMapSettings.DrawLabeling, True) | |
| job = QgsMapRendererSequentialJob(settings) | |
| job.start() | |
| job.waitForFinished() | |
| labeling_results = job.takeLabelingResults() | |
| if labeling_results is None: | |
| return [] | |
| try: | |
| return labeling_results.allLabels() | |
| except AttributeError: | |
| return [] | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Extrair a parte correta de uma MultiLineString | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def get_closest_singlepart(multi_geom, ref_point): | |
| """ | |
| Se a geometria for MultiLineString, retorna a parte mais próxima | |
| do ponto de referência. Se for simples, retorna ela mesma. | |
| """ | |
| if multi_geom.isMultipart(): | |
| parts = multi_geom.asMultiPolyline() | |
| best_dist = float('inf') | |
| best_geom = None | |
| ref_geom = QgsGeometry.fromPointXY(ref_point) | |
| for part in parts: | |
| g = QgsGeometry.fromPolylineXY(part) | |
| d = g.distance(ref_geom) | |
| if d < best_dist: | |
| best_dist = d | |
| best_geom = g | |
| return best_geom if best_geom else multi_geom | |
| return multi_geom | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Determinar o lado do offset (acima / abaixo) via produto vetorial | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def determine_offset_side(line_geom, label_centroid_pt, center_distance): | |
| """ | |
| Retorna +1 (esquerda / acima) ou -1 (direita / abaixo) com base | |
| na posição do centróide do label em relação à tangente da linha. | |
| """ | |
| line_length = line_geom.length() | |
| if line_length <= 0: | |
| return 1 | |
| # Obter dois pontos ao longo da linha para calcular tangente local | |
| d = center_distance | |
| delta = min(line_length * 0.001, 0.5) # pequeno incremento | |
| d1 = max(0, d - delta) | |
| d2 = min(line_length, d + delta) | |
| p1 = line_geom.interpolate(d1).asPoint() | |
| p2 = line_geom.interpolate(d2).asPoint() | |
| # Vetor tangente | |
| tx = p2.x() - p1.x() | |
| ty = p2.y() - p1.y() | |
| # Vetor: ponto na linha → centróide do label | |
| base_pt = line_geom.interpolate(d).asPoint() | |
| dx = label_centroid_pt.x() - base_pt.x() | |
| dy = label_centroid_pt.y() - base_pt.y() | |
| # Produto vetorial (z-component): tx*dy - ty*dx | |
| cross = tx * dy - ty * dx | |
| return 1 if cross >= 0 else -1 | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Processar um rótulo → feature de linha | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def process_label_to_line(lbl_pos, project, output_layer, font_info_cache, | |
| project_crs, crs_transforms): | |
| """ | |
| Para cada label: | |
| 1. Calcula comprimento do texto em map units | |
| 2. Projeta centróide do labelGeometry na feição de linha | |
| 3. Extrai curveSubstring com comprimento exato | |
| 4. Se acima/abaixo da linha, aplica offsetCurve | |
| Retorna 1 se gerou feature, 0 caso contrário. | |
| """ | |
| layer_id = lbl_pos.layerID | |
| feature_id = lbl_pos.featureId | |
| label_text = lbl_pos.labelText | |
| if not label_text: | |
| return 0 | |
| layer = project.mapLayer(layer_id) | |
| if layer is None: | |
| return 0 | |
| # Verificar se é camada de linhas | |
| if layer.geometryType() != QgsWkbTypes.LineGeometry: | |
| return 0 | |
| layer_name = layer.name() | |
| label_geom = lbl_pos.labelGeometry | |
| if label_geom.isEmpty(): | |
| return 0 | |
| # ── Info da fonte ── | |
| if layer_id not in font_info_cache: | |
| return 0 | |
| fi = font_info_cache[layer_id] | |
| if not fi.valid: | |
| return 0 | |
| # ── Comprimento do label em map units ── | |
| label_length_mu = compute_label_length_mu(label_text, fi) | |
| if label_length_mu <= 0: | |
| return 0 | |
| # ── Obter feição de linha original ── | |
| source_feat = layer.getFeature(feature_id) | |
| if not source_feat.isValid() or source_feat.geometry().isEmpty(): | |
| return 0 | |
| line_geom = QgsGeometry(source_feat.geometry()) | |
| # Transformar para CRS do projeto se necessário | |
| if layer_id in crs_transforms: | |
| line_geom.transform(crs_transforms[layer_id]) | |
| # ── Centróide do label ── | |
| label_centroid = label_geom.centroid() | |
| if label_centroid.isEmpty(): | |
| return 0 | |
| label_centroid_pt = label_centroid.asPoint() | |
| # ── Resolver multipart ── | |
| line_geom = get_closest_singlepart(line_geom, label_centroid_pt) | |
| line_length = line_geom.length() | |
| if line_length <= 0: | |
| return 0 | |
| # ── Projetar centróide na linha ── | |
| closest_pt_geom = line_geom.nearestPoint(label_centroid) | |
| center_distance = line_geom.lineLocatePoint(closest_pt_geom) | |
| # ── Extrair substring com comprimento exato do label ── | |
| half_len = label_length_mu / 2.0 | |
| start_d = center_distance - half_len | |
| end_d = center_distance + half_len | |
| # Ajustar se extrapolar os limites da linha | |
| if start_d < 0: | |
| start_d = 0.0 | |
| end_d = min(label_length_mu, line_length) | |
| if end_d > line_length: | |
| end_d = line_length | |
| start_d = max(0.0, end_d - label_length_mu) | |
| # curveSubstring opera na geometria abstrata | |
| abstract_geom = line_geom.constGet() | |
| if abstract_geom is None: | |
| return 0 | |
| try: | |
| substring = abstract_geom.curveSubstring(start_d, end_d) | |
| except Exception: | |
| return 0 | |
| if substring is None or substring.isEmpty(): | |
| return 0 | |
| result_geom = QgsGeometry(substring) | |
| # ── Offset baseado na configuração da camada ── | |
| offset_dist = 0.0 | |
| side_str = "on" | |
| only_on_line = fi.is_on_line and not fi.is_above_line and not fi.is_below_line | |
| only_above_below = (fi.is_above_line or fi.is_below_line) and not fi.is_on_line | |
| mixed_flags = fi.is_on_line and (fi.is_above_line or fi.is_below_line) | |
| if only_on_line: | |
| # ON LINE exclusivo: nunca aplicar offset | |
| offset_dist = 0.0 | |
| side_str = "on" | |
| elif only_above_below: | |
| # ABOVE/BELOW exclusivo: sempre aplicar offset | |
| dist_centroid_to_line = line_geom.distance(label_centroid) | |
| dist_bottom_to_line = line_geom.distance(label_geom) | |
| real_offset = max(0.0, dist_centroid_to_line - dist_bottom_to_line) | |
| offset_sign = determine_offset_side( | |
| line_geom, label_centroid_pt, center_distance | |
| ) | |
| offset_dist = real_offset * offset_sign | |
| side_str = "above" if offset_sign > 0 else "below" | |
| elif mixed_flags: | |
| # Ambos habilitados: QGIS decide por label. | |
| # Detectar empiricamente pela distância da borda à linha. | |
| label_height = compute_label_height_mu(fi) | |
| dist_bottom_to_line = line_geom.distance(label_geom) | |
| if dist_bottom_to_line > label_height * ON_LINE_TOLERANCE_FACTOR: | |
| # Este label está above/below | |
| dist_centroid_to_line = line_geom.distance(label_centroid) | |
| real_offset = max(0.0, dist_centroid_to_line - dist_bottom_to_line) | |
| offset_sign = determine_offset_side( | |
| line_geom, label_centroid_pt, center_distance | |
| ) | |
| offset_dist = real_offset * offset_sign | |
| side_str = "above" if offset_sign > 0 else "below" | |
| else: | |
| offset_dist = 0.0 | |
| side_str = "on" | |
| # ── Aplicar offsetCurve se necessário (QGIS 3.28+) ── | |
| if offset_dist != 0.0: | |
| try: | |
| offset_result = result_geom.offsetCurve( | |
| offset_dist, | |
| 8, # segments | |
| Qgis.JoinStyle.Round, # joinStyle (enum QGIS 3.28+) | |
| 5.0 # miterLimit | |
| ) | |
| if offset_result and not offset_result.isEmpty(): | |
| result_geom = offset_result | |
| else: | |
| # Fallback: tentar com valor negado | |
| offset_result = result_geom.offsetCurve( | |
| -offset_dist, 8, Qgis.JoinStyle.Round, 5.0 | |
| ) | |
| if offset_result and not offset_result.isEmpty(): | |
| result_geom = offset_result | |
| side_str = "below" if side_str == "above" else "above" | |
| except Exception as e: | |
| print(f" ⚠ offsetCurve falhou: {e}") | |
| # ── Criar feature de saída ── | |
| dp = output_layer.dataProvider() | |
| feat = QgsFeature(output_layer.fields()) | |
| feat.setGeometry(result_geom) | |
| feat.setAttributes([ | |
| layer_name, | |
| feature_id, | |
| label_text, | |
| round(label_length_mu, 4), | |
| round(result_geom.length(), 4), | |
| round(offset_dist, 4), | |
| side_str, | |
| ]) | |
| dp.addFeature(feat) | |
| return 1 | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Função principal | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| def extract_label_lines(): | |
| project = QgsProject.instance() | |
| canvas = iface.mapCanvas() | |
| project_crs = project.crs() | |
| root = project.layerTreeRoot() | |
| # ── Camada de referência ── | |
| matches = project.mapLayersByName(POLYGON_LAYER_NAME) | |
| if not matches: | |
| raise RuntimeError( | |
| f"Camada '{POLYGON_LAYER_NAME}' não encontrada.\n" | |
| f"Ajuste POLYGON_LAYER_NAME no início do script." | |
| ) | |
| ref_layer = matches[0] | |
| total_polys = ref_layer.featureCount() | |
| print(f"\nCamada de referência: '{ref_layer.name()}' ({total_polys} polígonos)") | |
| # ── Camadas visíveis ── | |
| visible_layers = [] | |
| for layer in project.mapLayers().values(): | |
| if not layer.isSpatial(): | |
| continue | |
| tree_layer = root.findLayer(layer.id()) | |
| if tree_layer is None or not tree_layer.isVisible(): | |
| continue | |
| if layer.name() == OUTPUT_LAYER_NAME: | |
| continue | |
| visible_layers.append(layer) | |
| labeled = [l for l in visible_layers | |
| if hasattr(l, 'labelsEnabled') and l.labelsEnabled()] | |
| if not labeled: | |
| raise RuntimeError("Nenhuma camada visível com rótulos habilitados.") | |
| print(f"Camadas com rótulos: {[l.name() for l in labeled]}") | |
| # ── Resolução ── | |
| canvas_dpi = canvas.mapSettings().outputDpi() | |
| canvas_mupp = canvas.mapUnitsPerPixel() | |
| canvas_scale = canvas.scale() | |
| if RENDER_SCALE is not None and canvas_scale > 0: | |
| mu_per_px = canvas_mupp * (RENDER_SCALE / canvas_scale) | |
| use_scale = RENDER_SCALE | |
| else: | |
| mu_per_px = canvas_mupp | |
| use_scale = canvas_scale | |
| print(f"DPI: {canvas_dpi:.0f}") | |
| print(f"Escala de renderização: 1:{use_scale:,.0f} ({mu_per_px:.10f} u.m./px)") | |
| # ── Info de fonte de cada camada ── | |
| print("\nExtraindo propriedades de fonte:") | |
| font_info_cache = {} | |
| for layer in labeled: | |
| print(f" [{layer.name()}]", end=" ") | |
| font_info_cache[layer.id()] = LabelFontInfo(layer, mu_per_px, canvas_dpi) | |
| # ── Transformações CRS ── | |
| crs_transforms = {} | |
| for layer in labeled: | |
| if layer.crs() != project_crs: | |
| crs_transforms[layer.id()] = QgsCoordinateTransform( | |
| layer.crs(), project_crs, project | |
| ) | |
| print(f" CRS transform: {layer.name()} " | |
| f"({layer.crs().authid()} → {project_crs.authid()})") | |
| needs_ref_transform = (ref_layer.crs() != project_crs) | |
| if needs_ref_transform: | |
| ref_xform = QgsCoordinateTransform(ref_layer.crs(), project_crs, project) | |
| # ── Remover camada anterior ── | |
| from qgis.PyQt.QtWidgets import QApplication | |
| ids = [l.id() for l in project.mapLayersByName(OUTPUT_LAYER_NAME)] | |
| if ids: | |
| canvas.setRenderFlag(False) | |
| QApplication.processEvents() | |
| project.removeMapLayers(ids) | |
| QApplication.processEvents() | |
| canvas.setRenderFlag(True) | |
| QApplication.processEvents() | |
| # ── Criar camada de saída ── | |
| output_layer = QgsVectorLayer( | |
| f"LineString?crs={project_crs.authid()}&index=yes", | |
| OUTPUT_LAYER_NAME, "memory" | |
| ) | |
| dp = output_layer.dataProvider() | |
| dp.addAttributes([ | |
| QgsField("camada", QVariant.String), | |
| QgsField("fid", QVariant.LongLong), | |
| QgsField("texto", QVariant.String), | |
| QgsField("comp_label", QVariant.Double), | |
| QgsField("comp_linha", QVariant.Double), | |
| QgsField("offset", QVariant.Double), | |
| QgsField("lado", QVariant.String), | |
| ]) | |
| output_layer.updateFields() | |
| seen = set() | |
| total_labels = 0 | |
| # ── Loop principal ── | |
| print(f"\nProcessando {total_polys} polígonos…") | |
| for i, feat in enumerate(ref_layer.getFeatures()): | |
| geom = feat.geometry() | |
| if geom.isEmpty(): | |
| continue | |
| if needs_ref_transform: | |
| geom.transform(ref_xform) | |
| bbox = geom.boundingBox() | |
| mw = max(bbox.width() * BBOX_MARGIN, 0.001) | |
| mh = max(bbox.height() * BBOX_MARGIN, 0.001) | |
| extent = QgsRectangle( | |
| bbox.xMinimum() - mw, bbox.yMinimum() - mh, | |
| bbox.xMaximum() + mw, bbox.yMaximum() + mh, | |
| ) | |
| show = ((i + 1) % 10 == 0) or (i == 0) or ((i + 1) == total_polys) | |
| if show: | |
| print(f" Polígono {i + 1}/{total_polys}…", end="") | |
| labels = render_and_collect_labels( | |
| extent, visible_layers, project_crs, mu_per_px, canvas_dpi | |
| ) | |
| n_new = 0 | |
| for lbl in labels: | |
| key = (lbl.layerID, lbl.featureId, lbl.labelText) | |
| if not lbl.labelText or key in seen: | |
| continue | |
| seen.add(key) | |
| n = process_label_to_line( | |
| lbl, project, output_layer, font_info_cache, | |
| project_crs, crs_transforms | |
| ) | |
| total_labels += n | |
| n_new += n | |
| if show: | |
| print(f" → {n_new} novos rótulos") | |
| # ── Resultado ── | |
| output_layer.updateExtents() | |
| project.addMapLayer(output_layer) | |
| # Estilo: linha vermelha | |
| symbol = QgsLineSymbol.createSimple({ | |
| 'color': '255,50,50,200', | |
| 'width': '0.5', | |
| 'capstyle': 'round', | |
| }) | |
| output_layer.setRenderer(QgsSingleSymbolRenderer(symbol)) | |
| output_layer.triggerRepaint() | |
| canvas.refresh() | |
| print(f"\n{'═' * 60}") | |
| print(f" ✔ Polígonos processados : {total_polys}") | |
| print(f" ✔ Rótulos capturados : {total_labels}") | |
| print(f" ✔ Rótulos únicos : {len(seen)}") | |
| print(f" → Camada '{OUTPUT_LAYER_NAME}' (linha vermelha)") | |
| print(f"{'═' * 60}") | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| # Execução | |
| # ══════════════════════════════════════════════════════════════════════════ | |
| try: | |
| extract_label_lines() | |
| except RuntimeError as e: | |
| print(f"\n⚠ ERRO: {e}\n") | |
| except Exception as e: | |
| print(f"\n⚠ ERRO INESPERADO: {e}\n") | |
| import traceback | |
| traceback.print_exc() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment