User:DressyPear4/EscalonarPerimetro

From OpenStreetMap Wiki
Jump to navigation Jump to search

Escalonar perímetro

Usava para determinar espaços entre casas criadas com plugin Gridfy, prático para geometria quadradas ou retangulares mas devido a casas terem formatos irregulares, ferramentas de cópias são mais eficientes.

Como funciona?

  • Crie um retângulo um pouco maior que o total de grupo de casas
  • Use o pugin Gridify para determinar a quantidade
  • Faça a seleção de todo grupo
  • Escolha um tamanho adequado
  • Marque "Separar geometrias" para removera mesclagem de nós

Demonstração

Imagem.gif, clique para visualizar.

Código

from org.openstreetmap.josm.data.osm import Way, Node
from org.openstreetmap.josm.gui import MainApplication, Notification
from org.openstreetmap.josm.data.coor import LatLon
from org.openstreetmap.josm.command import ChangeCommand, DeleteCommand, AddCommand, SequenceCommand
from org.openstreetmap.josm.data.UndoRedoHandler import getInstance
from javax.swing import JOptionPane, JPanel, JLabel, JTextField, JRadioButton, BoxLayout, UIManager
import math

def calcular_distancia_aproximada(n1, n2):
    lat1 = n1.getCoor().lat()
    lon1 = n1.getCoor().lon()
    lat2 = n2.getCoor().lat()
    lon2 = n2.getCoor().lon()
    dlat = (lat2 - lat1) * 111320
    avg_lat = (lat1 + lat2) / 2.0
    dlon = (lon2 - lon1) * 111320 * math.cos(math.radians(avg_lat))
    return math.hypot(dlat, dlon)

def calcular_perimetro(nodes):
    return sum(
        calcular_distancia_aproximada(nodes[i], nodes[i + 1])
        for i in range(len(nodes) - 1)
    )

def calcular_centro(nodes):
    lat_sum = sum(node.getCoor().lat() for node in nodes)
    lon_sum = sum(node.getCoor().lon() for node in nodes)
    return LatLon(lat_sum / len(nodes), lon_sum / len(nodes))

