User:DressyPear4/ResolverNotas

From OpenStreetMap Wiki
Jump to navigation Jump to search

Resolver notas selecionadas

Resolver_Notas é um script para o JOSM que automatiza o fechamento de notas OSM vinculadas a um upload de changeset.

Funcionalidades:

- Captura notas selecionadas no mapa e as mantém em uma lista visual com destaque laranja no mapa e no painel de notas do JOSM;
- Ao abrir a janela de upload, injeta automaticamente a tag closed:note com os IDs das notas no changeset;
- Detecta a conclusão do upload e fecha as notas no servidor OSM com o comentário "Resolvido com changeset/ID";
- Opção de adicionar um comentário personalizado por nota antes de capturá-la;
- Checkbox para controlar se o campo comment do changeset é preenchido automaticamente;
- Notas já fechadas são ignoradas automaticamente.

Como funciona?

  • Execute o script, um painel flutuante será aberto
  • Selecione uma nota no mapa
  • Clique em Capturar Nota Selecionada
  • Faça suas edições normalmente e envie o changeset
  • O script detecta o upload concluído e fecha as notas automaticamente no servidor

Demonstração

Código

JavaScript

Última atualização: 2026-05-19

"use strict";

import { println } from 'josm/scriptingconsole';

(function() {
    const MainApplication = org.openstreetmap.josm.gui.MainApplication;

    // --- Importacoes Java ---
    const JDialog           = Java.type("javax.swing.JDialog");
    const JPanel            = Java.type("javax.swing.JPanel");
    const JButton           = Java.type("javax.swing.JButton");
    const JLabel            = Java.type("javax.swing.JLabel");
    const BoxLayout         = Java.type("javax.swing.BoxLayout");
    const JScrollPane       = Java.type("javax.swing.JScrollPane");
    const BorderLayout      = Java.type("java.awt.BorderLayout");
    const Dimension         = Java.type("java.awt.Dimension");
    const Notification      = Java.type("org.openstreetmap.josm.gui.Notification");
    const UIManager         = Java.type("javax.swing.UIManager");
    const Color             = Java.type("java.awt.Color");
    const BasicStroke       = Java.type("java.awt.BasicStroke");
    const AlphaComposite    = Java.type("java.awt.AlphaComposite");
    const Path2D            = Java.type("java.awt.geom.Path2D");
    const Path2DFloat       = Java.type("java.awt.geom.Path2D$Float");
    const SwingUtilities    = Java.type("javax.swing.SwingUtilities");
    const Timer             = Java.type("javax.swing.Timer");
    const WindowAdapter     = Java.type("java.awt.event.WindowAdapter");
    const KeyboardFocusManager = Java.type("java.awt.KeyboardFocusManager");
    const JOptionPane       = Java.type("javax.swing.JOptionPane");
    const BorderFactory     = Java.type("javax.swing.BorderFactory");
    const GridBagLayout     = Java.type("java.awt.GridBagLayout");
    const GridBagConstraints= Java.type("java.awt.GridBagConstraints");
    const Insets            = Java.type("java.awt.Insets");
    const Font              = Java.type("java.awt.Font");
    const Integer           = Java.type("java.lang.Integer");
    const JCheckBox         = Java.type("javax.swing.JCheckBox");
    
    const MapViewPaintable  = org.openstreetmap.josm.gui.layer.MapViewPaintable;
    const NoteLayer         = org.openstreetmap.josm.gui.layer.NoteLayer;
    const LayerChangeListener = org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;

    // --- Variáveis de Estado e Destaque ---
    let rememberedNotes = []; 
    let currentPainter = null;
    let scanTimer = null;
    let lastChangesetId = null;
    let notesJList = null;
    let rendererOriginal = null;
    const COR_DESTAQUE = new Color(255, 220, 100); // Amarelo

    // --- Lógica de Destaque no Painel Lateral ---
    const encontrarNotesJList = function() {
        try {
            const map = MainApplication.getMap();
            if (!map) return null;
            const buscarRecursivo = function(comp, classe) {
                if (!comp) return null;
                if (comp.getClass().getSimpleName() === classe) return comp;
                if (comp.getComponents) {
                    let kids = comp.getComponents();
                    for (let i = 0; i < kids.length; i++) {
                        let r = buscarRecursivo(kids[i], classe);
                        if (r) return r;
                    }
                }
                return null;
            };
            const notesDialog = buscarRecursivo(map, "NotesDialog");
            return notesDialog ? buscarRecursivo(notesDialog, "JList") : null;
        } catch(e) { return null; }
    };

    const instalarRendererDestaque = function() {
        notesJList = encontrarNotesJList();
        if (!notesJList) return;
        rendererOriginal = notesJList.getCellRenderer();
        const DefaultListCellRenderer = Java.extend(Java.type("javax.swing.DefaultListCellRenderer"));
        const customRenderer = new DefaultListCellRenderer({
            getListCellRendererComponent: function(list, value, index, isSelected, cellHasFocus) {
                const comp = rendererOriginal.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                if (!isSelected && value !== null) {
                    const noteId = Number(value.getId());
                    const estaCapturada = rememberedNotes.some(function(n) { return Number(n.note.getId()) === noteId; });
                    if (estaCapturada) {
                        comp.setBackground(COR_DESTAQUE);
                        comp.setOpaque(true);
                    }
                }
                return comp;
            }
        });
        notesJList.setCellRenderer(customRenderer);
    };

    // --- Monitoramento de UI e Captura de Changeset ---
    const monitorChangesetID = function() {
        const windows = java.awt.Window.getWindows();
        for (let i = 0; i < windows.length; i++) {
            const win = windows[i];
            if (!win || !win.isVisible()) continue;
            
            const winClass = win.getClass().getSimpleName();
            // Janelas a ignorar para otimização
            if (winClass.match(/HistoryBrowserDialog|NotePopup|OSMObjInfoDialog|ValidatorDialog/i)) {
                continue;
            }
            
            let foundId = null;
            let isConfirmacaoUpload = false;

            const find = function(comp) {
                try {
                    const compClass = comp.getClass().getSimpleName();
                    if (compClass.match(/NotePopup/i)) return; 

                    let txt = comp.getText ? String(comp.getText()) : (comp.getContent ? String(comp.getContent()) : "");
                    
                    if (txt) {
                        const m = txt.match(/changeset\/(\d+)/) || txt.match(/changeset\s(\d+)/);
                        if (m) {
                            foundId = m[1];
                            // As notificações de sucesso usam H3 ou H4 no JOSM
                            // As notas normais possuem "Nota XXXXX" e tags <hr>
                            if (txt.toLowerCase().match(/<h[34]>/) && txt.toLowerCase().match(/sucesso|successful|upload|enviado/)) {
                                isConfirmacaoUpload = true;
                            }
                        }
                    }

                    if (!isConfirmacaoUpload && comp.getComponents) {
                        const children = comp.getComponents();
                        for (let j = 0; j < children.length; j++) {
                            find(children[j]);
                            if (isConfirmacaoUpload) break;
                        }
                    }
                } catch(e) {}
            };

            find(win);
            
            if (foundId && isConfirmacaoUpload && foundId !== lastChangesetId) {
                lastChangesetId = foundId;
                processarFechamentoNotas(foundId);
            }
        }
    };

    const processarFechamentoNotas = function(id) {
        SwingUtilities.invokeLater(function() {
            const noteLayer = MainApplication.getLayerManager().getNoteLayer();
            if (!noteLayer || rememberedNotes.length === 0) return;
            const nd = noteLayer.getNoteData();

            const notaAtualPorId = {};
            const notasAtuais = nd.getNotes();
            const itN = notasAtuais.iterator();
            
            while (itN.hasNext()) {
                const n = itN.next();
                notaAtualPorId[String(n.getId())] = n;
            }

            rememberedNotes.forEach(function(item) {
                try {
                    const notaIdStr = String(item.note.getId());
                    const notaAtual = notaAtualPorId[notaIdStr] || item.note;
                    
                    if (notaAtual.getState().toString() !== "CLOSED") {
                        let finalComment = (item.comment ? item.comment + "\n" : "") + "Resolvido com https://www.openstreetmap.org/changeset/" + id;
                        nd.closeNote(notaAtual, finalComment);
                    }
                } catch (err) { println("Erro ao fechar nota: " + item.note.getId() + " - " + err); }
            });
            
            rememberedNotes = [];
            updateUI(); refreshMap(); syncTags();
            if (notesJList) notesJList.repaint();

            try {
                const UploadNotesAction = Java.type("org.openstreetmap.josm.actions.UploadNotesAction");
                new UploadNotesAction().actionPerformed(null);
            } catch (err) { println("Erro Upload: " + err); }
        });
    };

    // --- Lógica de Destaque no Notas no Mapa ---
    const createPainter = function() {
        const PaintClass = Java.extend(MapViewPaintable, {
            paint: function(g, mv, bbox) {
                try {
                    if (rememberedNotes.length === 0) return;
                    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75));
                    g.setStroke(new BasicStroke(2.0));
                    g.setColor(Color.ORANGE);
                    rememberedNotes.forEach(function(item) {
                        const p = mv.getPoint(item.note.getLatLon());
                        const drop = new Path2DFloat();
                        drop.moveTo(p.x, p.y);
                        drop.curveTo(p.x - 5, p.y - 5, p.x - 7, p.y - 15, p.x, p.y - 15);
                        drop.curveTo(p.x + 7, p.y - 15, p.x + 5, p.y - 5, p.x, p.y);
                        drop.closePath();
                        g.fill(drop); g.draw(drop);
                    });
                } catch(e) {}
            }
        });
        return new PaintClass();
    };

    const refreshMap = function() {
        if (!MainApplication.getMap()) return;
        const mv = MainApplication.getMap().mapView;
        if (currentPainter) { try { mv.removeTemporaryLayer(currentPainter); } catch(e) {} }
        currentPainter = createPainter();
        mv.addTemporaryLayer(currentPainter);
        mv.repaint();
    };

    const syncTags = function() {
        const ds = MainApplication.getLayerManager().getEditDataSet();
        if (!ds) return;
        if (rememberedNotes.length > 0) {
            const ids = rememberedNotes.map(function(n) { return n.note.getId().toString(); }).join(';');
            ds.addChangeSetTag("closed:note", ids);
            if (chkInject.isSelected()) ds.addChangeSetTag("comment", "Resolvendo notas, " + ids.replace(/;/g, ", "));
        } else {
            ds.addChangeSetTag("closed:note", null);
            if (chkInject.isSelected()) ds.addChangeSetTag("comment", null);
        }
    };

    const focusHandler = new (Java.extend(Java.type("java.beans.PropertyChangeListener"), {
        propertyChange: function(e) {
            if (e.getPropertyName() === "activeWindow") {
                const win = e.getNewValue();
                if (win instanceof JDialog) {
                    const title = String(win.getTitle() || "");
                    if (title.match(/Upload|Enviar/)) {
                        lastChangesetId = null;
                        syncTags();
                    }
                }
            }
        }
    }))();

    const layerHandler = new (Java.extend(LayerChangeListener, {
        layerRemoving: function(e) {
            if (e.getRemovedLayer() instanceof NoteLayer) {
                SwingUtilities.invokeLater(function() {
                    rememberedNotes = [];
                    updateUI(); refreshMap(); syncTags();
                    if (notesJList) notesJList.repaint();
                });
            }
        },
        layerAdded: function(e) {
            if (e.getAddedLayer() instanceof NoteLayer && !notesJList) {
                SwingUtilities.invokeLater(function() { instalarRendererDestaque(); });
            }
        },
        activeLayerChange: function(e) {}, layerOrderChanged: function(e) {}
    }))();

    // --- Limpeza Total ao Fechar a Janela ---
    const fecharDialogoCompleto = function() {
        try {
            if (notesJList && rendererOriginal) notesJList.setCellRenderer(rendererOriginal);
            if (focusHandler) KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener(focusHandler);
            if (layerHandler) MainApplication.getLayerManager().removeLayerChangeListener(layerHandler);
            if (scanTimer) scanTimer.stop();
            const mv = MainApplication.getMap() ? MainApplication.getMap().mapView : null;
            if (currentPainter && mv) mv.removeTemporaryLayer(currentPainter);
        } catch(err) { println("Erro Resolver_Notas: " + err); } finally { dialog.dispose(); }
    };

    // --- Interface UI ---
    const dialog = new JDialog(MainApplication.getMainFrame(), "Resolver_Notas", false);
    dialog.setLayout(new BorderLayout(8, 8));
    dialog.setSize(new Dimension(250, 350));
    dialog.setLocationRelativeTo(MainApplication.getMainFrame());

    const outerPanel = new JPanel(new BorderLayout(6, 6));
    outerPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
    dialog.setContentPane(outerPanel);

    const capturePanel = new JPanel();
    capturePanel.setLayout(new BoxLayout(capturePanel, BoxLayout.Y_AXIS));

    const btnAdd = new JButton("Capturar Nota Selecionada");
    btnAdd.setAlignmentX(0.0);
    btnAdd.setMaximumSize(new Dimension(Integer.MAX_VALUE, 30));

    const chkInject = new JCheckBox("Inserir comentário no changeset 📝", true);
    chkInject.setAlignmentX(0.0); 
    chkInject.setFont(chkInject.getFont().deriveFont(10.0));
    chkInject.setToolTipText("Preenche automaticamente o campo 'comment' do changeset com os IDs das notas");

    const chkCustomComment = new JCheckBox("Inserir comentário na nota 🚩");
    chkCustomComment.setAlignmentX(0.0);
    chkCustomComment.setFont(chkCustomComment.getFont().deriveFont(10.0));
    chkCustomComment.setToolTipText("Abre uma caixa de texto para adicionar um prefixo personalizado ao fechar a nota");

    capturePanel.add(btnAdd);
    capturePanel.add(chkInject);
    capturePanel.add(chkCustomComment);
    outerPanel.add(capturePanel, BorderLayout.NORTH);

    const listWrapper = new JPanel(new BorderLayout());
    const listBorder = BorderFactory.createTitledBorder("Notas capturadas (0)");
    listWrapper.setBorder(listBorder);

    const listPanel = new JPanel(new GridBagLayout());
    const scrollPane = new JScrollPane(listPanel);
    scrollPane.setBorder(BorderFactory.createEmptyBorder());
    listWrapper.add(scrollPane, BorderLayout.CENTER);
    outerPanel.add(listWrapper, BorderLayout.CENTER);

    const updateUI = function() {
        listPanel.removeAll();
        listBorder.setTitle("Notas capturadas (" + rememberedNotes.length + ")");
        const gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weightx = 1.0; gbc.insets = new Insets(2, 2, 2, 2); gbc.gridx = 0;
        rememberedNotes.forEach(function(item, index) {
            gbc.gridy = index;
            const itemPanel = new JPanel(new BorderLayout(6, 0));
            itemPanel.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(new Color(200, 200, 200)),
                BorderFactory.createEmptyBorder(3, 6, 3, 6)
            ));
            const btnX = new JButton("x");
            btnX.setPreferredSize(new Dimension(24, 20));
            btnX.addActionListener(function() {
                rememberedNotes.splice(index, 1);
                updateUI(); refreshMap(); syncTags();
                if (notesJList) notesJList.repaint();
            });
            const lbl = new JLabel("Nota #" + item.note.getId());
            lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN, 11.0));
            if (item.comment) {
                const infoLabel = new JLabel("ⓘ");
                infoLabel.setForeground(new Color(0, 100, 200));
                infoLabel.setToolTipText(item.comment);
                itemPanel.add(infoLabel, BorderLayout.EAST);
            }
            itemPanel.add(btnX, BorderLayout.WEST);
            itemPanel.add(lbl, BorderLayout.CENTER);
            listPanel.add(itemPanel, gbc);
        });
        gbc.gridy = rememberedNotes.length; gbc.weighty = 1.0; listPanel.add(new JPanel(), gbc);
        listWrapper.revalidate(); listWrapper.repaint();
    };

    btnAdd.addActionListener(function() {
        const noteLayer = MainApplication.getLayerManager().getNoteLayer();
        if (!noteLayer) return;
        const selected = noteLayer.getNoteData().getSelectedNote();
        if (!selected) return;
        if (selected.getState().toString() === "CLOSED") {
            new Notification("Esta nota já está fechada.").setIcon(UIManager.getIcon("OptionPane.warningIcon")).show();
            return; 
        }
        if (!rememberedNotes.some(function(n){ return n.note.getId() === selected.getId(); })) {
            let userComment = null;
            if (chkCustomComment.isSelected()) {
                userComment = JOptionPane.showInputDialog(dialog, "Comentário para a nota #" + selected.getId() + ":", "Comentário Personalizado", JOptionPane.PLAIN_MESSAGE);
                if (userComment === null) return; 
                chkCustomComment.setSelected(false);
            }
            rememberedNotes.push({ note: selected, comment: userComment ? String(userComment).trim() : null });
            updateUI(); refreshMap(); syncTags();
            if (notesJList) notesJList.repaint();
        }
    });

    // --- Inicialização ---
    SwingUtilities.invokeLater(function() { instalarRendererDestaque(); });
    KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener(focusHandler);
    MainApplication.getLayerManager().addLayerChangeListener(layerHandler);
    scanTimer = new Timer(1000, monitorChangesetID);
    scanTimer.start();
    dialog.addWindowListener(new (Java.extend(WindowAdapter, { windowClosing: function(e) { fecharDialogoCompleto(); } })));
    dialog.setVisible(true);
})();