User:DressyPear4/SubstituicaoPoligonosLote

From OpenStreetMap Wiki
Jump to navigation Jump to search

Substituição de polígonos em lote

Script tem a mesma função que as teclas de atalho Ctrl+ Shift+G mas ao invés de substituir um a um, é possível fazer a substituição de multiplas geometrias.

Caso o polígono a ser substituído tenha uma quantidade maior de nós que o polígono de destino, esses serão deletados

Como funciona?

  • Modo 1
    • Selecione um ou mais polígonos com id
    • Selecione a mesma quantidade de polígonos sem id
      • Se a quantidade de polígonos com id for maior que a quantidade de destino; os selecionados extras serão ignorados
  • Modo 2
    • Selecione apenas um polígono com id
    • Selecione uma quantidade de polígonos com nós equivalentes para evitar serem deletados
      • O polígono será substituído para um e os nós serão distribuídos para os demais selecionados

Demonstração

Código

from org.openstreetmap.josm.data.osm import Way, Node
from org.openstreetmap.josm.data import UndoRedoHandler
from org.openstreetmap.josm.gui import MainApplication, Notification
from org.openstreetmap.josm.command import SequenceCommand, ChangeCommand, DeleteCommand, AddCommand
from javax.swing import JOptionPane, UIManager, JPanel, JRadioButton, ButtonGroup
from java.awt import GridLayout
import math

# FUNÇÕES AUXILIARES

def get_centroid(way):
    nodes = way.getNodes()
    if not nodes:
        return (0, 0)
    x = sum(n.getCoor().getX() for n in nodes) / len(nodes)
    y = sum(n.getCoor().getY() for n in nodes) / len(nodes)
    return (x, y)

def calcular_distancia(c1, c2):
    dx = c1[0] - c2[0]
    dy = c1[1] - c2[1]
    return math.sqrt(dx * dx + dy * dy)

def get_clean_nodes(way):
    nodes = list(way.getNodes())
    if way.isClosed() and nodes and nodes[0] == nodes[-1]:
        return nodes[:-1]
    return nodes

def safe_delete_check(node, way_context, safe_set):
    if node in safe_set:
        return False, False
    if node.getDataSet() is not None and len(node.getReferrers()) == 1 and way_context in node.getReferrers():
        safe_set.add(node)
        return True, (node.getUniqueId() > 0)
    return False, False

def safe_delete_check_list(node, way_list, safe_set):
    if node in safe_set:
        return False, False
    is_safe = True
    if node.getDataSet() is not None:
        if not node.getReferrers():
            is_safe = True
        else:
            for ref in node.getReferrers():
                if ref not in way_list:
                    is_safe = False
                    break
    if is_safe:
        safe_set.add(node)
        return True, (node.getUniqueId() > 0)
    return False, False

def pode_deletar(node):
    return not node.isDeleted() and node.getDataSet() is not None

# MODO 1: "Mesma quantidade" (N-para-N)