def aplicar_escala(nodes_originais, centro, fator):
    novos = []
    for node in nodes_originais:
        lat_offset = (node.getCoor().lat() - centro.lat()) * fator
        lon_offset = (node.getCoor().lon() - centro.lon()) * fator
        novos.append(LatLon(centro.lat() + lat_offset, centro.lon() + lon_offset))
    return novos

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

    selection = dataset.getSelected()
    ways = [way for way in selection if isinstance(way, Way) and not way.isDeleted()]

    if not ways:
        Notification(u"Selecione pelo menos uma geometria válida!")\
        .setIcon(UIManager.getIcon("OptionPane.informationIcon"))\
        .show()
        return

    way_nodes = {}
    perimeter_total = 0

    for way in ways:
        nodes = [node for node in way.getNodes() if not node.isDeleted()]
        if len(nodes) < 3:
            Notification(u"A geometria {} precisa ter pelo menos 3 pontos válidos!".format(way.getId()))\
            .setIcon(UIManager.getIcon("OptionPane.warningIcon"))\
            .show()
            return
        is_closed = nodes[0] == nodes[-1]
        if not is_closed:
            nodes = list(nodes) + [nodes[0]]
        way_nodes[way] = nodes
        perimeter_total += calcular_perimetro(nodes)

    panel = JPanel()
    panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS))
    panel.add(JLabel(u"Perímetro total atual: {:.2f} m".format(perimeter_total)))
    panel.add(JLabel(u"Digite o novo perímetro desejado:"))
    campo_valor = JTextField(10)
    panel.add(campo_valor)
    radio = JRadioButton("Separar geometrias antes de escalonar")
    panel.add(radio)
    aviso = JLabel(u"<html><span style='color:red'>ATENÇÃO! Não marque essa opção em geometrias que não<br>"
                   u"estiverem grudadas, pois o ID dos nós nao será preservado</span></html>")
    panel.add(aviso)

    result = JOptionPane.showConfirmDialog(None, panel, u"Ajustar Perímetro", JOptionPane.OK_CANCEL_OPTION)
    if result != JOptionPane.OK_OPTION:
        return

    try:
        new_perimeter = float(campo_valor.getText())
    except:
        Notification(u"Valor inserido inválido!")\
        .setIcon(UIManager.getIcon("OptionPane.warningIcon"))\
        .show()
        return

    separar = radio.isSelected()

    if perimeter_total == 0:
        Notification(u"Perímetro atual é zero. Não é possível escalar.")\
        .setIcon(UIManager.getIcon("OptionPane.warningIcon"))\
        .show()
        return

    fator = math.sqrt(new_perimeter / perimeter_total)
    precisao_alvo = 0.01
    max_iteracoes = 20

    for _ in range(max_iteracoes):
        total = 0
        for way, nodes in way_nodes.items():
            centro = calcular_centro(nodes) if separar else calcular_centro([n for nodes_ in way_nodes.values() for n in nodes_])
            novas_coords = aplicar_escala(nodes, centro, fator)
            total += calcular_perimetro([Node(coord) for coord in novas_coords])
        erro = new_perimeter - total
        if abs(erro) <= precisao_alvo:
            break
        fator *= math.sqrt(new_perimeter / total)

    all_commands = []
    used_nodes = set()
    node_replacements = {}
    coord_to_node = {}

    centro_comum = calcular_centro([n for nodes_ in way_nodes.values() for n in nodes_])

    for way in way_nodes:
        nodes = way_nodes[way]
        is_closed = nodes[0] == nodes[-1]
        centro = calcular_centro(nodes) if separar else centro_comum
        novas_coords = aplicar_escala(nodes, centro, fator)
        novos_nos = []

        add_node_commands = []

        for i, node in enumerate(nodes):
            if node.isDeleted():
                continue
            coord = novas_coords[i]
            coord_key = (round(coord.lat(), 9), round(coord.lon(), 9))

            if separar:
                if coord_key in coord_to_node:
                    novo_node = coord_to_node[coord_key]
                else:
                    novo_node = Node(coord)
                    novo_node.setModified(True)
                    coord_to_node[coord_key] = novo_node
                    add_node_commands.append(AddCommand(dataset, novo_node))
                novos_nos.append(novo_node)
                used_nodes.add(novo_node)
                node_replacements.setdefault(node, []).append((way, novo_node))
            else:
                node_novo = Node(node)
                node_novo.setCoor(coord)
                node_novo.setModified(True)
                all_commands.append(ChangeCommand(dataset, node, node_novo))
                novos_nos.append(node)
                used_nodes.add(node)

        if is_closed and len(novos_nos) > 1:
            novos_nos = novos_nos[:-1] + [novos_nos[0]]

        if len(novos_nos) < 3:
            Notification(u"A geometria {} resultou em menos de 3 nós válidos!".format(way.getId()))\
            .setIcon(UIManager.getIcon("OptionPane.warningIcon"))\
            .show()
            return

        if separar:
            novo_way = Way(way)
            novo_way.setNodes(novos_nos)
            novo_way.setModified(True)
            all_commands.extend(add_node_commands)
            all_commands.append(ChangeCommand(dataset, way, novo_way))
            way_nodes[way] = novos_nos
        else:
            way.setModified(True)
    nodes_to_delete = set()
    for node in dataset.getNodes():
        if node.isDeleted():
            continue
        referrers = [r for r in node.getReferrers() if not r.isDeleted()]
        if len(referrers) == 0 and not node.getKeys() and node not in used_nodes and node.getId() < 0:
            nodes_to_delete.add(node)
        elif node in node_replacements and not node.getKeys():
            selected_referrers = [r for r in referrers if r in way_nodes]
            replaced_in_ways = [r[0] for r in node_replacements[node]]
            if set(selected_referrers).issubset(set(replaced_in_ways)):
                nodes_to_delete.add(node)

    for node in nodes_to_delete:
        if not node.isDeleted():
            all_commands.append(DeleteCommand(dataset, node))

    if all_commands:
        getInstance().add(SequenceCommand("Ajustar perímetro das geometrias", all_commands))

    Notification(u"Geometrias ajustadas com perímetro final de aproximadamente {:.2f} m".format(new_perimeter))\
    .setIcon(UIManager.getIcon("OptionPane.informationIcon"))\
    .show()

# Executar
ajustar_perimetro_geometrias()