Facilita a separação de nós entre linhas ou polígonos grudados, a seleção pode ser feita entre linha/linha - linha/polígono - polígono/polígono.
Como funciona?
Selecione uma linha e um polígono
Defina a distância da separação
Ao selecionar Linha/Polígono, o id do nó é preservado na linha
Ao selecionar Polígono/Polígono, o id do nó é preservado na primeira seleção
Demonstração
Imagem.gif, clique para visualizar.
Código
fromorg.openstreetmap.josm.guiimportMainApplication,Notificationfromorg.openstreetmap.josm.data.osmimportDataSet,Way,Nodefromorg.openstreetmap.josm.data.coorimportLatLon,EastNorthfromorg.openstreetmap.josm.data.projectionimportProjectionRegistryfromorg.openstreetmap.josm.dataimportUndoRedoHandlerfromorg.openstreetmap.josm.commandimportChangeNodesCommand,AddCommand,SequenceCommandfromjavax.swingimportJOptionPane,JPanel,JLabel,JTextField,BoxLayout,Box,JSpinner,SpinnerNumberModel,UIManagerfromjava.awtimportDimensionimportmath# Utilidades de seleção/UIdefget_selected_valid_ways():active_layer=MainApplication.getLayerManager().getActiveDataLayer()ifnotactive_layer:Notification(u"Nenhuma camada de edição ativa.")\
.setIcon(UIManager.getIcon("OptionPane.errorIcon")).show()returnNoneds=active_layer.getDataSet()selected_ways=[wforwinds.getSelected()ifisinstance(w,Way)andnotw.isDeleted()andw.getNodesCount()>1]ifnotselected_ways:Notification(u"Nenhuma linha ou polígono selecionada.")\
.setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnNonereturnselected_waysdefget_user_input_for_distance():panel=JPanel()panel.setLayout(BoxLayout(panel,BoxLayout.Y_AXIS))panel.setPreferredSize(Dimension(450,100))panel.add(JLabel(u"Informe a distância de deslocamento (em metros):"))spinner=JSpinner(SpinnerNumberModel(10,0,50,5))spinner.setPreferredSize(Dimension(10,10))panel.add(spinner)panel.add(Box.createVerticalStrut(10))info_label=JLabel("<html><i><span style='color:#99ff00;'>"u"Ao selecionar Linha/Polígono, o id do nó é preservado na linha.<br>"u"Ao selecionar Polígono/Polígono, o id do nó é preservado na primeira seleção.""</span></i></html>")panel.add(info_label)result=JOptionPane.showConfirmDialog(None,panel,u"Configuração de Deslocamento",JOptionPane.OK_CANCEL_OPTION,JOptionPane.PLAIN_MESSAGE)ifresult==JOptionPane.OK_OPTION:returnspinner.getValue()returnNoneifresult==JOptionPane.OK_OPTION:try:distance_str=text_field.getText()ifnotdistance_str:Notification(u"Distância não informada.")\
.setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnNonedistance=float(distance_str)ifdistance==0:Notification(u"A distância não pode ser zero.")\
.setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnNonereturndistanceexceptValueError:Notification(u"Entrada inválida. Use número.")\
.setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnNone# Geometria / direçãodefis_clockwise(way,projection):nodes=way.getNodes()area=0foriinrange(len(nodes)-1):n1=projection.latlon2eastNorth(nodes[i].getCoor())n2=projection.latlon2eastNorth(nodes[i+1].getCoor())area+=(n2.east()-n1.east())*(n2.north()+n1.north())returnarea>0defcalculate_uniform_vector(node,way,distance):projection=ProjectionRegistry.getProjection()nodes=way.getNodes()node_index=-1fori,ninenumerate(nodes):ifn==node:node_index=ibreakifnode_index==-1:returnNoneprev_index=(node_index-1)%len(nodes)next_index=(node_index+1)%len(nodes)prev_node=nodes[prev_index]next_node=nodes[next_index]node_en=projection.latlon2eastNorth(node.getCoor())prev_en=projection.latlon2eastNorth(prev_node.getCoor())next_en=projection.latlon2eastNorth(next_node.getCoor())v1x=node_en.east()-prev_en.east()v1y=node_en.north()-prev_en.north()v2x=next_en.east()-node_en.east()v2y=next_en.north()-node_en.north()avgx=(v1x+v2x)/2.0avgy=(v1y+v2y)/2.0mag=math.sqrt(avgx*avgx+avgy*avgy)ifmag<1e-6:returnNoneifis_clockwise(way,projection):perpx=avgy/mag*distanceperpy=-avgx/mag*distanceelse:perpx=-avgy/mag*distanceperpy=avgx/mag*distancereturnEastNorth(node_en.east()+perpx,node_en.north()+perpy)defindices_of_node(way,node):idxs=[]nodes=way.getNodes()foriinrange(len(nodes)):ifnodes[i]==node:idxs.append(i)returnidxs# Planejamento em 2 passosdefbuild_separation_plan(all_selected_ways,distance_meters):ifnotall_selected_waysordistance_meters==0:return{},[]ds=all_selected_ways[0].getDataSet()projection=ProjectionRegistry.getProjection()# Mapeia cada nó para as vias selecionadas que o referenciamnode_to_selected_referrers={}forwayinall_selected_ways:ifway.isDeleted():continuefornodeinway.getNodes():ifnode.isDeleted():continuenode_to_selected_referrers.setdefault(node,[])ifwaynotinnode_to_selected_referrers[node]:node_to_selected_referrers[node].append(way)# Ajuda a respeitar a "primeira selecao" entre polígonosselected_order_index={w:ifori,winenumerate(all_selected_ways)}plan_per_way={}add_nodes=[]fororiginal_node,ways_usinginnode_to_selected_referrers.items():iforiginal_node.isDeleted():continueiflen(ways_using)<=1:continuepolygons=[wforwinways_usingifw.isClosed()]lines=[wforwinways_usingifnotw.isClosed()]polygons.sort(key=lambdaw:selected_order_index.get(w,10**9))# Caso 1: Linha + Polígono -> substituir nos polígonos, preservar na(s) linha(s)iflinesandpolygons:forpolyinpolygons:pos_en=calculate_uniform_vector(original_node,poly,distance_meters)ifpos_enisNone:continuenew_ll=projection.eastNorth2latlon(pos_en)new_node=Node(new_ll)idxs=indices_of_node(poly,original_node)ifnotidxs:continueplan_per_way.setdefault(poly,[])plan_per_way[poly].append({'indices':idxs,'new_node':new_node})add_nodes.append(new_node)# Caso 2: Polígono + Polígono -> preserva no primeiro polígono (pela ordem de seleção),substitui nos demais.elifpolygonsandnotlines:iflen(polygons)<2:continuepreserved_poly=polygons[0]forpolyinpolygons[1:]:pos_en=calculate_uniform_vector(original_node,poly,distance_meters)ifpos_enisNone:continuenew_ll=projection.eastNorth2latlon(pos_en)new_node=Node(new_ll)idxs=indices_of_node(poly,original_node)ifnotidxs:continueplan_per_way.setdefault(poly,[])plan_per_way[poly].append({'indices':idxs,'new_node':new_node})add_nodes.append(new_node)else:continuereturnplan_per_way,add_nodesdefapply_separation_plan(plan_per_way,add_nodes):ifnotplan_per_wayandnotadd_nodes:returnFalsecmds=[]seen=set()fornodeinadd_nodes:ifnodeinseen:continueseen.add(node)ds=MainApplication.getLayerManager().getEditDataSet()ifdsisNone:continuecmds.append(AddCommand(ds,node))forway,entriesinplan_per_way.items():current=list(way.getNodes())replaced_any=Falseforentryinentries:idxs=entry['indices']new_node=entry['new_node']foridxinidxs:if0<=idx<len(current):ifcurrent[idx]isnotnew_node:current[idx]=new_nodereplaced_any=Trueifreplaced_any:cmds.append(ChangeNodesCommand(way,current))ifcmds:seq=SequenceCommand("Separar nós compartilhados (2 passos)",cmds)UndoRedoHandler.getInstance().add(seq)returnTruereturnFalse# Validação da seleçãodefvalidate_selection(ways):ifnotwaysorlen(ways)<2:Notification("Selecione pelo menos dois caminhos.\n"u"Pode ser dois polígonos ou um polígono e uma linha.").setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnFalsehas_polygon=any(w.isClosed()forwinways)has_line=any(notw.isClosed()forwinways)ifnothas_polygonor(nothas_lineandlen([wforwinwaysifw.isClosed()])<2):Notification(u"Selecao inválida.\n"u"Deve conter dois polígonos ou um polígono e uma linha.").setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnFalse# Verifica se cada geometria compartilha pelo menos um nó com outraway_nodes_list=[set(w.getNodes())forwinways]fori,nodes_ainenumerate(way_nodes_list):connected=any(nodes_a&nodes_bforj,nodes_binenumerate(way_nodes_list)ifi!=j)ifnotconnected:Notification(u"Uma ou mais geometrias não compartilham nós com as outras.\n"u"A separação exige interseção de nós entre as geometrias selecionadas.").setIcon(UIManager.getIcon("OptionPane.warningIcon")).show()returnFalsereturnTrue# Maindefmain():ways=get_selected_valid_ways()ifnotways:returnifnotvalidate_selection(ways):returndistance=get_user_input_for_distance()ifdistanceisNone:returnplan_per_way,add_nodes=build_separation_plan(ways,distance)applied=apply_separation_plan(plan_per_way,add_nodes)ifapplied:Notification(u"Separação concluída!")\
.setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()else:Notification(u"Nenhuma alteração necessária (nada para separar).")\
.setIcon(UIManager.getIcon("OptionPane.informationIcon")).show()main()