def substituir_varios():
    layer_manager = MainApplication.getLayerManager()
    dataset = layer_manager.getEditDataSet()
    if not dataset:
        Notification(u"Nenhuma camada de edição esta ativa.")\
        .setIcon(UIManager.getIcon("OptionPane.errorIcon")).show()
        return

    ways = dataset.getSelectedWays()
    antigas = [w for w in ways if not w.isNew()]
    novas = [w for w in ways if w.isNew()]

    if not antigas or not novas:
        Notification(u"Selecione pelo menos uma geometria com ID e uma sem.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    pares = []
    usadas = set()
    for a in antigas:
        c1 = get_centroid(a)
        melhor = None
        menor_dist = float("inf")
        for n in novas:
            if n in usadas: continue
            dist = calcular_distancia(c1, get_centroid(n))
            if dist < menor_dist:
                melhor, menor_dist = n, dist
        if melhor:
            usadas.add(melhor)
            pares.append((a, melhor))

    if not pares:
        Notification(u"Nenhum par válido encontrado.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    add_node_cmds, change_node_cmds, change_way_cmds = [], [], []
    delete_old_node_cmds, delete_new_way_cmds, delete_new_node_cmds = [], [], []

    nos_ja_marcados_delete_new = set()
    nos_ja_marcados_delete_old = set()
    linhas_substituidas, nos_substituidos = 0, 0
    nos_removidos_antiga, nos_removidos_nova = 0, 0

    novas_deletadas = []

    for antiga, nova in pares:
        novas_deletadas.append(nova)
        nova_nodes = get_clean_nodes(nova)
        antiga_nodes = get_clean_nodes(antiga)

        if not nova_nodes or not antiga_nodes:
            continue

        min_len = min(len(nova_nodes), len(antiga_nodes))

        # 1. Comandos de Mudança de Nó
        for i in range(min_len):
            n_antigo, n_novo = antiga_nodes[i], nova_nodes[i]
            novo_estado = Node(n_antigo)
            novo_estado.setCoor(n_novo.getCoor())
            change_node_cmds.append(ChangeCommand(n_antigo, novo_estado))
            nos_substituidos += 1

        novos_nos_para_way = list(antiga_nodes[:min_len])

        # 2. Comandos de Adição de Nó (Se a nova way for maior)
        for i in range(min_len, len(nova_nodes)):
            n = nova_nodes[i]
            novo_no_obj = Node(n.getCoor())
            novo_no_obj.setModified(True)
            add_node_cmds.append(AddCommand(dataset, novo_no_obj))
            novos_nos_para_way.append(novo_no_obj)

        if antiga.isClosed() and novos_nos_para_way and novos_nos_para_way[0] != novos_nos_para_way[-1]:
            novos_nos_para_way.append(novos_nos_para_way[0])

        # 3. Comando de Mudança da Way Antiga
        nova_way = Way(antiga)
        nova_way.setNodes(novos_nos_para_way)
        tags = dict(antiga.getKeys())
        for entry in nova.getKeys().entrySet():
            k, v = entry.getKey(), entry.getValue()
            if k not in tags: tags[k] = v
        nova_way.setKeys(tags)
        change_way_cmds.append(ChangeCommand(antiga, nova_way))

        # 4. Comando de Deleção da Way Nova
        delete_new_way_cmds.append(DeleteCommand(dataset, nova))
        linhas_substituidas += 1

        # 6. Comandos de Deleção dos Nós Excedentes da Way Antiga
        for n in antiga_nodes[min_len:]:
            is_safe, is_old = safe_delete_check(n, antiga, nos_ja_marcados_delete_old)
            if is_safe:
                if is_old: nos_removidos_antiga += 1
                delete_old_node_cmds.append(DeleteCommand(dataset, n))

    # 5. Comandos de Deleção dos Nós das Ways Novas
    nodes_to_check_delete = set()
    for nova in novas_deletadas:
        nodes_to_check_delete.update(nova.getNodes()) # Adiciona todos os nós ao set (únicos)

    for n in nodes_to_check_delete:
        is_safe, is_old = safe_delete_check_list(n, novas_deletadas, nos_ja_marcados_delete_new)
        if is_safe:
            if is_old: nos_removidos_nova += 1
            delete_new_node_cmds.append(DeleteCommand(dataset, n))

    # Monta a sequência final
    comandos_finais = (
        add_node_cmds + change_node_cmds + change_way_cmds +
        delete_old_node_cmds + delete_new_way_cmds + delete_new_node_cmds
    )

    if comandos_finais:
        UndoRedoHandler.getInstance().add(SequenceCommand("Substituir multiplas geometrias", comandos_finais))
        Notification(
            u"Substituicoes (Modo 1) concluídas.\n"
            + u"Linhas substituídas: %d\n" % linhas_substituidas
            + u"Nós substituídos: %d\n" % nos_substituidos
            + u"Nós antigos removidos: %d\n" % nos_removidos_antiga
            + u"Nós novos removidos: %d" % nos_removidos_nova
        ).setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
    else:
        Notification(u"Nenhuma substituição (Modo 1) foi realizada.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()

# MODO 2: "Diferença quantidade" (1-para-N, com distribuição de IDs)

def substituir_diferenca():
    layer_manager = MainApplication.getLayerManager()
    dataset = layer_manager.getEditDataSet()
    if not dataset:
        Notification(u"Nenhuma camada de edição está ativa.")\
        .setIcon(UIManager.getIcon("OptionPane.errorIcon")).show()
        return

    ways = dataset.getSelectedWays()
    antigas = [w for w in ways if not w.isNew()]
    novas = [w for w in ways if w.isNew()]

    if len(antigas) != 1:
        Notification(u"Modo 'Diferença': Selecione EXATAMENTE UMA geometria com histórico (antiga).")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return
    if not novas:
        Notification(u"Modo 'Diferença': Selecione pelo menos UMA geometria sem histórico (nova).")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    antiga = antigas[0]
    antiga_centroid = get_centroid(antiga)
    novas.sort(key=lambda n: calcular_distancia(antiga_centroid, get_centroid(n)))

    add_node_cmds, change_node_cmds, change_way_cmds = [], [], []
    delete_old_node_cmds, delete_new_way_cmds, delete_new_node_cmds = [], [], []

    nos_ja_marcados_delete_new = set()
    nos_ja_marcados_delete_old = set()
    nos_substituidos, nos_removidos_antiga, nos_removidos_nova = 0, 0, 0

    nova_principal = novas[0]
    novas_excedentes = novas[1:]

    antiga_nodes_all = get_clean_nodes(antiga)
    nova_principal_nodes = get_clean_nodes(nova_principal)

    if not antiga_nodes_all or not nova_principal_nodes:
        Notification(u"Geometria principal (antiga ou nova) não possui nós.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    min_len_principal = min(len(antiga_nodes_all), len(nova_principal_nodes))

    for i in range(min_len_principal):
        n_antigo, n_novo = antiga_nodes_all[i], nova_principal_nodes[i]
        novo_estado = Node(n_antigo)
        novo_estado.setCoor(n_novo.getCoor())
        change_node_cmds.append(ChangeCommand(n_antigo, novo_estado))
        nos_substituidos += 1

    novos_nos_para_way_antiga = list(antiga_nodes_all[:min_len_principal])

    for i in range(min_len_principal, len(nova_principal_nodes)):
        n = nova_principal_nodes[i]
        novo_no_obj = Node(n.getCoor())
        novo_no_obj.setModified(True)
        add_node_cmds.append(AddCommand(dataset, novo_no_obj))
        novos_nos_para_way_antiga.append(novo_no_obj)

    if antiga.isClosed() and novos_nos_para_way_antiga and novos_nos_para_way_antiga[0] != novos_nos_para_way_antiga[-1]:
        novos_nos_para_way_antiga.append(novos_nos_para_way_antiga[0])

    nova_way = Way(antiga)
    nova_way.setNodes(novos_nos_para_way_antiga)
    tags = dict(antiga.getKeys())
    for entry in nova_principal.getKeys().entrySet():
        k, v = entry.getKey(), entry.getValue()
        if k not in tags:
            tags[k] = v
    nova_way.setKeys(tags)
    change_way_cmds.append(ChangeCommand(antiga, nova_way))

    delete_new_way_cmds.append(DeleteCommand(dataset, nova_principal))
    nos_antigos_excedentes = list(antiga_nodes_all[min_len_principal:])

    for nova_excedente in novas_excedentes:
        if not nos_antigos_excedentes:
            break

        nova_excedente_nodes = get_clean_nodes(nova_excedente)
        if not nova_excedente_nodes:
            continue

        min_len_excedente = min(len(nos_antigos_excedentes), len(nova_excedente_nodes))
        novos_nos_para_way_nova = []

        for i in range(min_len_excedente):
            n_antigo = nos_antigos_excedentes.pop(0)
            n_novo = nova_excedente_nodes[i]
            novo_estado = Node(n_antigo)
            novo_estado.setCoor(n_novo.getCoor())
            change_node_cmds.append(ChangeCommand(n_antigo, novo_estado))
            novos_nos_para_way_nova.append(n_antigo)
            nos_substituidos += 1

        for i in range(min_len_excedente, len(nova_excedente_nodes)):
            n = nova_excedente_nodes[i]
            novo_no_obj = Node(n.getCoor())
            novo_no_obj.setModified(True)
            add_node_cmds.append(AddCommand(dataset, novo_no_obj))
            novos_nos_para_way_nova.append(novo_no_obj)

        if nova_excedente.isClosed() and novos_nos_para_way_nova and novos_nos_para_way_nova[0] != novos_nos_para_way_nova[-1]:
            novos_nos_para_way_nova.append(novos_nos_para_way_nova[0])

        nova_way_excedente = Way(nova_excedente)
        nova_way_excedente.setNodes(novos_nos_para_way_nova)
        change_way_cmds.append(ChangeCommand(nova_excedente, nova_way_excedente))

    ways_realmente_deletadas = [nova_principal]
    nodes_to_check_delete = set()
    for nova_way in ways_realmente_deletadas:
        nodes_to_check_delete.update(nova_way.getNodes())

    for n in nodes_to_check_delete:
        is_safe, is_old = safe_delete_check_list(n, ways_realmente_deletadas, nos_ja_marcados_delete_new)
        if is_safe and pode_deletar(n):
            if is_old:
                nos_removidos_nova += 1
            delete_new_node_cmds.append(DeleteCommand(dataset, n))

    for n in nos_antigos_excedentes:
        is_safe, is_old = safe_delete_check(n, antiga, nos_ja_marcados_delete_old)
        if is_safe and pode_deletar(n):
            if is_old:
                nos_removidos_antiga += 1
            delete_old_node_cmds.append(DeleteCommand(dataset, n))

    comandos_finais = (
        add_node_cmds + change_node_cmds + change_way_cmds +
        delete_old_node_cmds + delete_new_way_cmds + delete_new_node_cmds
    )

    if comandos_finais:
        UndoRedoHandler.getInstance().add(SequenceCommand("Substituir 1-N (com distribuicao)", comandos_finais))
        Notification(
            u"Substituição (Modo 2) concluída.\n"
            + u"Ways selecionadas sem ID: %d\n" % len(novas_excedentes)
            + u"Nós substituídos/movidos: %d\n" % nos_substituidos
            + u"Nós antigos (não usados) removidos: %d\n" % nos_removidos_antiga
            + u"Nós novos (way principal) removidos: %d" % nos_removidos_nova
        ).setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()

        #  Etapa final: limpeza de nós sem ID que ficaram órfãos
        nodes_orfaos_removidos = 0
        delete_orfaos_cmds = []
        for node in list(dataset.getNodes()):
            if node.isNew() and not node.getReferrers() and pode_deletar(node):
                delete_orfaos_cmds.append(DeleteCommand(dataset, node))
                nodes_orfaos_removidos += 1

        if delete_orfaos_cmds:
            UndoRedoHandler.getInstance().add(SequenceCommand("Limpeza de nós órfãos", delete_orfaos_cmds))

    else:
        Notification(u"Nenhuma substituição (Modo 2) foi realizada.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()

# FUNÇÃO PRINCIPAL (Menu de Escolha)
def main():
    layer_manager = MainApplication.getLayerManager()
    dataset = layer_manager.getEditDataSet()

    # --- VERIFICAÇÕES ANTES DE ABRIR O DIALOG ---
    if not dataset:
        Notification(u"Nenhuma camada de edição está ativa.")\
        .setIcon(UIManager.getIcon("OptionPane.errorIcon")).show()
        return

    selected_ways = dataset.getSelectedWays()
    if not selected_ways:
        Notification(u"Nenhuma geometria selecionada. Selecione ao menos uma way.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    # Verifica se há ao menos uma 'antiga' e uma 'nova' (útil para modo 1/2)
    has_old = any(not w.isNew() for w in selected_ways)
    has_new = any(w.isNew() for w in selected_ways)
    if not has_old or not has_new:
        Notification(u"Selecione pelo menos uma geometria com ID (antiga) e uma sem ID (nova) para usar o script.")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()
        return

    # --- Exibe painel de escolha ---
    panel = JPanel(GridLayout(0, 1))

    rb_mesma = JRadioButton(u"Modo 1: Múltiplos pares (N-para-N)")
    rb_mesma.setToolTipText(u"Pareia cada linha antiga com a linha nova mais próxima. (ex: 3 antigas, 3 novas)")
    rb_mesma.setSelected(True)

    rb_diferente = JRadioButton(u"Modo 2: Distribuir IDs (1-para-N)")
    rb_diferente.setToolTipText(u"Usa 1 antiga; pareia com a mais próxima e distribui os IDs restantes para as outras novas.")

    group = ButtonGroup()
    group.add(rb_mesma)
    group.add(rb_diferente)

    panel.add(rb_mesma)
    panel.add(rb_diferente)

    result = JOptionPane.showConfirmDialog(
        MainApplication.getMainFrame(),
        panel,
        u"Escolha o Modo de Substituição",
        JOptionPane.OK_CANCEL_OPTION,
        JOptionPane.PLAIN_MESSAGE
    )

    if result == JOptionPane.OK_OPTION:
        if rb_mesma.isSelected():
            substituir_varios()
        elif rb_diferente.isSelected():
            substituir_diferenca()

# Executa
main()