Index: /trunk/data/ignoretags.cfg
===================================================================
--- /trunk/data/ignoretags.cfg	(revision 3669)
+++ /trunk/data/ignoretags.cfg	(revision 3669)
@@ -0,0 +1,363 @@
+# JOSM IgnoreTags
+;
+; Ignore valid and semi-valid keys that start with...
+;
+S:opengeodb
+S:openGeoDB
+S:name:
+S:note:
+S:tiger:
+S:gnis:
+S:census:
+S:au.gov.abs:
+S:qroti:
+S:is_in
+S:wikipedia
+S:source_ref:
+;
+; Ignore valid and semi-valid keys that equal...
+;
+E:loc_name
+E:attribution
+E:admin_level
+E:old_name
+E:operator
+E:usage
+E:construction
+E:collection
+E:addr:state
+E:import_uuid
+E:image
+E:url
+E:website
+E:postal_code
+E:source:boundary
+E:hour_on
+E:hour_off
+E:tower:type
+E:rcn_ref
+E:place_name
+E:cycleway
+E:abutters
+E:survey_date
+E:right:state
+E:left:state
+;
+; Ignore valid and semi-valid keys that end with...
+;
+F::nswgnb
+F::forward
+F::backward
+F::left
+F::right
+;
+; Misc Tags
+;
+K:type=is_in
+K:bridge=viaduct
+K:bridge=aqueduct
+K:bridge=swing
+;
+; Highway Key/Value Pairs
+;
+K:highway=motorway_link
+K:highway=trunk_link
+K:highway=primary_link
+K:highway=secondary_link
+K:oneway=-1
+;
+; traffic_calming Tags
+;
+K:traffic_calming=yes
+K:traffic_calming=bump
+K:traffic_calming=chicane
+K:traffic_calming=cushion
+K:traffic_calming=hump
+K:traffic_calming=rumble_strip
+K:traffic_calming=table
+K:traffic_calming=choker
+;
+; Aeroway Key/Value Pairs
+;
+K:aeroway=apron
+K:aeroway=hanger
+K:aeroway=helipad
+K:aeroway=runway
+K:aeroway=taxiway
+K:aeroway=terminal
+K:aeroway=aerodrome
+;
+; Amenity Key/Value Pairs
+;
+K:amenity=arts_centre
+K:amenity=atm
+K:amenity=baby_hatch
+K:amenity=bank
+K:amenity=bbq
+K:amenity=bench
+K:amenity=biergarten
+K:amenity=bicycle_parking
+K:amenity=bicycle_rental
+K:amenity=bureau_de_change
+K:amenity=bus_station
+K:amenity=brothel
+K:amenity=cafe
+K:amenity=car_rental
+K:amenity=car_sharing
+K:amenity=cinema
+K:amenity=college
+K:amenity=courthouse
+K:amenity=crematorium
+K:amenity=dentist
+K:amenity=doctors
+K:amenity=drinking_water
+K:amenity=embassy
+K:amenity=emergency_phone
+K:amenity=fast_food
+K:amenity=ferry_terminal
+K:amenity=fire_station
+K:amenity=food_court
+K:amenity=fountain
+K:amenity=fuel
+K:amenity=gallery
+K:amenity=grave_yard
+K:amenity=grit_bin
+K:amenity=gym
+K:amenity=hospital
+K:amenity=hunting_stand
+K:amenity=kindergarten
+K:amenity=library
+K:amenity=marketplace
+K:amenity=nightclub
+K:amenity=parking
+K:amenity=pharmacy
+K:amenity=place_of_worship
+K:amenity=police
+K:amenity=post_box
+K:amenity=post_office
+K:amenity=prison
+K:amenity=pub
+K:amenity=public_building
+K:amenity=recycling
+K:amenity=restaurant
+K:amenity=school
+K:amenity=shelter
+K:amenity=signpost
+K:amenity=studio
+K:amenity=taxi
+K:amenity=telephone
+K:amenity=theatre
+K:amenity=toilets
+K:amenity=townhall
+K:amenity=university
+K:amenity=vending_machine
+K:amenity=veterinary
+K:amenity=waste_basket
+K:amenity=waste_disposal
+;
+; Cuisine Tags
+;
+K:cuisine=coffee_shop
+K:cuisine=fish_and_chips
+K:cuisine=pie
+;
+; Cycleway Tags
+;
+K:cycleway=lane
+K:cycleway=track
+K:cycleway=opposite_lane
+K:cycleway=opposite
+K:cycleway=opposite_track
+;
+; Historic Tags
+;
+K:historic=castle
+K:historic=monument
+K:historic=memorial
+K:historic=archaeological_site
+K:historic=ruins
+K:historic=battlefield
+K:historic=wreck
+K:historic=yes
+;
+; Man_made Tags
+;
+T:man_made=pipeline|type=water
+T:man_made=pipeline|type=oil
+T:man_made=pipeline|type=gas
+T:man_made=pipeline|type=sewage
+T:man_made=pipeline|location=underground
+T:man_made=pipeline|location=underwater
+T:man_made=pipeline|location=overground
+;
+; Military Tags
+;
+K:military=airfield
+K:military=bunker
+K:military=barracks
+K:military=danger_area
+K:military=range
+K:military=naval_base
+;
+; Natural Tags
+;
+K:natural=bay
+K:natural=beach
+K:natural=cave_entrance
+K:natural=cliff
+K:natural=fell
+K:natural=glacier
+K:natural=heath
+K:natural=marsh
+K:natural=mud
+K:natural=peak
+K:natural=scree
+K:natural=scrub
+K:natural=spring
+K:natural=tree
+K:natural=volcano
+K:natural=wetland
+;
+; Surface Key/Value Pairs
+;
+K:surface=dirt
+K:surface=wood
+;
+; Relation Tags
+;
+K:relation=to
+K:relation=from
+;
+; Religious Key/Value Pairs
+;
+T:religion=christian|denomination=anglican
+T:religion=muslim|denomination=alaouite
+T:religion=jewish|denomination=alternative
+T:religion=christian|denomination=apostolic
+T:religion=jewish|denomination=ashkenazi
+T:religion=christian|denomination=baptist
+T:religion=christian|denomination=catholic
+T:religion=christian|denomination=christian_community
+T:religion=christian|denomination=christian_scientist
+T:religion=jewish|denomination=conservative
+T:religion=christian|denomination=coptic_orthodox
+T:religion=christian|denomination=czechoslovak_hussite
+T:religion=muslim|denomination=druze
+T:religion=christian|denomination=dutch_reformed
+T:religion=christian|denomination=evangelical
+T:religion=pastafarian|denomination=EVKdFSMiD
+T:religion=christian|denomination=foursquare
+T:religion=christian|denomination=greek_orthodox
+T:religion=jewish|denomination=hasidic
+T:religion=jewish|denomination=humanistic
+T:religion=muslim|denomination=ibadi
+T:religion=muslim|denomination=ismaili
+T:religion=christian|denomination=jehovahs_witness
+T:religion=christian|denomination=kabbalah
+T:religion=christian|denomination=karaite
+T:religion=jewish|denomination=liberal
+T:religion=christian|denomination=living_waters_church
+T:religion=christian|denomination=lutheran
+T:religion=christian|denomination=maronite
+T:religion=other|denomination=masonic
+T:religion=christian|denomination=mennonite
+T:religion=christian|denomination=methodist
+T:religion=jewish|denomination=modern_orthodox
+T:religion=christian|denomination=mormon
+T:religion=jewish|denomination=neo_orthodox
+T:religion=christian|denomination=new_apostolic
+T:religion=christian|denomination=nondenominational
+T:religion=jewish|denomination=nondenominational
+T:religion=muslim|denomination=nondenominational
+T:religion=christian|denomination=old_catholic
+T:religion=christian|denomination=orthodox
+T:religion=jewish|denomination=orthodox
+T:religion=christian|denomination=pentecostal
+T:religion=christian|denomination=presbyterian
+T:religion=jewish|denomination=progressive
+T:religion=christian|denomination=protestant
+T:religion=christian|denomination=quaker
+T:religion=jewish|denomination=reconstructionist
+T:religion=jewish|denomination=reform
+T:religion=jewish|denomination=renewal
+T:religion=christian|denomination=roman_catholic
+T:religion=christian|denomination=russian_orthodox
+T:religion=christian|denomination=salvation_army
+T:religion=jewish|denomination=samaritan
+T:religion=christian|denomination=seventh_day_adventist
+T:religion=muslim|denomination=shia
+T:religion=muslim|denomination=sunni
+T:religion=jewish|denomination=ultra_orthodox
+T:religion=christian|denomination=united
+T:religion=christian|denomination=united_reformed
+T:religion=christian|denomination=uniting
+;
+; Shop Key/Value Pairs
+;
+K:shop=alcohol
+K:shop=bakery
+K:shop=beverages
+K:shop=bicycle
+K:shop=books
+K:shop=butcher
+K:shop=car
+K:shop=car_repair
+K:shop=chemist
+K:shop=clothes
+K:shop=computer
+K:shop=confectionery
+K:shop=convenience
+K:shop=department_store
+K:shop=dry_cleaning
+K:shop=doityourself
+K:shop=electronics
+K:shop=florist
+K:shop=furniture
+K:shop=garden_centre
+K:shop=greengrocer
+K:shop=hairdresser
+K:shop=hardware
+K:shop=hifi
+K:shop=kiosk
+K:shop=laundry
+K:shop=mall
+K:shop=motorcycle
+K:shop=optician
+K:shop=organic
+K:shop=outdoor
+K:shop=sports
+K:shop=stationery
+K:shop=supermarket
+K:shop=shoes
+K:shop=toys
+K:shop=travel_agency
+K:shop=video
+;
+; Sports Tags
+;
+K:sport=boxing
+K:sport=netball
+;
+; Tourism Tags
+;
+K:tourism=alpine_hut
+K:tourism=attraction
+K:tourism=artwork
+K:tourism=camp_site
+K:tourism=caravan_site
+K:tourism=chalet
+K:tourism=guest_house
+K:tourism=hostel
+K:tourism=hotel
+K:tourism=information
+K:tourism=motel
+K:tourism=museum
+K:tourism=picnic_site
+K:tourism=theme_park
+K:tourism=viewpoint
+K:tourism=zoo
+K:tourism=yes
+;
+; Type Key/Value Pairs
+;
+K:type=collection
Index: /trunk/data/tagchecker.cfg
===================================================================
--- /trunk/data/tagchecker.cfg	(revision 3669)
+++ /trunk/data/tagchecker.cfg	(revision 3669)
@@ -0,0 +1,79 @@
+# JOSM TagChecker validator file
+
+# Format:
+# Each line specifies a certain error to be reported
+# <data type> : messagetype : <key><expression><value>
+#
+# Data type can be:
+#  node        - a node point
+#  way         - a way
+#  relation    - a relation
+#  *           - all data types
+#
+# Message type can be:
+# E            - an error
+# W            - a warning
+# I            - an low priority informational warning
+#
+# Key and value are expressions describing certain keys and values of these keys.
+# Regulator expressions are supported. In this case the expressions starts and
+# ends with // signs. If an 'i' is appended, the regular expression is
+# case insensitive.
+#
+# The * sign indicates any string.
+# The texts BOOLEAN_TRUE and BOOLEAN_FALSE in the value part indicate a special
+# handling for boolean values (yes, true, 0, false, no, ...).
+#
+# Expression can be:
+#  !=          - the key/value combination does not match
+#  ==          - the key/value combination does match
+#
+# To have more complicated expressions, multiple elements can be grouped together
+# with an logical and (&&).
+#
+# The comment at the end of a rule is displayed in validator description
+#
+# Empty lines and space signs are ignored
+
+way  : W : highway == * && name == /.* (Ave|Blvd|Cct|Cir|Cl|Cr|Crct|Cres|Crt|Ct|Dr|Drv|Esp|Espl|Hwy|Ln|Mw|Mwy|Pl|Rd|Qy|Qys|Sq|St|Str|Ter|Tce|Tr|Wy)\.?$/i               # abbreviated street name
+
+node : W : oneway == *                                         # oneway tag on a node
+node : W : bridge == BOOLEAN_TRUE                              # bridge tag on a node
+node : W : highway == tertiary                                 # wrong highway tag on a node
+node : W : highway == secondary                                # wrong highway tag on a node
+node : W : highway == residential                              # wrong highway tag on a node
+node : W : highway == unclassified                             # wrong highway tag on a node
+node : W : highway == track                                    # wrong highway tag on a node
+way  : W : highway == unclassified && name != *                # Unnamed unclassified highway
+way  : I : highway == secondary && ref != *                    # highway without a reference
+way  : I : highway == tertiary && ref != *                     # highway without a reference
+way  : I : highway == motorway && nat_ref != *                 # highway without a reference
+*    : W : highway == road                                     # temporary highway type
+*    : W : / *name */i == * && name != *                       # misspelled key name
+
+# The following could replace unnamed way check. Still at the moment we keep it as it is
+#way  : W : junction == roundabout && highway == /motorway|trunk|primary|secondary|tertiary|residential|pedestrian/ && /name|ref|(name:.*)|(.*_name)|(.*_ref)/ != * # Unnamed junction
+#way  : W : highway == /motorway|trunk|primary|secondary|tertiary|residential|pedestrian/ && /name|ref|(name:.*)|(.*_name)|(.*_ref)/ != * # Unnamed 
+
+way  : W : highway == cycleway && bicycle == BOOLEAN_FALSE     # cycleway with tag bicycle
+way  : W : highway == footway && foot == BOOLEAN_FALSE         # footway with tag foot
+#way  : I : highway == cycleway && bicycle == *                 # cycleway with tag bicycle
+#way  : I : highway == footway && foot == *                     # footway with tag foot
+way  : W : highway == cycleway && cycleway == lane             # separate cycleway as lane on a cycleway
+way  : W : highway == * && barrier == *                        # barrier used on a way
+
+#way  : I : waterway == * && layer != *                         # waterway without layer tag
+way  : I : highway == footway && maxspeed == *                 # maxspeed used for footway
+way  : I : highway == steps && maxspeed == *                   # maxspeed used for footway
+
+*    : W : layer == /\+.*/                                     # layer tag with + sign
+
+*    : I : name == /.*Strasse.*/i                              # street name contains ss
+
+relation : E : type != *                                       # relation without type
+
+node : I : amenity == /restaurant|cafe|fast_food/ && name != * # restaurant without name
+#way  : I : highway != * && railway != * && waterway != * && name == * # unusual named way type
+#*    : W : natural == water && waterway == *                   # unusual tag combination
+*    : W : highway == * && waterway == *                       # unusual tag combination
+*    : W : highway == * && natural == *                        # unusual tag combination
Index: /trunk/data/words.cfg
===================================================================
--- /trunk/data/words.cfg	(revision 3669)
+++ /trunk/data/words.cfg	(revision 3669)
@@ -0,0 +1,1116 @@
+# NOTE: Order *is* significant, case also.
+#
+# special symbols, must be first character:
+#  # Comment
+#  + correctly spelled word
+#  - incorrectly spelled word. Must follow correctly spelled word before next correctly spelled word.
+#
+# There must not be any white space before or after words, unless they are to be included in
+# the bad spelling.
++abutters
+-abuters
+-abbutter
+-abbutters
+-abuttors
+-abuutters
+-ubutters
++abutter
++address
++aka
++ambulance
++amenity
+-ameninty
+-amienty
+-amienity
+-amenitzy
+-amenety
+-amenitry
+-amnity
+-amenity 
+-amentiy
+-aminity
+-amneity
+-amnenity
+-aminety
+-amentity
+-ammenity
+-ameinty
+-anemity
+-amneity
+-amemity
+-ameity
+-amenity:
+-amenty
+-Amenity
+-maenity
+-emenity
++amenitylanduse
++atm
++Atta Oehlaweg
++bicycle
+-bycycle
+-biycle
+-bycicle
+-bicyle
++bike
+- bike
++biological
++by
++City
++class
+-Class
++classification
++ele
++emity
++email
++code_departement
++code_INSEE
++commercial
+-comercial
++comment
++COMMENTLINE
++confirmed
++created_by
+-created by
+-cretaed_by
+-crated_by
+-creared_by
+-creayed_by
+-{created_by
+- created_by
+-creeated_by
+-created_bu
++crossing
++denomination
+-denonimation
+-denomionation
+-denomation
+-demonination
+-demomination
+-denomition
+-denomincation
+-denominatation
+-denoination
++domination
++faith
++footway
++foot
+- foot
+-foor
++height
+-hieght
++highway
+-higwhay
+-highwaay
+-HighWay
+-huighway
+-highwayt
+-ghway
+-highaway
+-highwway
+-ighway
+-higjway
+-hioghway
+-hiway
+-hihjway
+-higheway
+-highwaY
+-hughway
+-hihgway
+-higoway
+-highwat
+-highwah
+-gighway
+-higyway
+-hichway
+-HIGHWAY
+-hingway
+-hhighway
+-highwayu
+-hyighway
+-hiughway
+-highwya
+-hifhway
+-hihway
+-hifgway
+-highway:
+-highjway
+-highway 
+-highwy
+-hgihway
+-highawy
+-highwau
+-highay
+-higbway
+-hignway
+-higway
+-highwayx
+- highway
+-hoghway
+-highwa
+-Highway
+-hghway
++highways
++highspeed
++highwaytype
++highway_type
++highwayunclassified
++horse
++hvg
++iata_ref
++icao_ref
++IM2
++image
+-imaqge
++island
++layer
+-layer 
+-elevation
+-leyer
+-lyaer
+- layer
++layer2
++layers
++layout
++leisure
+-leissure
+-leiruse
+-lesure
+-leisure 
+-Leisure
+-liesure
+-lieusure
+-lesiure
+-leasure
++naam
++name
+-nane
+-nme
+-namee
+-nname
+-naem
+-nmae
+-nacme
+-n ame
+-name`
+-namw
+-bame
+-nam
+- name
+-namr
+-name 
+-anme
+-Name
+-name:
+-NAME
+-name;
++name_1
++name_4
++named_by
++names
++name_right
++name_left
++note
+-Note
++note:fa
+-note:fa 
+-notes:fa
++Number
++oneway
+-aneway
+-onewway
+-onewau
+-oneay
+-neway
+-oneweay
+-onewa
+-omeway
+-one_way
+-onway
+- oneway
+-oeway
+-eway
++onte
++osmarender:nameDirection
+-name_direction
+-name-direction
+-osmarender:name_direction
++place
+-place 
+-Place
++passing
++passing_places
++railway
+- railway
+-Railway
+-raillway
++re
++regional_name
++ref
+-ref 
+-ref:
+-Ref
++ref:fa
++ref:source
++ref_nat
++ref_int
++retail
++time
++seats
++seventh_wonder_of_world
++square
++soccer
++source
+-sorce
+-soruce
+-soure
+-souce
+-sourec
+-aource
+-sourse
++source2
++source:ncn_ref
++source:highway
++SSSI
++status
++way
++waterway
+- waterway
+-waterwa
+-Waterway
+-waterwy
+-wateway
++wood
++unknown
+-unknwon
+#
+# Not sorted.
+#
++ 
++1
++4wd
++ car
++ class
++ horse
++ sport
++ open 09:00 - 16:00 daily
++ General McArthur lived here
++ Also coaches
++80n:ibm
++amenities
++bar
++batteries
++Bezeichnung
++boder
++Brand
++Food
++Fussweg
++Hauptstrasse
++POI
++Park
++Strasse
++abutment
++abutts
++access
++accident_and_emergency
++active_volcano
++aerialway
+-areilway
+-areialway
+-arielway
++aeroway
+-aeoroway
+-areoway
+-airoway
++ageofdgpsdata
++airport
+-aeroport
++airport_ident
++airport_ref
++airway
++alias
++alt
++altname
++alt_name
++alt_name_2
++alternative_name
++altitude
++alt_ref
++ame
++annotation
++annotate
++angle
++angle_to_last
++appearance
++approximate
++area
++art
++ascii_name
++asphalt
++author
++autocar
++b test tag
++badminton
++barnvagn
++barrier
++bb:name
++beach
++bicycleRoute
++bicycle_Route
++blackadder:name
++blackadder:commerce
++blackadder:cuisine
++blackadder:service
++blackadder:civic
++bicycles
++boat
++bogus_footpath_going_nowhere
++border
++borded
++border_edit
++border_type
++bottles
++boundary
++boundary_name
++boundary_type
++branch
++branch_code:fa
++branch_name:fa
++brand
++brewery
++bridge
+-brideg
+- bridge
+-brdige
+-bridgde
+-birdge
+-   bridge
+-bidge
+-brige
+-brigde
+-bridgw
++bridge_ref
++bridge_name
++bridleway
++building
+-buidling
+-buillding
+-buiding
+-bulding
++building_name
++build_date
++bus
++buss
++busway
++bus_routes
++building_type
++cafe
++capacity
++Car
++car
++carriage
++cars
++car park at rear
+- car park at rear
++car_repair
++carsharing
++category
++caution
++charge
++chemin
++checked_by
++cheshire_cycleway_ref
++christian_denomination
++cladding
++city
+-citya
++city_id
++clinic
++clothes
++cmt
++complete
+- complete
+-complite
++condition
++construction
++controlled
++converted_by
++core
++cost
++country
++course
++cover
++covered
++creator
++cycle
+-cyle
++cycleRoute
++cycle_route
++cycleway
+-cylceway
+-cycleway:
++cykel
++cuisine
++cusine
++cutting
++d_lat
++d_lon
++danger
++date
++date_off
++day_off
++day_on
++dead-end
++deadend
++depth
++desc
++description
+-descripion
+-desription
+-decription
++descriptions
++ details:naco
++destination
++difficulty
++direction
++direction_to
++dispensing
++distance
++distance_meter
++disused
++ECautomaton
++edited_some_more_by
++editor
++editor note
++ef
++elevated
++embankment
++emergency
++emergency_ward
++error
++exit
++exit_nr
++exit_name
++external_description
++external_link
++ev_charge
++facility
++farezone
++farm_vehicles
++fastfood
++FACC_CODE
++feature
+-featuer
++feature: NGIA map
+- feature: NGIA map
++features
++feet
++ferry
++fenced
++fire
++first_number
++fix
++fixme
++FIXME
++foobar
++food
++footpath
+-fottpath
++forrest
++foto
++free
++freight
+-frieght
++from_to
++from_zip
++fuel_diesel
++fuel_lpg
++fuel_octane_91
++fuel_octane_95
++fuel_octane_98
++full_name:fa
+-full_name:Fa
++full_name
++gate
++geoname_id
++glass
++glass_bottles
++glutenfree
++gluten_free
++goods
++gps_network
++grade
++grind
++gym
++halt
++hazard
++hame
++has_postalcode
++hdop
++helped_by
++heritage
++hgv
++highway E-number
++highway_border
+-highway_boarder
+-highway_boreder
++hill
++historic
++historic name
++historical
++history
++Higgy:ref
++hotelclass
++hospital:operator
++hour_off
++hour_on
++hours
++house_numbers
++iata
++icao
++id
+-ID
++import_ref
++incomplete
++incline_steep
++incline
++industrial
++info
++ info :naco
++infopoint
++informal_name
++int_name
+-int name
+-iint_name
++int_ref
++interpolation
++intersection
++is
++is_in
+-is in
+- is_in
++in_in
++is_in:de
++is_in:es
++junction
+-junktion
+-junctoin
+-junctioin
+-juntion
+-junctiion
+-jounction
+-jumction
+- junction
+-juction
+-Junction
+-junction 
++junction_ref
++junction:ref
++junction:name
++key
++Kingsmede
++label
++landuse
+-land_use
+-lansuse
+-lanudse
+-lanuse
++lane
++lanes
+-Lanes
++last_number
++ Last Edit: Batchoy 2006-12-14
++learning
++length
++level
++level crossing
++license
++liftStation
++liftType
++lighthouse
++line
++lines
++link:naming
++linje
++lit
++loc_ref
++loc_ref:fa
++loc_name
+-loc_name 
++loc_name:fa
++local_name
+-local_nama
++local_ref
++locality
++lock
++lock_gate
++long_name
++lorries
++Loyola Heights
++ma,e
++main
++major
++man_made
+-man-made
+-man_made 
+-mand_made
+-nan_made
+-madmade
+-manmade
++mapkey
++maplint:error
++maplint:notice
++maplint:warning
++mapping_status
+-mapping status
+-mappingstatus
++markedtrail
++marker
++marching_step
++minspeed
+-min_speed
++maxspeed
+-maspeed
+-max_speed
+-max speed
+-maxsepeed
+-maxseep
+-mayspeed
+-maxspeed 
+-maxpeed
++maxheight
+-max_hieght
+-max_height
++maxweight
+-maxwieght
+-max_weight
+-max_wieght
+-maxweihgt
++maxwidth
++membership
++memorial
++menu
++military
++missing_street
++mixed
++monument
++more_data
++motor
++motorbike
++motorcar
+-otorcar
+- motorcar
+-motocar
++motor_car
++motorcars
+-motocars
++motorcycle
+-motorcylce
+-motocycle
+-motorcycle 
++motorway
++motorway station
++motorway_junction
++munro
++museum
++nad_ref
++name.2
++name.alt
++name.en
++name:ar
++name:af
++name:cy
++name:de
++name:en
++name:en-cy
++name:es
++name:eu
++name:fa
++name:fi
++name:fr
++name:gd
++name:la
++name:my
++name:nl
++name:non
++name:ru
++name:sv
++name.se
++name.short
++name:zh-Latn
++name1
++name2
++name_alt
++name int
++name_int
++name:source
++name_source
++name:cym
++name_ie
++name_loc
++name_segment
++namelayer
++name:ref
++name_ref
++nat_name
++nat_ref
++nat_pref
++nat_reg
++natural
+-nataural
+- natural
+-natrual
+-nautral
+-natural 
++natural2
++nature
++navigable
++ncn_name
++ncn_ref
++ncn:ref
++ncn_route
++needs_to_be
++network
++net_ref
++newforest:pathtype
++newsagent_code
++nickb_marker
++nickname
++nlanes
++noat
++node
++noentry
++noexit
+- noexit
++notes
++note_1
+-note_!
++note_2
+-note2
++note_3
++note_4
++note_
++noturn
++number
++numbers
++obstacles
++obstruction
++official
++ojw2
++ojw_test
++old_name
+-oldname
++old_name:fa
++old_full_name:fa
++old_ref
++opened
++open_in
++operator
++opm:capacity
++opm:difficulty
++opm:liftStation
++opm:liftType
+-opm:lifttype
++osmarender:renderName
+-osmarender:rendername
+-soamrender:renderName
++osmarender:renderRef
+-osmrender:renderRef
++owner
++owners
++park_and_ride
++parking
++parking:cost
++parking:spaces
++passenger
++path
++paved
++pcv_only
++pdop
++pedestrian
++pedestrians
++permissive
++petrol lhs
++petrol petron
++petrol
++phone
++phone_number
++physical
++place_postal
++place numbers
++place_code
++place_name
+- place_name
++place_numbers
++place_of_worship
++plave
++playe
++plant
++poi
++point_of_interest
++population
++popul
++port
++position_accuracy
++possible_name
++post_code
++postal_code
+- postal_code
+-posatl_code
++postcode
++post_office
++power
++primary
++private
+-privat
++provided
++Properties
++problem
++psv
++public
++public_transportation
++punting
++quality
++rail
++railroad
++railway_tracks
++ramp
++recreation
++recycling
++recycling:batteries
++recycling:bicycles
++recycling:books
++recycling:clothing
++recycling:clothes
++recycling:glas
+-recyling:glas
++recycling:shoes
++recycling:engine_oil
++recycling:glass_bottles
++recycling:green_waste
++recycling:magazines
++recycling:mobile_phones
++recycling:newspaper
++recycling:newspapers
++recycling:printer_cartridges
++recycling:cardboard
++recycling:music
++recycling:paper
+-reycling:paper
++recycle:plastic_bottles
++recycle:paper
++recycle:magazines
++recycle:cardboard
++recycle:cans
++recycling:cans
++recycling:plastic_bottles
++recycline:cardboard
++recycle:glass_bottles
++recycling:cork
++recycling:glass
++recycling:plastic
++recycling:plastic_bags
++recycling:plasic_bottles
++recycling:scrap_metal
++recycling:white_goods
++recycling:wood
++recycling:tyres
++red
++reference
++ref_direction
++reg
++reg_name
++reg_ref
++reg_reg
++region_id
++religion
+-relgion
++residence
++residential
+-residentail
++restriction
+-restrction
+-Restriction
++restricted
++restrictions
+-Restrictions
+-Restrictions 
++riverwidth
++river_width
++road
++rollerblade
++roundabout
++route
++routing
++rue
++runway
++sat
++sculpture
++service
++shape
++sheltered
++shortcut
++sidewalk
++sign
++single_track
++singletrack
++shop
++shopping
++size
++ski
++slope
++snowboarding
++some_data
++source:loc_name
++source:old_name
++source:old_ref
++source:name
+-source:Name
++source:ref
++source_ref
++source_ref:ref
++source_ref:name
++source:uri
++source_uri
++source:url
+-source_url
++source:oneway
++southglos:heritagetrail
++species
++speed
++speed_limit
++speed limit
++speedlimit
++speedevil
++sport
++sport_2
++sports
++stairs
++start_date
++state
++station
++stream
++street
++street_name
++steps
++structure
++subtype
++suburb
++suggested
++surface
+-sruface
++surfaced
++survey_ref
++survey
+- survey
++svg:font-size
++svg:stroke-width
++svg_font-size
++svg:stroke-dasharray
++swimming
++sym
++symbol
++symbolic
++tag
++taxi
++telephone
++telephone:operator
++telephone_number
++telephonenumber
++telephone_type
++test
++testing
++testnode
++ this was a children's playarea with a path leading through it. Didn't want to look too dodgy :)
++the Netherlands
++NL
++thoroughfare
++time_diff
++times
++tractor
++track
++tracktype_1
++to_zip
++todo
++TODO
++toll
++tollway
++topspeed
++tourism
+-tourismn
+-tourim
+-tourism 
+-toursim
+-touristm
++tourist
++tourist_attraction
++towards
++town
++towpath
++tracks
++traffic
++traffic_signals
++train
++tracktype
+-tractype
+-tracltype
+-trackype
++tram
++tramline
+-tram_line
++true
++truck
++tube
++tune
++tunnel
+-tunne
+-tunnel 
+-Tunnel
+-tunel
+-tunnely
++turn_right
++typ
++type
+-tyoe
++type.en
++uk:row
++unclassified
++upload_tag
++uploader
++uploaded_by
++uri
++url
++use
++use_status
++usage
++utility
++vdop
++vehicle
++viaduct
++vicar
++view
++village
++visited_by
++volcano
++voltage
++warning
++water
++waterfall
++wayclass
++waypoint
++web
++website
++website:official
++weight_limit
++wide
++width
++width_restriction
++wiki
++wikipedia
++wikipedia:es
++wiki:nl
++wrong
++www
++zip
++zip_code
++{}
Index: /trunk/src/org/openstreetmap/josm/Main.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/Main.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/Main.java	(revision 3669)
@@ -52,7 +52,9 @@
 import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
 import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.gui.GettingStarted;
 import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
 import org.openstreetmap.josm.gui.io.SaveLayersDialog;
@@ -136,8 +138,9 @@
     public final MainMenu menu;
 
+    public final OsmValidator validator;
     /**
      * The MOTD Layer.
      */
-    private GettingStarted gettingStarted=new GettingStarted();
+    private GettingStarted gettingStarted = new GettingStarted();
 
     /**
@@ -217,4 +220,7 @@
         TaggingPresetPreference.initialize();
         MapPaintPreference.initialize();
+
+        validator = new OsmValidator();
+        MapView.addLayerChangeListener(validator);
 
         toolbar.refreshToolbarControl();
@@ -387,8 +393,6 @@
         if (args.containsKey("geometry")) {
             geometry = args.get("geometry").iterator().next();
-            // Main.debug("Main window geometry from args: \""+geometry+"\"");
         } else {
             geometry = Main.pref.get("gui.geometry");
-            // Main.debug("Main window geometry from preferences: \""+geometry+"\"");
         }
         if (geometry.length() != 0) {
Index: /trunk/src/org/openstreetmap/josm/actions/UploadAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/UploadAction.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/actions/UploadAction.java	(revision 3669)
@@ -16,4 +16,5 @@
 import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook;
 import org.openstreetmap.josm.actions.upload.UploadHook;
+import org.openstreetmap.josm.actions.upload.ValidateUploadHook;
 import org.openstreetmap.josm.data.APIDataSet;
 import org.openstreetmap.josm.data.conflict.ConflictCollection;
@@ -50,4 +51,5 @@
     private static final LinkedList<UploadHook> uploadHooks = new LinkedList<UploadHook>();
     static {
+        uploadHooks.add(new ValidateUploadHook());
         /**
          * Checks server capabilities before upload.
Index: /trunk/src/org/openstreetmap/josm/actions/ValidateAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/ValidateAction.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/actions/ValidateAction.java	(revision 3669)
@@ -0,0 +1,187 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.AgregatePrimitivesVisitor;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.tools.Shortcut;
+import org.xml.sax.SAXException;
+
+/**
+ * The action that does the validate thing.
+ * <p>
+ * This action iterates through all active tests and give them the data, so that
+ * each one can test it.
+ *
+ * @author frsantos
+ */
+public class ValidateAction extends JosmAction {
+    /** Serializable ID */
+    private static final long serialVersionUID = -2304521273582574603L;
+
+    /** Last selection used to validate */
+    private Collection<OsmPrimitive> lastSelection;
+
+    /**
+     * Constructor
+     */
+    public ValidateAction() {
+        super(tr("Validation"), "dialogs/validator", tr("Performs the data validation"),
+        Shortcut.registerShortcut("tools:validate", tr("Tool: {0}", tr("Validation")), KeyEvent.VK_V, Shortcut.GROUP_EDIT, Shortcut.SHIFT_DEFAULT), true);
+    }
+
+    public void actionPerformed(ActionEvent ev) {
+        doValidate(ev, true);
+    }
+
+    /**
+     * Does the validation.
+     * <p>
+     * If getSelectedItems is true, the selected items (or all items, if no one
+     * is selected) are validated. If it is false, last selected items are
+     * revalidated
+     *
+     * @param ev The event
+     * @param getSelectedItems If selected or last selected items must be validated
+     */
+    public void doValidate(ActionEvent ev, boolean getSelectedItems) {
+        if (Main.main.validator.validateAction == null || Main.map == null || !Main.map.isVisible())
+            return;
+
+        OsmValidator.initializeErrorLayer();
+
+        Collection<Test> tests = OsmValidator.getEnabledTests(false);
+        if (tests.isEmpty())
+            return;
+
+        Collection<OsmPrimitive> selection;
+        if (getSelectedItems) {
+            selection = Main.main.getCurrentDataSet().getSelected();
+            if (selection.isEmpty()) {
+                selection = Main.main.getCurrentDataSet().allNonDeletedPrimitives();
+                lastSelection = null;
+            } else {
+                AgregatePrimitivesVisitor v = new AgregatePrimitivesVisitor();
+                selection = v.visit(selection);
+                lastSelection = selection;
+            }
+        } else {
+            if (lastSelection == null)
+                selection = Main.main.getCurrentDataSet().allNonDeletedPrimitives();
+            else
+                selection = lastSelection;
+        }
+
+        ValidationTask task = new ValidationTask(tests, selection, lastSelection);
+        Main.worker.submit(task);
+    }
+
+    @Override
+    public void updateEnabledState() {
+        setEnabled(getEditLayer() != null);
+    }
+
+    /**
+     * Asynchronous task for running a collection of tests against a collection
+     * of primitives
+     *
+     */
+
+    class ValidationTask extends PleaseWaitRunnable {
+        private Collection<Test> tests;
+        private Collection<OsmPrimitive> validatedPrimitmives;
+        private Collection<OsmPrimitive> formerValidatedPrimitives;
+        private boolean canceled;
+        private List<TestError> errors;
+
+        /**
+         *
+         * @param tests  the tests to run
+         * @param validatedPrimitives the collection of primitives to validate.
+         * @param formerValidatedPrimitives the last collection of primitives being validates. May be null.
+         */
+        public ValidationTask(Collection<Test> tests, Collection<OsmPrimitive> validatedPrimitives, Collection<OsmPrimitive> formerValidatedPrimitives) {
+            super(tr("Validating"), false /*don't ignore exceptions */);
+            this.validatedPrimitmives  = validatedPrimitives;
+            this.formerValidatedPrimitives = formerValidatedPrimitives;
+            this.tests = tests;
+        }
+
+        @Override
+        protected void cancel() {
+            this.canceled = true;
+        }
+
+        @Override
+        protected void finish() {
+            if (canceled) return;
+
+            // update GUI on Swing EDT
+            //
+            Runnable r = new Runnable()  {
+                public void run() {
+                    Main.map.validatorDialog.tree.setErrors(errors);
+                    Main.map.validatorDialog.unfurlDialog();
+                    Main.main.getCurrentDataSet().fireSelectionChanged();
+                }
+            };
+            if (SwingUtilities.isEventDispatchThread()) {
+                r.run();
+            } else {
+                SwingUtilities.invokeLater(r);
+            }
+        }
+
+        @Override
+        protected void realRun() throws SAXException, IOException,
+                OsmTransferException {
+            if (tests == null || tests.isEmpty()) return;
+            errors = new ArrayList<TestError>(200);
+            getProgressMonitor().setTicksCount(tests.size() * validatedPrimitmives.size());
+            int testCounter = 0;
+            for (Test test : tests) {
+                if (canceled) return;
+                testCounter++;
+                getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, tests.size(),test.getName()));
+                test.setPartialSelection(formerValidatedPrimitives != null);
+                test.startTest(getProgressMonitor().createSubTaskMonitor(validatedPrimitmives.size(), false));
+                test.visit(validatedPrimitmives);
+                test.endTest();
+                errors.addAll(test.getErrors());
+            }
+            tests = null;
+            if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+                getProgressMonitor().subTask(tr("Updating ignored errors ..."));
+                for (TestError error : errors) {
+                    if (canceled) return;
+                    List<String> s = new ArrayList<String>();
+                    s.add(error.getIgnoreState());
+                    s.add(error.getIgnoreGroup());
+                    s.add(error.getIgnoreSubGroup());
+                    for (String state : s) {
+                        if (state != null && OsmValidator.hasIgnoredError(state)) {
+                            error.setIgnored(true);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/actions/upload/ValidateUploadHook.java	(revision 3669)
@@ -0,0 +1,126 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.actions.upload;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.APIDataSet;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.AgregatePrimitivesVisitor;
+import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * The action that does the validate thing.
+ * <p>
+ * This action iterates through all active tests and give them the data, so that
+ * each one can test it.
+ *
+ * @author frsantos
+ */
+public class ValidateUploadHook implements UploadHook
+{
+    /** Serializable ID */
+    private static final long serialVersionUID = -2304521273582574603L;
+
+    /**
+     * Validate the modified data before uploading
+     */
+    public boolean checkUpload(APIDataSet apiDataSet)
+    {
+        Collection<Test> tests = OsmValidator.getEnabledTests(true);
+        if (tests.isEmpty())
+            return true;
+
+        AgregatePrimitivesVisitor v = new AgregatePrimitivesVisitor();
+        v.visit(apiDataSet.getPrimitivesToAdd());
+        Collection<OsmPrimitive> selection = v.visit(apiDataSet.getPrimitivesToUpdate());
+
+        List<TestError> errors = new ArrayList<TestError>(30);
+        for(Test test : tests)
+        {
+            test.setBeforeUpload(true);
+            test.setPartialSelection(true);
+            test.startTest(null);
+            test.visit(selection);
+            test.endTest();
+            if(Main.pref.getBoolean(ValidatorPreference.PREF_OTHER_UPLOAD, false))
+                errors.addAll( test.getErrors() );
+            else
+            {
+                for(TestError e : test.getErrors())
+                {
+                    if(e.getSeverity() != Severity.OTHER)
+                        errors.add(e);
+                }
+            }
+        }
+        tests = null;
+        if(errors == null || errors.isEmpty())
+            return true;
+
+        if(Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true))
+        {
+            int nume = 0;
+            for(TestError error : errors)
+            {
+                List<String> s = new ArrayList<String>();
+                s.add(error.getIgnoreState());
+                s.add(error.getIgnoreGroup());
+                s.add(error.getIgnoreSubGroup());
+                for(String state : s)
+                {
+                    if(state != null && OsmValidator.hasIgnoredError(state))
+                    {
+                        error.setIgnored(true);
+                    }
+                }
+                if(!error.getIgnored())
+                    ++nume;
+            }
+            if(nume == 0)
+                return true;
+        }
+        return displayErrorScreen(errors);
+    }
+
+    /**
+     * Displays a screen where the actions that would be taken are displayed and
+     * give the user the possibility to cancel the upload.
+     * @param errors The errors displayed in the screen
+     * @return <code>true</code>, if the upload should continue. <code>false</code>
+     *          if the user requested cancel.
+     */
+    private boolean displayErrorScreen(List<TestError> errors)
+    {
+        JPanel p = new JPanel(new GridBagLayout());
+        ValidatorTreePanel errorPanel = new ValidatorTreePanel(errors);
+        errorPanel.expandAll();
+        p.add(new JScrollPane(errorPanel), GBC.eol());
+
+        int res  = JOptionPane.showConfirmDialog(Main.parent, p,
+        tr("Data with errors. Upload anyway?"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+        if(res == JOptionPane.NO_OPTION)
+        {
+            OsmValidator.initializeErrorLayer();
+            Main.map.validatorDialog.unfurlDialog();
+            Main.map.validatorDialog.tree.setErrors(errors);
+            Main.main.getCurrentDataSet().fireSelectionChanged();
+        }
+        return res == JOptionPane.YES_OPTION;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/GridLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/GridLayer.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/GridLayer.java	(revision 3669)
@@ -0,0 +1,204 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.geom.Point2D;
+
+import javax.swing.Action;
+import javax.swing.Icon;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.validation.util.ValUtil;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * A debug layer for testing the grid cells a way crosses.
+ *
+ * @author frsantos
+ */
+public class GridLayer extends Layer
+{
+    /**
+     * Constructor
+     * @param name
+     */
+    public GridLayer(String name)
+    {
+        super(name);
+    }
+
+    /**
+     * Return a static icon.
+     */
+    @Override public Icon getIcon() {
+        return ImageProvider.get("layer", "validator");
+    }
+
+    /**
+     * Draw the grid and highlight all cells acuppied by any selected primitive.
+     */
+    @Override
+    public void paint(final Graphics2D g, final MapView mv, Bounds bounds)
+    {
+        if( !Main.pref.hasKey(ValidatorPreference.PREF_DEBUG + ".grid") )
+            return;
+
+        int gridWidth = Integer.parseInt(Main.pref.get(ValidatorPreference.PREF_DEBUG + ".grid") );
+        int width = mv.getWidth();
+        int height = mv.getHeight();
+
+        EastNorth origin = mv.getEastNorth(0, 0);
+        EastNorth border = mv.getEastNorth(width, height);
+
+        if( border.east() * gridWidth > 50 )
+            return;
+
+        g.setColor(Color.RED.darker().darker());
+        HighlightCellVisitor visitor = new HighlightCellVisitor(g, mv, gridWidth);
+        for(OsmPrimitive p : Main.main.getCurrentDataSet().getSelected() )
+            p.visit(visitor);
+
+        long x0 = (long)Math.floor(origin.east()  * gridWidth);
+        long x1 = (long)Math.floor(border.east()  * gridWidth);
+        long y0 = (long)Math.floor(origin.north() * gridWidth) + 1;
+        long y1 = (long)Math.floor(border.north() * gridWidth) + 1;
+        long aux;
+        if( x0 > x1 ) { aux = x0; x0 = x1; x1 = aux; }
+        if( y0 > y1 ) { aux = y0; y0 = y1; y1 = aux; }
+
+        g.setColor(Color.RED.brighter().brighter());
+        for( double x = x0; x <= x1; x++)
+        {
+            Point point = mv.getPoint( new EastNorth(x/gridWidth, 0));
+            g.drawLine(point.x, 0, point.x, height);
+        }
+
+        for( double y = y0; y <= y1; y++)
+        {
+            Point point = mv.getPoint( new EastNorth(0, y/gridWidth));
+            g.drawLine(0, point.y, width, point.y);
+        }
+    }
+
+    @Override
+    public String getToolTipText()
+    {
+        return null;
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {}
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {}
+
+    @Override
+    public Object getInfoComponent()
+    {
+        return getToolTipText();
+    }
+
+    @Override
+    public Action[] getMenuEntries()
+    {
+        return new Action[] {
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                new RenameLayerAction(null, this),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this)};
+    }
+
+    @Override public void destroy() { }
+
+    /**
+     * Visitor that highlights all cells the selected primitives go through
+     */
+    static class HighlightCellVisitor extends AbstractVisitor
+    {
+        /** The MapView */
+        private final MapView mv;
+        /** The graphics */
+        private final Graphics g;
+        /** The grid width */
+        private final int gridDetail;
+        /** The width of a cell */
+        private int cellWidth;
+
+        /**
+         * Constructor
+         * @param g the graphics
+         * @param mv The MapView
+         * @param gridDetail The grid detail
+         */
+        public HighlightCellVisitor(final Graphics g, final MapView mv, int gridDetail)
+        {
+            this.g = g;
+            this.mv = mv;
+            this.gridDetail = gridDetail;
+
+            Point p = mv.getPoint( new EastNorth(0, 0) );
+            Point p2 = mv.getPoint( new EastNorth(1d/gridDetail, 1d/gridDetail) );
+            cellWidth = Math.abs(p2.x - p.x);
+        }
+
+        public void visit(Node n)
+        {
+            double x = n.getEastNorth().east() * gridDetail;
+            double y = n.getEastNorth().north()* gridDetail + 1;
+
+            drawCell( Math.floor(x), Math.floor(y) );
+        }
+
+        public void visit(Way w)
+        {
+            Node lastN = null;
+            for (Node n : w.getNodes()) {
+                if (lastN == null) {
+                    lastN = n;
+                    continue;
+                }
+                for (Point2D p : ValUtil.getSegmentCells(lastN, n, gridDetail)) {
+                    drawCell( p.getX(), p.getY() );
+                }
+                lastN = n;
+            }
+        }
+
+        public void visit(Relation r) {}
+
+        /**
+         * Draws a solid cell at the (x,y) location
+         * @param x
+         * @param y
+         */
+        protected void drawCell(double x, double y)
+        {
+            Point p = mv.getPoint( new EastNorth(x/gridDetail, y/gridDetail) );
+            g.fillRect(p.x, p.y, cellWidth, cellWidth);
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/OsmValidator.java	(revision 3669)
@@ -0,0 +1,295 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ValidateAction;
+import org.openstreetmap.josm.actions.upload.ValidateUploadHook;
+import org.openstreetmap.josm.data.projection.Epsg4326;
+import org.openstreetmap.josm.data.projection.Lambert;
+import org.openstreetmap.josm.data.projection.Mercator;
+import org.openstreetmap.josm.data.validation.tests.Coastlines;
+import org.openstreetmap.josm.data.validation.tests.CrossingWays;
+import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
+import org.openstreetmap.josm.data.validation.tests.DuplicateWay;
+import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes;
+import org.openstreetmap.josm.data.validation.tests.MultipolygonTest;
+import org.openstreetmap.josm.data.validation.tests.NameMismatch;
+import org.openstreetmap.josm.data.validation.tests.NodesWithSameName;
+import org.openstreetmap.josm.data.validation.tests.OverlappingWays;
+import org.openstreetmap.josm.data.validation.tests.RelationChecker;
+import org.openstreetmap.josm.data.validation.tests.SelfIntersectingWay;
+import org.openstreetmap.josm.data.validation.tests.SimilarNamedWays;
+import org.openstreetmap.josm.data.validation.tests.TagChecker;
+import org.openstreetmap.josm.data.validation.tests.TurnrestrictionTest;
+import org.openstreetmap.josm.data.validation.tests.UnclosedWays;
+import org.openstreetmap.josm.data.validation.tests.UnconnectedWays;
+import org.openstreetmap.josm.data.validation.tests.UntaggedNode;
+import org.openstreetmap.josm.data.validation.tests.UntaggedWay;
+import org.openstreetmap.josm.data.validation.tests.WronglyOrderedWays;
+import org.openstreetmap.josm.data.validation.util.ValUtil;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.dialogs.ValidatorDialog;
+import org.openstreetmap.josm.gui.layer.ValidatorLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+
+/**
+ *
+ * A OSM data validator
+ *
+ * @author Francisco R. Santos <frsantos@gmail.com>
+ */
+public class OsmValidator implements LayerChangeListener {
+
+    public static ValidatorLayer errorLayer = null;
+
+    /** The validate action */
+    public ValidateAction validateAction = new ValidateAction();
+
+    /** The validation dialog */
+    ValidatorDialog validationDialog;
+
+    /** Grid detail, multiplier of east,north values for valuable cell sizing */
+    public static double griddetail;
+
+    public static Collection<String> ignoredErrors = new TreeSet<String>();
+
+    /**
+     * All available tests
+     * TODO: is there any way to find out automatically all available tests?
+     */
+    @SuppressWarnings("unchecked")
+    public static Class<Test>[] allAvailableTests = new Class[] {
+            DuplicateNode.class, // ID    1 ..   99
+            OverlappingWays.class, // ID  101 ..  199
+            UntaggedNode.class, // ID  201 ..  299
+            UntaggedWay.class, // ID  301 ..  399
+            SelfIntersectingWay.class, // ID  401 ..  499
+            DuplicatedWayNodes.class, // ID  501 ..  599
+            CrossingWays.class, // ID  601 ..  699
+            SimilarNamedWays.class, // ID  701 ..  799
+            NodesWithSameName.class, // ID  801 ..  899
+            Coastlines.class, // ID  901 ..  999
+            WronglyOrderedWays.class, // ID 1001 .. 1099
+            UnclosedWays.class, // ID 1101 .. 1199
+            TagChecker.class, // ID 1201 .. 1299
+            UnconnectedWays.class, // ID 1301 .. 1399
+            DuplicateWay.class, // ID 1401 .. 1499
+            NameMismatch.class, // ID  1501 ..  1599
+            MultipolygonTest.class, // ID  1601 ..  1699
+            RelationChecker.class, // ID  1701 ..  1799
+            TurnrestrictionTest.class, // ID  1801 ..  1899
+    };
+
+    public OsmValidator() {
+        checkPluginDir();
+        initializeGridDetail();
+        initializeTests(getTests());
+        loadIgnoredErrors(); //FIXME: load only when needed
+    }
+
+    /**
+     * Check if plugin directory exists (store ignored errors file)
+     */
+    private void checkPluginDir() {
+        try {
+        File pathDir = new File(ValUtil.getPluginDir());
+        if (!pathDir.exists())
+            pathDir.mkdirs();
+        } catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    private void loadIgnoredErrors() {
+        ignoredErrors.clear();
+        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+            try {
+                final BufferedReader in = new BufferedReader(new FileReader(ValUtil.getPluginDir() + "ignorederrors"));
+                for (String line = in.readLine(); line != null; line = in.readLine()) {
+                    ignoredErrors.add(line);
+                }
+            } catch (final FileNotFoundException e) {
+                // Ignore
+            } catch (final IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static void addIgnoredError(String s) {
+        ignoredErrors.add(s);
+    }
+
+    public static boolean hasIgnoredError(String s) {
+        return ignoredErrors.contains(s);
+    }
+
+    public static void saveIgnoredErrors() {
+        try {
+            final PrintWriter out = new PrintWriter(new FileWriter(ValUtil.getPluginDir() + "ignorederrors"), false);
+            for (String e : ignoredErrors)
+                out.println(e);
+            out.close();
+        } catch (final IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private ValidateUploadHook uploadHook;
+
+//    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+//        if (newFrame != null) {
+//            initializeErrorLayer();
+//            if (Main.pref.hasKey(ValidatorPreference.PREF_DEBUG + ".grid"))
+//                Main.main.addLayer(new GridLayer(tr("Grid")));
+//        }
+//    }
+
+    public static void initializeErrorLayer() {
+        if (!Main.pref.getBoolean(ValidatorPreference.PREF_LAYER, true))
+            return;
+        if (errorLayer == null) {
+            errorLayer = new ValidatorLayer();
+            Main.main.addLayer(errorLayer);
+        }
+    }
+
+    /** Gets a map from simple names to all tests. */
+    public static Map<String, Test> getAllTestsMap() {
+        Map<String, Test> tests = new HashMap<String, Test>();
+        for (Class<Test> testClass : getAllAvailableTests()) {
+            try {
+                Test test = testClass.newInstance();
+                tests.put(testClass.getSimpleName(), test);
+            } catch (Exception e) {
+                e.printStackTrace();
+                continue;
+            }
+        }
+        applyPrefs(tests, false);
+        applyPrefs(tests, true);
+        return tests;
+    }
+
+    private static void applyPrefs(Map<String, Test> tests, boolean beforeUpload) {
+        Pattern regexp = Pattern.compile("(\\w+)=(true|false),?");
+        Matcher m = regexp.matcher(Main.pref.get(beforeUpload ? ValidatorPreference.PREF_TESTS_BEFORE_UPLOAD
+                : ValidatorPreference.PREF_TESTS));
+        int pos = 0;
+        while (m.find(pos)) {
+            String testName = m.group(1);
+            Test test = tests.get(testName);
+            if (test != null) {
+                boolean enabled = Boolean.valueOf(m.group(2));
+                if (beforeUpload) {
+                    test.testBeforeUpload = enabled;
+                } else {
+                    test.enabled = enabled;
+                }
+            }
+            pos = m.end();
+        }
+    }
+
+    public static Collection<Test> getTests() {
+        return getAllTestsMap().values();
+    }
+
+    public static Collection<Test> getEnabledTests(boolean beforeUpload) {
+        Collection<Test> enabledTests = getTests();
+        for (Test t : new ArrayList<Test>(enabledTests)) {
+            if (beforeUpload ? t.testBeforeUpload : t.enabled)
+                continue;
+            enabledTests.remove(t);
+        }
+        return enabledTests;
+    }
+
+    /**
+     * Gets the list of all available test classes
+     *
+     * @return An array of the test classes
+     */
+    public static Class<Test>[] getAllAvailableTests() {
+        return allAvailableTests;
+    }
+
+    /**
+     * Initialize grid details based on current projection system. Values based on
+     * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&error
+     * until most bugs were discovered while keeping the processing time reasonable)
+     */
+    public void initializeGridDetail() {
+        if (Main.proj.toString().equals(new Epsg4326().toString()))
+            OsmValidator.griddetail = 10000;
+        else if (Main.proj.toString().equals(new Mercator().toString()))
+            OsmValidator.griddetail = 100000;
+        else if (Main.proj.toString().equals(new Lambert().toString()))
+            OsmValidator.griddetail = 0.1;
+    }
+
+    /**
+     * Initializes all tests
+     * @param allTests The tests to initialize
+     */
+    public static void initializeTests(Collection<Test> allTests) {
+        for (Test test : allTests) {
+            try {
+                if (test.enabled) {
+                    test.initialize();
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+                JOptionPane.showMessageDialog(Main.parent,
+                        tr("Error initializing test {0}:\n {1}", test.getClass()
+                        .getSimpleName(), e),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE);
+            }
+        }
+    }
+
+    /* -------------------------------------------------------------------------- */
+    /* interface LayerChangeListener                                              */
+    /* -------------------------------------------------------------------------- */
+    public void activeLayerChange(Layer oldLayer, Layer newLayer) {}
+
+    public void layerAdded(Layer newLayer) {}
+
+    public void layerRemoved(Layer oldLayer) {
+        if (oldLayer instanceof OsmDataLayer && Main.map.mapView.getActiveLayer() == oldLayer) {
+            Main.map.validatorDialog.tree.setErrorList(new ArrayList<TestError>());
+        }
+        if (oldLayer == errorLayer) {
+            errorLayer = null;
+            return;
+        }
+        if (Main.map.mapView.getLayersOfType(OsmDataLayer.class).isEmpty()) {
+            if (errorLayer != null) {
+                Main.map.mapView.removeLayer(errorLayer);
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/Severity.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/Severity.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/Severity.java	(revision 3669)
@@ -0,0 +1,68 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+
+import org.openstreetmap.josm.Main;
+
+/** The error severity */
+public enum Severity {
+    /** Error messages */
+    ERROR(tr("Errors"), "error.gif",       Main.pref.getColor(marktr("validation error"), Color.RED)),
+    /** Warning messages */
+    WARNING(tr("Warnings"), "warning.gif", Main.pref.getColor(marktr("validation warning"), Color.YELLOW)),
+    /** Other messages */
+    OTHER(tr("Other"), "other.gif",        Main.pref.getColor(marktr("validation other"), Color.CYAN));
+
+    /** Description of the severity code */
+    private final String message;
+
+    /** Associated icon */
+    private final String icon;
+
+    /** Associated color */
+    private final Color color;
+
+    /**
+     * Constructor
+     *
+     * @param message Description
+     * @param icon Associated icon
+     * @param color The color of this severity
+     */
+    Severity(String message, String icon, Color color)
+    {
+        this.message = message;
+        this.icon = icon;
+        this.color = color;
+    }
+
+    @Override
+    public String toString()
+    {
+        return message;
+    }
+
+    /**
+     * Gets the associated icon
+     * @return the associated icon
+     */
+    public String getIcon()
+    {
+        return icon;
+    }
+
+    /**
+     * Gets the associated color
+     * @return The associated color
+     */
+    public Color getColor()
+    {
+        return color;
+    }
+
+
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/Test.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/Test.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/Test.java	(revision 3669)
@@ -0,0 +1,224 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagConstraints;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.swing.JCheckBox;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Parent class for all validation tests.
+ * <p>
+ * A test is a primitive visitor, so that it can access to all data to be
+ * validated. These primitives are always visited in the same order: nodes
+ * first, then ways.
+ *
+ * @author frsantos
+ */
+public class Test extends AbstractVisitor
+{
+    /** Name of the test */
+    protected final String name;
+
+    /** Description of the test */
+    protected final String description;
+
+    /** Whether this test is enabled. Enabled by default */
+    public boolean enabled = true;
+
+    /** The preferences check for validation */
+    protected JCheckBox checkEnabled;
+
+    /** The preferences check for validation on upload */
+    protected JCheckBox checkBeforeUpload;
+
+    /** Whether this test must check before upload. Enabled by default */
+    public boolean testBeforeUpload = true;
+
+    /** Whether this test is performing just before an upload */
+    protected boolean isBeforeUpload;
+
+    /** The list of errors */
+    protected List<TestError> errors = new ArrayList<TestError>(30);
+
+    /** Whether the test is run on a partial selection data */
+    protected boolean partialSelection;
+
+    /** the progress monitor to use */
+    protected ProgressMonitor progressMonitor;
+    /**
+     * Constructor
+     * @param name Name of the test
+     * @param description Description of the test
+     */
+    public Test(String name, String description)
+    {
+        this.name = name;
+        this.description = description;
+    }
+
+    /**
+     * Constructor
+     * @param name Name of the test
+     */
+    public Test(String name)
+    {
+        this(name, null);
+    }
+
+    /**
+     * Initializes any global data used this tester.
+     * @throws Exception When cannot initialize the test
+     */
+    public void initialize() throws Exception {}
+
+    /**
+     * Start the test using a given progress monitor
+     *
+     * @param progressMonitor  the progress monitor
+     */
+    public void startTest(ProgressMonitor progressMonitor) {
+        if (progressMonitor == null) {
+                this.progressMonitor = NullProgressMonitor.INSTANCE;
+        } else {
+                this.progressMonitor = progressMonitor;
+        }
+        this.progressMonitor.beginTask(tr("Running test {0}", name));
+        errors = new ArrayList<TestError>(30);
+    }
+
+    /**
+     * Flag notifying that this test is run over a partial data selection
+     * @param partialSelection Whether the test is on a partial selection data
+     */
+    public void setPartialSelection(boolean partialSelection)
+    {
+        this.partialSelection = partialSelection;
+    }
+
+    /**
+     * Gets the validation errors accumulated until this moment.
+     * @return The list of errors
+     */
+    public List<TestError> getErrors()
+    {
+        return errors;
+    }
+
+    /**
+     * Notification of the end of the test. The tester may perform additional
+     * actions and destroy the used structures
+     */
+    public void endTest() {
+        progressMonitor.finishTask();
+        progressMonitor = null;
+    }
+
+    /**
+     * Visits all primitives to be tested. These primitives are always visited
+     * in the same order: nodes first, then ways.
+     *
+     * @param selection The primitives to be tested
+     */
+    public void visit(Collection<OsmPrimitive> selection)
+    {
+        progressMonitor.setTicksCount(selection.size());
+        for (OsmPrimitive p : selection) {
+            if( p.isUsable() )
+                p.visit(this);
+            progressMonitor.worked(1);
+        }
+    }
+
+    public void visit(Node n) {}
+
+    public void visit(Way w) {}
+
+    public void visit(Relation r) {}
+
+    /**
+     * Allow the tester to manage its own preferences
+     * @param testPanel The panel to add any preferences component
+     */
+    public void addGui(JPanel testPanel)
+    {
+        checkEnabled = new JCheckBox(name, enabled);
+        checkEnabled.setToolTipText(description);
+        testPanel.add(checkEnabled, GBC.std());
+
+        GBC a = GBC.eol();
+        a.anchor = GridBagConstraints.EAST;
+        checkBeforeUpload = new JCheckBox();
+        checkBeforeUpload.setSelected(testBeforeUpload);
+        testPanel.add(checkBeforeUpload, a);
+    }
+
+    /**
+     * Called when the used submits the preferences
+     */
+    public boolean ok()
+    {
+        enabled = checkEnabled.isSelected();
+        testBeforeUpload = checkBeforeUpload.isSelected();
+        return false;
+    }
+
+    /**
+     * Fixes the error with the appropiate command
+     *
+     * @param testError
+     * @return The command to fix the error
+     */
+    public Command fixError(TestError testError)
+    {
+        return null;
+    }
+
+    /**
+     * Returns true if the given error can be fixed automatically
+     *
+     * @param testError The error to check if can be fixed
+     * @return true if the error can be fixed
+     */
+    public boolean isFixable(TestError testError)
+    {
+        return false;
+    }
+
+    /**
+     * Returns true if this plugin must check the uploaded data before uploading
+     * @return true if this plugin must check the uploaded data before uploading
+     */
+    public boolean testBeforeUpload()
+    {
+        return testBeforeUpload;
+    }
+
+    /**
+     * Sets the flag that marks an upload check
+     * @param isUpload if true, the test is before upload
+     */
+    public void setBeforeUpload(boolean isUpload)
+    {
+        this.isBeforeUpload = isUpload;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/TestError.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/TestError.java	(revision 3669)
@@ -0,0 +1,411 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Point;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeSet;
+
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+import org.openstreetmap.josm.gui.MapView;
+
+/**
+ * Validation error
+ * @author frsantos
+ */
+public class TestError {
+    /** is this error on the ignore list */
+    private Boolean ignored = false;
+    /** Severity */
+    private Severity severity;
+    /** The error message */
+    private String message;
+    /** Deeper error description */
+    private String description;
+    private String description_en;
+    /** The affected primitives */
+    private List<? extends OsmPrimitive> primitives;
+    /** The primitives to be highlighted */
+    private List<?> highlighted;
+    /** The tester that raised this error */
+    private Test tester;
+    /** Internal code used by testers to classify errors */
+    private int code;
+    /** If this error is selected */
+    private boolean selected;
+
+    /**
+     * Constructors
+     * @param tester The tester
+     * @param severity The severity of this error
+     * @param message The error message
+     * @param primitive The affected primitive
+     * @param primitives The affected primitives
+     * @param code The test error reference code
+     */
+    public TestError(Test tester, Severity severity, String message, String description, String description_en,
+            int code, List<? extends OsmPrimitive> primitives, List<?> highlighted) {
+        this.tester = tester;
+        this.severity = severity;
+        this.message = message;
+        this.description = description;
+        this.description_en = description_en;
+        this.primitives = primitives;
+        this.highlighted = highlighted;
+        this.code = code;
+    }
+
+    public TestError(Test tester, Severity severity, String message, int code, List<? extends OsmPrimitive> primitives,
+            List<?> highlighted) {
+        this(tester, severity, message, null, null, code, primitives, highlighted);
+    }
+
+    public TestError(Test tester, Severity severity, String message, String description, String description_en,
+            int code, List<? extends OsmPrimitive> primitives) {
+        this(tester, severity, message, description, description_en, code, primitives, primitives);
+    }
+
+    public TestError(Test tester, Severity severity, String message, int code, List<? extends OsmPrimitive> primitives) {
+        this(tester, severity, message, null, null, code, primitives, primitives);
+    }
+
+    public TestError(Test tester, Severity severity, String message, int code, OsmPrimitive primitive) {
+        this(tester, severity, message, null, null, code, Collections.singletonList(primitive), Collections
+                .singletonList(primitive));
+    }
+
+    public TestError(Test tester, Severity severity, String message, String description, String description_en,
+            int code, OsmPrimitive primitive) {
+        this(tester, severity, message, description, description_en, code, Collections.singletonList(primitive));
+    }
+
+    /**
+     * Gets the error message
+     * @return the error message
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * Gets the error message
+     * @return the error description
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Sets the error message
+     * @param message The error message
+     */
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    /**
+     * Gets the list of primitives affected by this error
+     * @return the list of primitives affected by this error
+     */
+    public List<? extends OsmPrimitive> getPrimitives() {
+        return primitives;
+    }
+
+    /**
+     * Sets the list of primitives affected by this error
+     * @param primitives the list of primitives affected by this error
+     */
+
+    public void setPrimitives(List<OsmPrimitive> primitives) {
+        this.primitives = primitives;
+    }
+
+    /**
+     * Gets the severity of this error
+     * @return the severity of this error
+     */
+    public Severity getSeverity() {
+        return severity;
+    }
+
+    /**
+     * Sets the severity of this error
+     * @param severity the severity of this error
+     */
+    public void setSeverity(Severity severity) {
+        this.severity = severity;
+    }
+
+    /**
+     * Sets the ignore state for this error
+     */
+    public String getIgnoreState() {
+        Collection<String> strings = new TreeSet<String>();
+        String ignorestring = getIgnoreSubGroup();
+        for (OsmPrimitive o : primitives) {
+            // ignore data not yet uploaded
+            if (o.isNew())
+                return null;
+            String type = "u";
+            if (o instanceof Way)
+                type = "w";
+            else if (o instanceof Relation)
+                type = "r";
+            else if (o instanceof Node)
+                type = "n";
+            strings.add(type + "_" + o.getId());
+        }
+        for (String o : strings) {
+            ignorestring += ":" + o;
+        }
+        return ignorestring;
+    }
+
+    public String getIgnoreSubGroup() {
+        String ignorestring = getIgnoreGroup();
+        if (description_en != null)
+            ignorestring += "_" + description_en;
+        return ignorestring;
+    }
+
+    public String getIgnoreGroup() {
+        return Integer.toString(code);
+    }
+
+    public void setIgnored(boolean state) {
+        ignored = state;
+    }
+
+    public Boolean getIgnored() {
+        return ignored;
+    }
+
+    /**
+     * Gets the tester that raised this error
+     * @return the tester that raised this error
+     */
+    public Test getTester() {
+        return tester;
+    }
+
+    /**
+     * Gets the code
+     * @return the code
+     */
+    public int getCode() {
+        return code;
+    }
+
+    /**
+     * Returns true if the error can be fixed automatically
+     *
+     * @return true if the error can be fixed
+     */
+    public boolean isFixable() {
+        return tester != null && tester.isFixable(this);
+    }
+
+    /**
+     * Fixes the error with the appropiate command
+     *
+     * @return The command to fix the error
+     */
+    public Command getFix() {
+        if (tester == null)
+            return null;
+
+        return tester.fixError(this);
+    }
+
+    /**
+     * Paints the error on affected primitives
+     *
+     * @param g The graphics
+     * @param mv The MapView
+     */
+    public void paint(Graphics g, MapView mv) {
+        if (!ignored) {
+            PaintVisitor v = new PaintVisitor(g, mv);
+            visitHighlighted(v);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void visitHighlighted(ValidatorVisitor v) {
+        for (Object o : highlighted) {
+            if (o instanceof OsmPrimitive)
+                v.visit((OsmPrimitive) o);
+            else if (o instanceof WaySegment)
+                v.visit((WaySegment) o);
+            else if (o instanceof List<?>) {
+                v.visit((List<Node>)o);
+            }
+        }
+    }
+
+    /**
+     * Visitor that highlights the primitives affected by this error
+     * @author frsantos
+     */
+    class PaintVisitor extends AbstractVisitor implements ValidatorVisitor {
+        /** The graphics */
+        private final Graphics g;
+        /** The MapView */
+        private final MapView mv;
+
+        /**
+         * Constructor
+         * @param g The graphics
+         * @param mv The Mapview
+         */
+        public PaintVisitor(Graphics g, MapView mv) {
+            this.g = g;
+            this.mv = mv;
+        }
+
+        public void visit(OsmPrimitive p) {
+            if (p.isUsable()) {
+                p.visit(this);
+            }
+        }
+
+        /**
+         * Draws a circle around the node
+         * @param n The node
+         * @param color The circle color
+         */
+        public void drawNode(Node n, Color color) {
+            Point p = mv.getPoint(n);
+            g.setColor(color);
+            if (selected) {
+                g.fillOval(p.x - 5, p.y - 5, 10, 10);
+            } else
+                g.drawOval(p.x - 5, p.y - 5, 10, 10);
+        }
+
+        public void drawSegment(Point p1, Point p2, Color color) {
+            g.setColor(color);
+
+            double t = Math.atan2(p2.x - p1.x, p2.y - p1.y);
+            double cosT = Math.cos(t);
+            double sinT = Math.sin(t);
+            int deg = (int) Math.toDegrees(t);
+            if (selected) {
+                int[] x = new int[] { (int) (p1.x + 5 * cosT), (int) (p2.x + 5 * cosT), (int) (p2.x - 5 * cosT),
+                        (int) (p1.x - 5 * cosT) };
+                int[] y = new int[] { (int) (p1.y - 5 * sinT), (int) (p2.y - 5 * sinT), (int) (p2.y + 5 * sinT),
+                        (int) (p1.y + 5 * sinT) };
+                g.fillPolygon(x, y, 4);
+                g.fillArc(p1.x - 5, p1.y - 5, 10, 10, deg, 180);
+                g.fillArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
+            } else {
+                g.drawLine((int) (p1.x + 5 * cosT), (int) (p1.y - 5 * sinT), (int) (p2.x + 5 * cosT),
+                        (int) (p2.y - 5 * sinT));
+                g.drawLine((int) (p1.x - 5 * cosT), (int) (p1.y + 5 * sinT), (int) (p2.x - 5 * cosT),
+                        (int) (p2.y + 5 * sinT));
+                g.drawArc(p1.x - 5, p1.y - 5, 10, 10, deg, 180);
+                g.drawArc(p2.x - 5, p2.y - 5, 10, 10, deg, -180);
+            }
+        }
+
+        /**
+         * Draws a line around the segment
+         *
+         * @param s The segment
+         * @param color The color
+         */
+        public void drawSegment(Node n1, Node n2, Color color) {
+            drawSegment(mv.getPoint(n1), mv.getPoint(n2), color);
+        }
+
+        /**
+         * Draw a small rectangle.
+         * White if selected (as always) or red otherwise.
+         *
+         * @param n The node to draw.
+         */
+        public void visit(Node n) {
+            if (isNodeVisible(n))
+                drawNode(n, severity.getColor());
+        }
+
+        public void visit(Way w) {
+            visit(w.getNodes());
+        }
+
+        public void visit(WaySegment ws) {
+            if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
+                return;
+            Node a = ws.way.getNodes().get(ws.lowerIndex), b = ws.way.getNodes().get(ws.lowerIndex + 1);
+            if (isSegmentVisible(a, b)) {
+                drawSegment(a, b, severity.getColor());
+            }
+        }
+
+        public void visit(Relation r) {
+            /* No idea how to draw a relation. */
+        }
+
+        /**
+         * Checks if the given node is in the visible area.
+         * @param n The node to check for visibility
+         * @return true if the node is visible
+         */
+        protected boolean isNodeVisible(Node n) {
+            Point p = mv.getPoint(n);
+            return !((p.x < 0) || (p.y < 0) || (p.x > mv.getWidth()) || (p.y > mv.getHeight()));
+        }
+
+        /**
+         * Checks if the given segment is in the visible area.
+         * NOTE: This will return true for a small number of non-visible
+         *       segments.
+         * @param ls The segment to check
+         * @return true if the segment is visible
+         */
+        protected boolean isSegmentVisible(Node n1, Node n2) {
+            Point p1 = mv.getPoint(n1);
+            Point p2 = mv.getPoint(n2);
+            if ((p1.x < 0) && (p2.x < 0))
+                return false;
+            if ((p1.y < 0) && (p2.y < 0))
+                return false;
+            if ((p1.x > mv.getWidth()) && (p2.x > mv.getWidth()))
+                return false;
+            if ((p1.y > mv.getHeight()) && (p2.y > mv.getHeight()))
+                return false;
+            return true;
+        }
+
+        public void visit(List<Node> nodes) {
+            Node lastN = null;
+            for (Node n : nodes) {
+                if (lastN == null) {
+                    lastN = n;
+                    continue;
+                }
+                if (n.isDrawable() && isSegmentVisible(lastN, n)) {
+                    drawSegment(lastN, n, severity.getColor());
+                }
+                lastN = n;
+            }
+        }
+    }
+
+    /**
+     * Sets the selection flag of this error
+     * @param selected if this error is selected
+     */
+    public void setSelected(boolean selected) {
+        this.selected = selected;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/ValidatorVisitor.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/ValidatorVisitor.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/ValidatorVisitor.java	(revision 3669)
@@ -0,0 +1,14 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation;
+
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.WaySegment;
+
+public interface ValidatorVisitor {
+    void visit(OsmPrimitive p);
+    void visit(WaySegment ws);
+    void visit(List<Node> nodes);
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/ChangePropertyKeyCommand.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/ChangePropertyKeyCommand.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/ChangePropertyKeyCommand.java	(revision 3669)
@@ -0,0 +1,100 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JLabel;
+
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.PseudoCommand;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.util.NameVisitor;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Command that replaces the key of several objects
+ *
+ */
+public class ChangePropertyKeyCommand extends Command {
+    /**
+     * All primitives, that are affected with this command.
+     */
+    private final List<OsmPrimitive> objects;
+    /**
+     * The key that is subject to change.
+     */
+    private final String key;
+    /**
+     * The mew key.
+     */
+    private final String newKey;
+
+    /**
+     * Constructor
+     *
+     * @param objects all objects subject to change replacement
+     * @param key The key to replace
+     * @param newKey the new value of the key
+     */
+    public ChangePropertyKeyCommand(Collection<? extends OsmPrimitive> objects, String key, String newKey) {
+        this.objects = new LinkedList<OsmPrimitive>(objects);
+        this.key = key;
+        this.newKey = newKey;
+    }
+
+    @Override public boolean executeCommand() {
+        if (!super.executeCommand()) return false; // save old
+        for (OsmPrimitive osm : objects) {
+            if(osm.hasKeys())
+            {
+                osm.setModified(true);
+                String oldValue = osm.get(key);
+                osm.put(newKey, oldValue);
+                osm.remove(key);
+            }
+        }
+        return true;
+    }
+
+    @Override public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
+        modified.addAll(objects);
+    }
+
+    @Override public JLabel getDescription() {
+        String text = tr( "Replace \"{0}\" by \"{1}\" for", key, newKey);
+        if (objects.size() == 1) {
+            NameVisitor v = new NameVisitor();
+            objects.iterator().next().visit(v);
+            text += " "+tr(v.className)+" "+v.name;
+        } else
+            text += " "+objects.size()+" "+trn("object","objects",objects.size());
+        return new JLabel(text, ImageProvider.get("data", "key"), JLabel.HORIZONTAL);
+    }
+
+    @Override public Collection<PseudoCommand> getChildren() {
+        if (objects.size() == 1)
+            return null;
+        List<PseudoCommand> children = new ArrayList<PseudoCommand>();
+
+        final NameVisitor v = new NameVisitor();
+        for (final OsmPrimitive osm : objects) {
+            osm.visit(v);
+            children.add(new PseudoCommand() {
+                @Override public JLabel getDescription() {
+                    return v.toLabel();
+                }
+                @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
+                    return Collections.singleton(osm);
+                }
+            });
+        }
+        return children;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/Coastlines.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/Coastlines.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/Coastlines.java	(revision 3669)
@@ -0,0 +1,206 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.geom.Area;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Check coastlines for errors
+ *
+ * @author frsantos
+ * @author Teemu Koskinen
+ */
+public class Coastlines extends Test
+{
+    protected static int UNORDERED_COASTLINE = 901;
+    protected static int REVERSED_COASTLINE = 902;
+    protected static int UNCONNECTED_COASTLINE = 903;
+
+    private List<Way> coastlines;
+
+    private Area downloadedArea = null;
+
+    /**
+     * Constructor
+     */
+    public Coastlines()
+    {
+        super(tr("Coastlines."),
+              tr("This test checks that coastlines are correct."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+
+        OsmDataLayer layer = Main.map.mapView.getEditLayer();
+
+        if (layer != null)
+            downloadedArea = layer.data.getDataSourceArea();
+
+        coastlines = new LinkedList<Way>();
+    }
+
+    @Override
+    public void endTest()
+    {
+        for (Way c1 : coastlines) {
+            Node head = c1.firstNode();
+            Node tail = c1.lastNode();
+
+            if (head.equals(tail))
+                continue;
+
+            int headWays = 0;
+            int tailWays = 0;
+            boolean headReversed = false;
+            boolean tailReversed = false;
+            boolean headUnordered = false;
+            boolean tailUnordered = false;
+            Way next = null;
+            Way prev = null;
+
+            for (Way c2 : coastlines) {
+                if (c1 == c2)
+                    continue;
+
+                if (c2.containsNode(head)) {
+                    headWays++;
+                    next = c2;
+
+                    if (head.equals(c2.firstNode()))
+                        headReversed = true;
+                    else if (!head.equals(c2.lastNode()))
+                        headUnordered = true;
+                }
+
+                if (c2.containsNode(tail)) {
+                    tailWays++;
+                    prev = c2;
+
+                    if (tail.equals(c2.lastNode()))
+                        tailReversed = true;
+                    else if (!tail.equals(c2.firstNode()))
+                        tailUnordered = true;
+                }
+            }
+
+
+            List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>();
+            primitives.add(c1);
+
+            if (headWays == 0 || tailWays == 0) {
+                List<OsmPrimitive> highlight = new ArrayList<OsmPrimitive>();
+
+                System.out.println("Unconnected coastline: " + c1.getId());
+                if (headWays == 0 && (downloadedArea == null || downloadedArea.contains(head.getCoor()))) {
+                    System.out.println("headways: " +headWays+ " node: " + head.toString());
+                    highlight.add(head);
+                }
+                if (tailWays == 0 && (downloadedArea == null || downloadedArea.contains(tail.getCoor()))) {
+                    System.out.println("tailways: " +tailWays+ " tail: " + tail.toString());
+                    highlight.add(tail);
+                }
+
+                if (highlight.size() > 0)
+                    errors.add(new TestError(this, Severity.ERROR, tr("Unconnected coastline"),
+                                             UNCONNECTED_COASTLINE, primitives, highlight));
+            }
+
+            boolean unordered = false;
+            boolean reversed = false;
+
+            if (headWays == 1 && headReversed && tailWays == 1 && tailReversed)
+                reversed = true;
+
+            if (headWays > 1 || tailWays > 1)
+                unordered = true;
+            else if (headUnordered || tailUnordered)
+                unordered = true;
+            else if (reversed && next == prev)
+                unordered = true;
+
+            if (unordered) {
+                List<OsmPrimitive> highlight = new ArrayList<OsmPrimitive>();
+
+                System.out.println("Unordered coastline: " + c1.toString());
+                if (headWays > 1 || headUnordered || reversed) {
+                    System.out.println("head: " + head.toString());
+                    highlight.add(head);
+                }
+                if (tailWays > 1 || tailUnordered || reversed) {
+                    System.out.println("tail: " + tail.toString());
+                    highlight.add(tail);
+                }
+
+                errors.add(new TestError(this, Severity.ERROR, tr("Unordered coastline"),
+                                         UNORDERED_COASTLINE, primitives, highlight));
+            }
+            else if (reversed) {
+                errors.add(new TestError(this, Severity.ERROR, tr("Reversed coastline"),
+                                         REVERSED_COASTLINE, primitives));
+            }
+        }
+
+        coastlines = null;
+        downloadedArea = null;
+
+        super.endTest();
+    }
+
+    @Override
+    public void visit(Way way)
+    {
+        if (!way.isUsable())
+            return;
+
+        String natural = way.get("natural");
+        if (natural == null || !natural.equals("coastline"))
+            return;
+
+        coastlines.add(way);
+    }
+
+    @Override
+    public Command fixError(TestError testError) {
+        if (isFixable(testError)) {
+            Way way = (Way) testError.getPrimitives().iterator().next();
+            Way newWay = new Way(way);
+
+            List<Node> nodesCopy = newWay.getNodes();
+            Collections.reverse(nodesCopy);
+            newWay.setNodes(nodesCopy);
+
+            return new ChangeCommand(way, newWay);
+        }
+
+        return null;
+    }
+
+    @Override
+    public boolean isFixable(TestError testError) {
+        if (testError.getTester() instanceof Coastlines) {
+            return (testError.getCode() == REVERSED_COASTLINE);
+        }
+
+        return false;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(revision 3669)
@@ -0,0 +1,221 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.ValUtil;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Tests if there are segments that crosses in the same layer
+ *
+ * @author frsantos
+ */
+public class CrossingWays extends Test
+{
+    protected static int CROSSING_WAYS = 601;
+
+    /** All way segments, grouped by cells */
+    Map<Point2D,List<ExtendedSegment>> cellSegments;
+    /** The already detected errors */
+    HashSet<WaySegment> errorSegments;
+    /** The already detected ways in error */
+    Map<List<Way>, List<WaySegment>> ways_seen;
+
+
+    /**
+     * Constructor
+     */
+    public CrossingWays()
+    {
+        super(tr("Crossing ways."),
+              tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, but are not connected by a node."));
+    }
+
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        cellSegments = new HashMap<Point2D,List<ExtendedSegment>>(1000);
+        errorSegments = new HashSet<WaySegment>();
+        ways_seen = new HashMap<List<Way>, List<WaySegment>>(50);
+    }
+
+    @Override
+    public void endTest()
+    {
+        super.endTest();
+        cellSegments = null;
+        errorSegments = null;
+        ways_seen = null;
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        if( !w.isUsable() )
+            return;
+
+        String coastline1 = w.get("natural");
+        boolean isCoastline1 = "water".equals(coastline1) || "coastline".equals(coastline1);
+        String railway1 = w.get("railway");
+        boolean isSubway1 = "subway".equals(railway1);
+        boolean isTram1 = "tram".equals(railway1);
+        boolean isBuilding = (w.get("building") != null);
+
+        if( w.get("highway") == null && w.get("waterway") == null && (railway1 == null || isSubway1 || isTram1)  && !isCoastline1 && !isBuilding)
+            return;
+
+        String layer1 = w.get("layer");
+
+        int nodesSize = w.getNodesCount();
+        for (int i = 0; i < nodesSize - 1; i++) {
+            WaySegment ws = new WaySegment(w, i);
+            ExtendedSegment es1 = new ExtendedSegment(ws, layer1, railway1, coastline1);
+            List<List<ExtendedSegment>> cellSegments = getSegments(es1.n1, es1.n2);
+            for( List<ExtendedSegment> segments : cellSegments)
+            {
+                for( ExtendedSegment es2 : segments)
+                {
+                    List<Way> prims;
+                    List<WaySegment> highlight;
+
+                    if (errorSegments.contains(ws) && errorSegments.contains(es2.ws))
+                        continue;
+
+                    String layer2 = es2.layer;
+                    String railway2 = es2.railway;
+                    String coastline2 = es2.coastline;
+                    if (layer1 == null ? layer2 != null : !layer1.equals(layer2))
+                        continue;
+
+                    if( !es1.intersects(es2) ) continue;
+                    if( isSubway1 && "subway".equals(railway2)) continue;
+                    if( isTram1 && "tram".equals(railway2)) continue;
+
+                    boolean isCoastline2 = coastline2 != null && (coastline2.equals("water") || coastline2.equals("coastline"));
+                    if( isCoastline1 != isCoastline2 ) continue;
+
+                    if((es1.railway != null && es1.railway.equals("abandoned")) || (railway2 != null && railway2.equals("abandoned"))) continue;
+
+                    prims = Arrays.asList(es1.ws.way, es2.ws.way);
+                    if ((highlight = ways_seen.get(prims)) == null)
+                    {
+                        highlight = new ArrayList<WaySegment>();
+                        highlight.add(es1.ws);
+                        highlight.add(es2.ws);
+
+                        errors.add(new TestError(this, Severity.WARNING,
+                        isBuilding ? tr("Crossing buildings") : tr("Crossing ways"), CROSSING_WAYS, prims, highlight));
+                        ways_seen.put(prims, highlight);
+                    }
+                    else
+                    {
+                        highlight.add(es1.ws);
+                        highlight.add(es2.ws);
+                    }
+                }
+                segments.add(es1);
+            }
+        }
+    }
+
+    /**
+    * Returns all the cells this segment crosses.  Each cell contains the list
+    * of segments already processed
+    *
+    * @param n1 The first node
+    * @param n2 The second node
+    * @return A list with all the cells the segment crosses
+    */
+    public List<List<ExtendedSegment>> getSegments(Node n1, Node n2)
+    {
+        List<List<ExtendedSegment>> cells = new ArrayList<List<ExtendedSegment>>();
+        for( Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.griddetail) )
+        {
+            List<ExtendedSegment> segments = cellSegments.get( cell );
+            if( segments == null )
+            {
+                segments = new ArrayList<ExtendedSegment>();
+                cellSegments.put(cell, segments);
+            }
+            cells.add(segments);
+        }
+
+        return cells;
+    }
+
+    /**
+     * A way segment with some additional information
+     * @author frsantos
+     */
+    private static class ExtendedSegment
+    {
+        public Node n1, n2;
+
+        public WaySegment ws;
+
+        /** The layer */
+        public String layer;
+
+        /** The railway type */
+        public String railway;
+
+        /** The coastline type */
+        public String coastline;
+
+        /**
+         * Constructor
+         * @param ws The way segment
+         * @param layer The layer of the way this segment is in
+         * @param railway The railway type of the way this segment is in
+         * @param coastline The coastlyne typo of the way the segment is in
+         */
+        public ExtendedSegment(WaySegment ws, String layer, String railway, String coastline)
+        {
+            this.ws = ws;
+            this.n1 = ws.way.getNodes().get(ws.lowerIndex);
+            this.n2 = ws.way.getNodes().get(ws.lowerIndex + 1);
+            this.layer = layer;
+            this.railway = railway;
+            this.coastline = coastline;
+        }
+
+        /**
+         * Checks whether this segment crosses other segment
+         * @param s2 The other segment
+         * @return true if both segements crosses
+         */
+        public boolean intersects(ExtendedSegment s2)
+        {
+            if( n1.equals(s2.n1) || n2.equals(s2.n2) ||
+                n1.equals(s2.n2)   || n2.equals(s2.n1) )
+            {
+                return false;
+            }
+
+            return Line2D.linesIntersect(
+                n1.getEastNorth().east(), n1.getEastNorth().north(),
+                n2.getEastNorth().east(), n2.getEastNorth().north(),
+                s2.n1.getEastNorth().east(), s2.n1.getEastNorth().north(),
+                s2.n2.getEastNorth().east(), s2.n2.getEastNorth().north());
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateNode.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateNode.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateNode.java	(revision 3669)
@@ -0,0 +1,424 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.awt.geom.Area;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.MergeNodesAction;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Hash;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Storage;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Tests if there are duplicate nodes
+ *
+ * @author frsantos
+ */
+public class DuplicateNode extends Test {
+
+    private class NodeHash implements Hash<Object, Object> {
+
+        double precision = Main.pref.getDouble("validator.duplicatenodes.precision", 0.);
+
+        private LatLon RoundCoord(Node o) {
+            return new LatLon(
+                    Math.round(o.getCoor().lat() / precision) * precision,
+                    Math.round(o.getCoor().lon() / precision) * precision
+            );
+        }
+
+        @SuppressWarnings("unchecked")
+        private LatLon getLatLon(Object o) {
+            if (o instanceof Node) {
+                if (precision==0) {
+                    return ((Node) o).getCoor().getRoundedToOsmPrecision();
+                } else {
+                    return RoundCoord((Node) o);
+                }
+            } else if (o instanceof List<?>) {
+                if (precision==0) {
+                    return ((List<Node>) o).get(0).getCoor().getRoundedToOsmPrecision();
+                } else {
+                    return RoundCoord(((List<Node>) o).get(0));
+                }
+            } else {
+                throw new AssertionError();
+            }
+        }
+
+        public boolean equals(Object k, Object t) {
+            return getLatLon(k).equals(getLatLon(t));
+        }
+
+        public int getHashCode(Object k) {
+            return getLatLon(k).hashCode();
+        }
+
+    }
+
+    protected static int DUPLICATE_NODE = 1;
+    protected static int DUPLICATE_NODE_MIXED = 2;
+    protected static int DUPLICATE_NODE_OTHER = 3;
+    protected static int DUPLICATE_NODE_BUILDING = 10;
+    protected static int DUPLICATE_NODE_BOUNDARY = 11;
+    protected static int DUPLICATE_NODE_HIGHWAY = 12;
+    protected static int DUPLICATE_NODE_LANDUSE = 13;
+    protected static int DUPLICATE_NODE_NATURAL = 14;
+    protected static int DUPLICATE_NODE_POWER = 15;
+    protected static int DUPLICATE_NODE_RAILWAY = 16;
+    protected static int DUPLICATE_NODE_WATERWAY = 17;
+
+    /** The map of potential duplicates.
+     *
+     * If there is exactly one node for a given pos, the map includes a pair <pos, Node>.
+     * If there are multiple nodes for a given pos, the map includes a pair
+     * <pos, NodesByEqualTagsMap>
+     */
+    Storage<Object> potentialDuplicates;
+
+    /**
+     * Constructor
+     */
+    public DuplicateNode()
+    {
+        super(tr("Duplicated nodes")+".",
+                tr("This test checks that there are no nodes at the very same location."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor) {
+        super.startTest(monitor);
+        potentialDuplicates = new Storage<Object>(new NodeHash());
+    }
+
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void endTest() {
+        for (Object v: potentialDuplicates) {
+            if (v instanceof Node) {
+                // just one node at this position. Nothing to report as
+                // error
+                continue;
+            }
+
+            // multiple nodes at the same position -> report errors
+            //
+            List<Node> nodes = (List<Node>)v;
+            errors.addAll(buildTestErrors(this, nodes));
+        }
+        super.endTest();
+        potentialDuplicates = null;
+    }
+
+    public List<TestError> buildTestErrors(Test parentTest, List<Node> nodes) {
+        List<TestError> errors = new ArrayList<TestError>();
+
+        Bag<Map<String,String>, OsmPrimitive> bag = new Bag<Map<String,String>, OsmPrimitive>();
+        for (Node n: nodes) {
+            bag.add(n.getKeys(), n);
+        }
+
+        Map<String,Boolean> typeMap=new HashMap<String,Boolean>();
+        String[] types = {"none", "highway", "railway", "waterway", "boundary", "power", "natural", "landuse", "building"};
+
+
+        // check whether we have multiple nodes at the same position with
+        // the same tag set
+        //
+        for (Iterator<Map<String,String>> it = bag.keySet().iterator(); it.hasNext(); ) {
+            Map<String,String> tagSet = it.next();
+            if (bag.get(tagSet).size() > 1) {
+
+                for (String type: types) {
+                    typeMap.put(type, false);
+                }
+
+                for (OsmPrimitive p : bag.get(tagSet)) {
+                    if (p.getType()==OsmPrimitiveType.NODE) {
+                        Node n = (Node) p;
+                        List<OsmPrimitive> lp=n.getReferrers();
+                        for (OsmPrimitive sp: lp) {
+                            if (sp.getType()==OsmPrimitiveType.WAY) {
+                                boolean typed = false;
+                                Way w=(Way) sp;
+                                Map<String, String> keys = w.getKeys();
+                                for (String type: typeMap.keySet()) {
+                                    if (keys.containsKey(type)) {
+                                        typeMap.put(type, true);
+                                        typed=true;
+                                    }
+                                }
+                                if (!typed) typeMap.put("none", true);
+                            }
+                        }
+
+                    }
+                }
+
+                int nbType=0;
+                for (String type: typeMap.keySet()) {
+                    if (typeMap.get(type)) nbType++;
+                }
+
+                if (nbType>1) {
+                    String msg = marktr("Mixed type duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.WARNING,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_MIXED,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("highway")) {
+                    String msg = marktr("Highway duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_HIGHWAY,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("railway")) {
+                    String msg = marktr("Railway duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_RAILWAY,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("waterway")) {
+                    String msg = marktr("Waterway duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_WATERWAY,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("boundary")) {
+                    String msg = marktr("Boundary duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_BOUNDARY,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("power")) {
+                    String msg = marktr("Power duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_POWER,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("natural")) {
+                    String msg = marktr("Natural duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_NATURAL,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("building")) {
+                    String msg = marktr("Building duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_BUILDING,
+                            bag.get(tagSet)
+                    ));
+                } else if (typeMap.get("landuse")) {
+                    String msg = marktr("Landuse duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.ERROR,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_LANDUSE,
+                            bag.get(tagSet)
+                    ));
+                } else {
+                    String msg = marktr("Other duplicated nodes");
+                    errors.add(new TestError(
+                            parentTest,
+                            Severity.WARNING,
+                            tr("Duplicated nodes"),
+                            tr(msg),
+                            msg,
+                            DUPLICATE_NODE_OTHER,
+                            bag.get(tagSet)
+                    ));
+
+                }
+                it.remove();
+            }
+
+        }
+
+        // check whether we have multiple nodes at the same position with
+        // differing tag sets
+        //
+        if (!bag.isEmpty()) {
+            List<OsmPrimitive> duplicates = new ArrayList<OsmPrimitive>();
+            for (List<OsmPrimitive> l: bag.values()) {
+                duplicates.addAll(l);
+            }
+            if (duplicates.size() > 1) {
+                errors.add(new TestError(
+                        parentTest,
+                        Severity.WARNING,
+                        tr("Nodes at same position"),
+                        DUPLICATE_NODE,
+                        duplicates
+                ));
+            }
+        }
+        return errors;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public void visit(Node n) {
+        if (n.isUsable()) {
+            if (potentialDuplicates.get(n) == null) {
+                // in most cases there is just one node at a given position. We
+                // avoid to create an extra object and add remember the node
+                // itself at this position
+                potentialDuplicates.put(n);
+            } else if (potentialDuplicates.get(n) instanceof Node) {
+                // we have an additional node at the same position. Create an extra
+                // object to keep track of the nodes at this position.
+                //
+                Node n1 = (Node)potentialDuplicates.get(n);
+                List<Node> nodes = new ArrayList<Node>(2);
+                nodes.add(n1);
+                nodes.add(n);
+                potentialDuplicates.put(nodes);
+            } else if (potentialDuplicates.get(n) instanceof List<?>) {
+                // we have multiple nodes at the same position.
+                //
+                List<Node> nodes = (List<Node>)potentialDuplicates.get(n);
+                nodes.add(n);
+            }
+        }
+    }
+
+    /**
+     * Merge the nodes into one.
+     * Copied from UtilsPlugin.MergePointsAction
+     */
+    @Override
+    public Command fixError(TestError testError)
+    {
+        Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(testError.getPrimitives());
+        LinkedHashSet<Node> nodes = new LinkedHashSet<Node>(OsmPrimitive.getFilteredList(sel, Node.class));
+
+        // Use first existing node or first node if all nodes are new
+        Node target = null;
+        for (Node n: nodes) {
+            if (!n.isNew()) {
+                target = n;
+                break;
+            }
+        }
+        if (target == null) {
+            target = nodes.iterator().next();
+        }
+
+        if(checkAndConfirmOutlyingDeletes(nodes))
+            return MergeNodesAction.mergeNodes(Main.main.getEditLayer(), nodes, target);
+
+        return null;// undoRedo handling done in mergeNodes
+    }
+
+    @Override
+    public boolean isFixable(TestError testError) {
+        return (testError.getTester() instanceof DuplicateNode);
+    }
+
+    /**
+     * Check whether user is about to delete data outside of the download area.
+     * Request confirmation if he is.
+     */
+    private static boolean checkAndConfirmOutlyingDeletes(LinkedHashSet<Node> del) {
+        Area a = Main.main.getCurrentDataSet().getDataSourceArea();
+        if (a != null) {
+            for (OsmPrimitive osm : del) {
+                if (osm instanceof Node && !osm.isNew()) {
+                    Node n = (Node) osm;
+                    if (!a.contains(n.getCoor())) {
+                        JPanel msg = new JPanel(new GridBagLayout());
+                        msg.add(new JLabel(
+                                "<html>" +
+                                // leave message in one tr() as there is a grammatical
+                                // connection.
+                                tr("You are about to delete nodes outside of the area you have downloaded."
+                                        + "<br>"
+                                        + "This can cause problems because other objects (that you do not see) might use them."
+                                        + "<br>" + "Do you really want to delete?") + "</html>"));
+
+                        return ConditionalOptionPaneUtil.showConfirmationDialog(
+                                "delete_outside_nodes",
+                                Main.parent,
+                                msg,
+                                tr("Delete confirmation"),
+                                JOptionPane.YES_NO_OPTION,
+                                JOptionPane.QUESTION_MESSAGE,
+                                JOptionPane.YES_OPTION);
+                    }
+                }
+            }
+        }
+        return true;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateWay.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateWay.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicateWay.java	(revision 3669)
@@ -0,0 +1,198 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Tests if there are duplicate ways
+ */
+public class DuplicateWay extends Test
+{
+
+    private static class WayPair {
+        public List<LatLon> coor;
+        public Map<String, String> keys;
+        public WayPair(List<LatLon> _coor,Map<String, String> _keys) {
+            coor=_coor;
+            keys=_keys;
+        }
+        @Override
+        public int hashCode() {
+            return coor.hashCode()+keys.hashCode();
+        }
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof WayPair)) return false;
+            WayPair wp = (WayPair) obj;
+            return wp.coor.equals(coor) && wp.keys.equals(keys);
+        }
+    }
+
+    protected static int DUPLICATE_WAY = 1401;
+
+    /** Bag of all ways */
+    Bag<WayPair, OsmPrimitive> ways;
+
+    /**
+     * Constructor
+     */
+    public DuplicateWay()
+    {
+        super(tr("Duplicated ways")+".",
+              tr("This test checks that there are no ways with same tags and same node coordinates."));
+    }
+
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        ways = new Bag<WayPair, OsmPrimitive>(1000);
+    }
+
+    @Override
+    public void endTest()
+    {
+        super.endTest();
+        for(List<OsmPrimitive> duplicated : ways.values() )
+        {
+            if( duplicated.size() > 1)
+            {
+                TestError testError = new TestError(this, Severity.ERROR, tr("Duplicated ways"), DUPLICATE_WAY, duplicated);
+                errors.add( testError );
+            }
+        }
+        ways = null;
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        if( !w.isUsable() )
+            return;
+        List<Node> wNodes=w.getNodes();
+        Vector<LatLon> wLat=new Vector<LatLon>(wNodes.size());
+        for(int i=0;i<wNodes.size();i++) {
+                 wLat.add(wNodes.get(i).getCoor());
+        }
+        Map<String, String> wkeys=w.getKeys();
+        wkeys.remove("created_by");
+        WayPair wKey=new WayPair(wLat,wkeys);
+        ways.add(wKey, w);
+    }
+
+    /**
+     * Fix the error by removing all but one instance of duplicate ways
+     */
+    @Override
+    public Command fixError(TestError testError)
+    {
+        Collection<? extends OsmPrimitive> sel = testError.getPrimitives();
+        HashSet<Way> ways = new HashSet<Way>();
+
+        for (OsmPrimitive osm : sel)
+            if (osm instanceof Way)
+                ways.add((Way)osm);
+
+        if( ways.size() < 2 )
+            return null;
+
+        long idToKeep = 0;
+        Way wayToKeep = ways.iterator().next();
+        // Only one way will be kept - the one with lowest positive ID, if such exist
+        // or one "at random" if no such exists. Rest of the ways will be deleted
+        for (Way w: ways) {
+            if (!w.isNew()) {
+                if (idToKeep == 0 || w.getId() < idToKeep) {
+                    idToKeep = w.getId();
+                    wayToKeep = w;
+                }
+            }
+        }
+
+        // Find the way that is member of one or more relations. (If any)
+        Way wayWithRelations = null;
+        List<Relation> relations = null;
+        for (Way w : ways) {
+            List<Relation> rel = OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class);
+            if (!rel.isEmpty()) {
+                if (wayWithRelations != null)
+                    throw new AssertionError("Cannot fix duplicate Ways: More than one way is relation member.");
+                wayWithRelations = w;
+                relations = rel;
+            }
+        }
+
+        Collection<Command> commands = new LinkedList<Command>();
+
+        // Fix relations.
+        if (wayWithRelations != null && wayToKeep != wayWithRelations) {
+            for (Relation rel : relations) {
+                Relation newRel = new Relation(rel);
+                for (int i = 0; i < newRel.getMembers().size(); ++i) {
+                    RelationMember m = newRel.getMember(i);
+                    if (wayWithRelations.equals(m.getMember())) {
+                        newRel.setMember(i, new RelationMember(m.getRole(), wayToKeep));
+                    }
+                }
+                commands.add(new ChangeCommand(rel, newRel));
+            }
+        }
+
+        //Delete all ways in the list
+        //Note: nodes are not deleted, these can be detected and deleted at next pass
+        ways.remove(wayToKeep);
+        commands.add(new DeleteCommand(ways));
+        return new SequenceCommand(tr("Delete duplicate ways"), commands);
+    }
+
+    @Override
+    public boolean isFixable(TestError testError)
+    {
+        if (!(testError.getTester() instanceof DuplicateWay))
+            return false;
+
+        // We fix it only if there is no more than one way that is relation member.
+        Collection<? extends OsmPrimitive> sel = testError.getPrimitives();
+        HashSet<Way> ways = new HashSet<Way>();
+
+        for (OsmPrimitive osm : sel)
+            if (osm instanceof Way)
+                ways.add((Way)osm);
+
+        if (ways.size() < 2)
+            return false;
+
+        int waysWithRelations = 0;
+        for (Way w : ways) {
+            List<Relation> rel = OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class);
+            if (!rel.isEmpty()) {
+                ++waysWithRelations;
+            }
+        }
+        return (waysWithRelations <= 1);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicatedWayNodes.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicatedWayNodes.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/DuplicatedWayNodes.java	(revision 3669)
@@ -0,0 +1,71 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangeCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+
+public class DuplicatedWayNodes extends Test {
+    protected static int DUPLICATE_WAY_NODE = 501;
+
+    public DuplicatedWayNodes() {
+        super(tr("Duplicated way nodes."),
+            tr("Checks for ways with identical consecutive nodes."));
+    }
+
+    @Override public void visit(Way w) {
+        if (!w.isUsable()) return;
+
+        Node lastN = null;
+        for (Node n : w.getNodes()) {
+            if (lastN == null) {
+                lastN = n;
+                continue;
+            }
+            if (lastN == n) {
+                errors.add(new TestError(this, Severity.ERROR, tr("Duplicated way nodes"), DUPLICATE_WAY_NODE,
+                    Arrays.asList(w), Arrays.asList(n)));
+                break;
+            }
+            lastN = n;
+        }
+    }
+
+    @Override public Command fixError(TestError testError) {
+        Way w = (Way) testError.getPrimitives().iterator().next();
+        Way wnew = new Way(w);
+        wnew.setNodes(null);
+        Node lastN = null;
+        for (Node n : w.getNodes()) {
+            if (lastN == null) {
+                wnew.addNode(n);
+            } else if (n == lastN) {
+                // Skip this node
+            } else {
+                wnew.addNode(n);
+            }
+            lastN = n;
+        }
+        if (wnew.getNodesCount() < 2) {
+            // Empty way, delete
+            return DeleteCommand.delete(Main.map.mapView.getEditLayer(), Collections.singleton(w));
+        } else {
+            return new ChangeCommand(w, wnew);
+        }
+    }
+
+    @Override public boolean isFixable(TestError testError) {
+        return testError.getTester() instanceof DuplicatedWayNodes;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/MultipolygonTest.java	(revision 3669)
@@ -0,0 +1,235 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.geom.GeneralPath;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
+import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
+import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.PolyData.Intersection;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.mappaint.AreaElemStyle;
+import org.openstreetmap.josm.gui.mappaint.ElemStyle;
+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
+import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
+
+public class MultipolygonTest extends Test {
+
+    protected static final int WRONG_MEMBER_TYPE = 1601;
+    protected static final int WRONG_MEMBER_ROLE = 1602;
+    protected static final int NON_CLOSED_WAY = 1603;
+    protected static final int MISSING_OUTER_WAY = 1604;
+    protected static final int INNER_WAY_OUTSIDE = 1605;
+    protected static final int CROSSING_WAYS = 1606;
+    protected static final int OUTER_STYLE_MISMATCH = 1607;
+    protected static final int INNER_STYLE_MISMATCH = 1608;
+    protected static final int NOT_CLOSED = 1609;
+    protected static final int NO_STYLE = 1610;
+    protected static final int NO_STYLE_POLYGON = 1611;
+
+    private static ElemStyles.StyleSet styles;
+
+    private final List<List<Node>> nonClosedWays = new ArrayList<List<Node>>();
+
+    public MultipolygonTest() {
+        super(tr("Multipolygon"),
+                tr("This test checks if multipolygons are valid"));
+    }
+
+    @Override
+    public void initialize() throws Exception
+    {
+        styles = MapPaintStyles.getStyles().getStyleSet();
+    }
+
+    private List<List<Node>> joinWays(Collection<Way> ways) {
+        List<List<Node>> result = new ArrayList<List<Node>>();
+        List<Way> waysToJoin = new ArrayList<Way>();
+        for (Way way: ways) {
+            if (way.isClosed()) {
+                result.add(way.getNodes());
+            } else {
+                waysToJoin.add(way);
+            }
+        }
+
+        for (JoinedWay jw: Multipolygon.joinWays(waysToJoin)) {
+            if (!jw.isClosed()) {
+                nonClosedWays.add(jw.getNodes());
+            } else {
+                result.add(jw.getNodes());
+            }
+        }
+        return result;
+    }
+
+    private GeneralPath createPath(List<Node> nodes) {
+        GeneralPath result = new GeneralPath();
+        result.moveTo((float)nodes.get(0).getCoor().lat(), (float)nodes.get(0).getCoor().lon());
+        for (int i=1; i<nodes.size(); i++) {
+            Node n = nodes.get(i);
+            result.lineTo((float)n.getCoor().lat(), (float)n.getCoor().lon());
+        }
+        return result;
+    }
+
+    private List<GeneralPath> createPolygons(List<List<Node>> joinedWays) {
+        List<GeneralPath> result = new ArrayList<GeneralPath>();
+        for (List<Node> way: joinedWays) {
+            result.add(createPath(way));
+        }
+        return result;
+    }
+
+    private Intersection getPolygonIntersection(GeneralPath outer, List<Node> inner) {
+        boolean inside = false;
+        boolean outside = false;
+
+        for (Node n: inner) {
+            boolean contains = outer.contains(n.getCoor().lat(), n.getCoor().lon());
+            inside = inside | contains;
+            outside = outside | !contains;
+            if (inside & outside) {
+                return Intersection.CROSSING;
+            }
+        }
+
+        return inside?Intersection.INSIDE:Intersection.OUTSIDE;
+    }
+
+    @Override
+    public void visit(Way w) {
+        if (styles != null && !w.isClosed())
+        {
+            ElemStyle e = styles.getArea(w);
+            if(e instanceof AreaElemStyle && !((AreaElemStyle)e).closed)
+                errors.add( new TestError(this, Severity.WARNING, tr("Area style way is not closed"), NOT_CLOSED,  w));
+        }
+    }
+
+    @Override
+    public void visit(Relation r) {
+        nonClosedWays.clear();
+        if ("multipolygon".equals(r.get("type"))) {
+            checkMembersAndRoles(r);
+
+            Multipolygon polygon = new Multipolygon(Main.map.mapView);
+            polygon.load(r);
+
+            if (polygon.getOuterWays().isEmpty()) {
+                errors.add( new TestError(this, Severity.WARNING, tr("No outer way for multipolygon"), MISSING_OUTER_WAY,  r));
+            }
+
+            for (RelationMember rm: r.getMembers()) {
+                if (!rm.getMember().isUsable()) {
+                    return; // Rest of checks is only for complete multipolygons
+                }
+            }
+
+            List<List<Node>> innerWays = joinWays(polygon.getInnerWays()); // Side effect - sets nonClosedWays
+            List<List<Node>> outerWays = joinWays(polygon.getOuterWays());
+
+            if(styles != null) {
+                ElemStyle wayStyle = styles.get(r);
+
+                // If area style was not found for relation then use style of ways
+                if(!(wayStyle instanceof AreaElemStyle)) {
+                    errors.add( new TestError(this, Severity.OTHER, tr("No style in multipolygon relation"),
+                    NO_STYLE_POLYGON, r));
+                    for (Way w : polygon.getOuterWays()) {
+                        wayStyle = styles.getArea(w);
+                        if(wayStyle != null) {
+                            break;
+                        }
+                    }
+                }
+
+                if (wayStyle instanceof AreaElemStyle) {
+                    for (Way wInner : polygon.getInnerWays())
+                    {
+                        ElemStyle innerStyle = styles.get(wInner);
+                        if(wayStyle != null && wayStyle.equals(innerStyle)) {
+                            List<OsmPrimitive> l = new ArrayList<OsmPrimitive>();
+                            l.add(r);
+                            l.add(wInner);
+                            errors.add( new TestError(this, Severity.WARNING, tr("Style for inner way equals multipolygon"),
+                            INNER_STYLE_MISMATCH, l, Collections.singletonList(wInner)));
+                        }
+                    }
+                    for (Way wOuter : polygon.getOuterWays())
+                    {
+                        ElemStyle outerStyle = styles.get(wOuter);
+                        if(outerStyle instanceof AreaElemStyle && !wayStyle.equals(outerStyle)) {
+                            List<OsmPrimitive> l = new ArrayList<OsmPrimitive>();
+                            l.add(r);
+                            l.add(wOuter);
+                            errors.add( new TestError(this, Severity.WARNING, tr("Style for outer way mismatches"),
+                            OUTER_STYLE_MISMATCH, l, Collections.singletonList(wOuter)));
+                        }
+                    }
+                }
+                else
+                    errors.add( new TestError(this, Severity.OTHER, tr("No style for multipolygon"),
+                    NO_STYLE, r));
+            }
+
+            if (!nonClosedWays.isEmpty()) {
+                errors.add( new TestError(this, Severity.WARNING, tr("Multipolygon is not closed"), NON_CLOSED_WAY,  Collections.singletonList(r), nonClosedWays));
+            }
+
+            // For painting is used Polygon class which works with ints only. For validation we need more precision
+            List<GeneralPath> outerPolygons = createPolygons(outerWays);
+            for (List<Node> pdInner: innerWays) {
+                boolean outside = true;
+                boolean crossing = false;
+                List<Node> outerWay = null;
+                for (int i=0; i<outerWays.size(); i++) {
+                    GeneralPath outer = outerPolygons.get(i);
+                    Intersection intersection = getPolygonIntersection(outer, pdInner);
+                    outside = outside & intersection == Intersection.OUTSIDE;
+                    if (intersection == Intersection.CROSSING) {
+                        crossing = true;
+                        outerWay = outerWays.get(i);
+                    }
+                }
+                if (outside || crossing) {
+                    List<List<Node>> highlights = new ArrayList<List<Node>>();
+                    highlights.add(pdInner);
+                    if (outside) {
+                        errors.add(new TestError(this, Severity.WARNING, tr("Multipolygon inner way is outside"), INNER_WAY_OUTSIDE, Collections.singletonList(r), highlights));
+                    } else if (crossing) {
+                        highlights.add(outerWay);
+                        errors.add(new TestError(this, Severity.WARNING, tr("Intersection between multipolygon ways"), CROSSING_WAYS, Collections.singletonList(r), highlights));
+                    }
+                }
+            }
+        }
+    }
+
+    private void checkMembersAndRoles(Relation r) {
+        for (RelationMember rm: r.getMembers()) {
+            if (rm.isWay()) {
+                if (!("inner".equals(rm.getRole()) || "outer".equals(rm.getRole()) || !rm.hasRole())) {
+                    errors.add( new TestError(this, Severity.WARNING, tr("No useful role for multipolygon member"), WRONG_MEMBER_ROLE, rm.getMember()));
+                }
+            } else {
+                errors.add( new TestError(this, Severity.WARNING, tr("Non-Way in multipolygon"), WRONG_MEMBER_TYPE, rm.getMember()));
+            }
+        }
+    }
+
+
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/NameMismatch.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/NameMismatch.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/NameMismatch.java	(revision 3669)
@@ -0,0 +1,110 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map.Entry;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+
+/**
+ * Check for missing name:* translations.
+ * <p>
+ * This test finds multilingual objects whose 'name' attribute is not
+ * equal to any 'name:*' attribute and not a composition of some
+ * 'name:*' attributes separated by ' - '.
+ * <p>
+ * For example, a node with name=Europe, name:de=Europa should have
+ * name:en=Europe to avoid triggering this test.  An object with
+ * name='Suomi - Finland' should have at least name:fi=Suomi and
+ * name:sv=Finland to avoid a warning (name:et=Soome would not
+ * matter).  Also, complain if an object has some name:* attribute but
+ * no name.
+ *
+ * @author Skela
+ */
+public class NameMismatch extends Test {
+    protected static final int NAME_MISSING = 1501;
+    protected static final int NAME_TRANSLATION_MISSING = 1502;
+
+    public NameMismatch() {
+        super(tr("Missing name:* translation."),
+            tr("This test finds multilingual objects whose 'name' attribute is not equal to some 'name:*' attribute and not a composition of 'name:*' attributes, e.g., Italia - Italien - Italy."));
+    }
+
+    /**
+     * Report a missing translation.
+     *
+     * @param p The primitive whose translation is missing
+     */
+    private void missingTranslation(OsmPrimitive p) {
+        errors.add(new TestError(this, Severity.OTHER,
+            tr("A name:* translation is missing."),
+            NAME_TRANSLATION_MISSING, p));
+    }
+
+    /**
+     * Check a primitive for a name mismatch.
+     *
+     * @param p The primitive to be tested
+     */
+    public void check(OsmPrimitive p) {
+        HashSet<String> names = new HashSet<String>();
+
+        for (Entry<String, String> entry : p.getKeys().entrySet()) {
+            if (entry.getKey().startsWith("name:")) {
+                String name_s = entry.getValue();
+                if (name_s != null) {
+                    names.add(name_s);
+                }
+            }
+        }
+
+        if (names.isEmpty()) return;
+
+        String name = p.get("name");
+
+        if (name == null) {
+            errors.add(new TestError(this, Severity.OTHER,
+                tr("A name is missing, even though name:* exists."),
+                                     NAME_MISSING, p));
+        return;
+    }
+
+        if (names.contains(name)) return;
+        /* If name is not equal to one of the name:*, it should be a
+        composition of some (not necessarily all) name:* labels.
+        Check if this is the case. */
+
+        String split_names[] = name.split(" - ");
+        if (split_names.length == 1) {
+            /* The name is not composed of multiple parts. Complain. */
+            missingTranslation(p);
+            return;
+        }
+
+        /* Check that each part corresponds to a translated name:*. */
+        for (String n : split_names) {
+            if (!names.contains(n)) {
+                missingTranslation(p);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Checks a name mismatch in all primitives.
+     *
+     * @param selection The primitives to be tested
+     */
+    @Override public void visit(Collection<OsmPrimitive> selection) {
+        for (OsmPrimitive p : selection)
+            if (!p.isDeleted() && !p.isIncomplete())
+                check(p);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/NodesWithSameName.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/NodesWithSameName.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/NodesWithSameName.java	(revision 3669)
@@ -0,0 +1,72 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Map;
+import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.ArrayList;
+
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+public class NodesWithSameName extends Test {
+    protected static int SAME_NAME = 801;
+
+    private Map<String, List<Node>> namesToNodes;
+
+    public NodesWithSameName() {
+        super(tr("Nodes with same name"),
+            tr("This test finds nodes that have the same name (might be duplicates)."));
+    }
+
+    @Override public void startTest(ProgressMonitor monitor) {
+        super.startTest(monitor);
+        namesToNodes = new HashMap<String, List<Node>>();
+    }
+
+    @Override public void visit(Node n) {
+        if (!n.isUsable()) return;
+
+        String name = n.get("name");
+        String sign = n.get("traffic_sign");
+        String highway = n.get("highway");
+        if (name == null
+            || (sign != null && sign.equals("city_limit"))
+            || (highway != null && highway.equals("bus_stop"))) {
+            return;
+        }
+
+        List<Node> nodes = namesToNodes.get(name);
+        if (nodes == null)
+            namesToNodes.put(name, nodes = new ArrayList<Node>());
+
+        nodes.add(n);
+    }
+
+    @Override public void endTest() {
+        for (List<Node> nodes : namesToNodes.values()) {
+            if (nodes.size() > 1) {
+                // Report the same-name nodes, unless each has a unique ref=*.
+                HashSet<String> refs = new HashSet<String>();
+
+                for (Node n : nodes) {
+                    String ref = n.get("ref");
+                    if (ref == null || !refs.add(ref)) {
+                        errors.add(new TestError(this, Severity.OTHER,
+                            tr("Nodes with same name"), SAME_NAME, nodes));
+                        break;
+                    }
+                }
+            }
+        }
+        super.endTest();
+        namesToNodes = null;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/OverlappingWays.java	(revision 3669)
@@ -0,0 +1,174 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.tools.Pair;
+
+/**
+ * Tests if there are overlapping ways
+ *
+ * @author frsantos
+ */
+public class OverlappingWays extends Test
+{
+    /** Bag of all way segments */
+    Bag<Pair<Node,Node>, WaySegment> nodePairs;
+
+    protected static int OVERLAPPING_HIGHWAY = 101;
+    protected static int OVERLAPPING_RAILWAY = 102;
+    protected static int OVERLAPPING_WAY = 103;
+    protected static int OVERLAPPING_HIGHWAY_AREA = 111;
+    protected static int OVERLAPPING_RAILWAY_AREA = 112;
+    protected static int OVERLAPPING_WAY_AREA = 113;
+    protected static int OVERLAPPING_AREA = 120;
+
+    /** Constructor */
+    public OverlappingWays()
+    {
+        super(tr("Overlapping ways."),
+              tr("This test checks that a connection between two nodes "
+                + "is not used by more than one way."));
+
+    }
+
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        nodePairs = new Bag<Pair<Node,Node>, WaySegment>(1000);
+    }
+
+    @Override
+    public void endTest()
+    {
+        Map<List<Way>, List<WaySegment>> ways_seen = new HashMap<List<Way>, List<WaySegment>>(500);
+
+        for (List<WaySegment> duplicated : nodePairs.values())
+        {
+            int ways = duplicated.size();
+
+            if (ways > 1)
+            {
+                List<OsmPrimitive> prims = new ArrayList<OsmPrimitive>();
+                List<Way> current_ways = new ArrayList<Way>();
+                List<WaySegment> highlight;
+                int highway = 0;
+                int railway = 0;
+                int area = 0;
+
+                for (WaySegment ws : duplicated)
+                {
+                    if (ws.way.get("highway") != null)
+                        highway++;
+                    else if (ws.way.get("railway") != null)
+                        railway++;
+                    Boolean ar = OsmUtils.getOsmBoolean(ws.way.get("area"));
+                    if (ar != null && ar)
+                        area++;
+                    if (ws.way.get("landuse") != null || ws.way.get("natural") != null
+                    || ws.way.get("amenity") != null || ws.way.get("leisure") != null
+                    || ws.way.get("building") != null)
+                    {
+                        area++; ways--;
+                    }
+
+                    prims.add(ws.way);
+                    current_ways.add(ws.way);
+                }
+                /* These ways not seen before
+                 * If two or more of the overlapping ways are
+                 * highways or railways mark a separate error
+                 */
+                if ((highlight = ways_seen.get(current_ways)) == null)
+                {
+                    String errortype;
+                    int type;
+
+                    if(area > 0)
+                    {
+                        if (ways == 0 || duplicated.size() == area)
+                        {
+                            errortype = tr("Overlapping areas");
+                            type = OVERLAPPING_AREA;
+                        }
+                        else if (highway == ways)
+                        {
+                            errortype = tr("Overlapping highways (with area)");
+                            type = OVERLAPPING_HIGHWAY_AREA;
+                        }
+                        else if (railway == ways)
+                        {
+                            errortype = tr("Overlapping railways (with area)");
+                            type = OVERLAPPING_RAILWAY_AREA;
+                        }
+                        else
+                        {
+                            errortype = tr("Overlapping ways (with area)");
+                            type = OVERLAPPING_WAY_AREA;
+                        }
+                    }
+                    else if (highway == ways)
+                    {
+                        errortype = tr("Overlapping highways");
+                        type = OVERLAPPING_HIGHWAY;
+                    }
+                    else if (railway == ways)
+                    {
+                        errortype = tr("Overlapping railways");
+                        type = OVERLAPPING_RAILWAY;
+                    }
+                    else
+                    {
+                        errortype = tr("Overlapping ways");
+                        type = OVERLAPPING_WAY;
+                    }
+
+                    errors.add(new TestError(this, type < OVERLAPPING_HIGHWAY_AREA
+                    ? Severity.WARNING : Severity.OTHER, tr(errortype), type, prims, duplicated));
+                    ways_seen.put(current_ways, duplicated);
+                }
+                else    /* way seen, mark highlight layer only */
+                {
+                    for (WaySegment ws : duplicated)
+                        highlight.add(ws);
+                }
+            }
+        }
+        super.endTest();
+        nodePairs = null;
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        Node lastN = null;
+        int i = -2;
+        for (Node n : w.getNodes()) {
+            i++;
+            if (lastN == null) {
+                lastN = n;
+                continue;
+            }
+            nodePairs.add(Pair.sort(new Pair<Node,Node>(lastN, n)),
+                new WaySegment(w, i));
+            lastN = n;
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java	(revision 3669)
@@ -0,0 +1,206 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.preferences.TaggingPresetPreference;
+import org.openstreetmap.josm.gui.tagging.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.TaggingPreset.PresetType;
+
+/**
+ * Check for wrong relations
+ *
+ */
+public class RelationChecker extends Test
+{
+    protected static int ROLE_UNKNOWN      = 1701;
+    protected static int ROLE_EMPTY        = 1702;
+    protected static int WRONG_TYPE        = 1703;
+    protected static int HIGH_COUNT        = 1704;
+    protected static int LOW_COUNT         = 1705;
+    protected static int ROLE_MISSING      = 1706;
+    protected static int RELATION_UNKNOWN  = 1707;
+    protected static int RELATION_EMPTY    = 1708;
+
+    /**
+     * Constructor
+     */
+    public RelationChecker()
+    {
+        super(tr("Relation checker :"),
+                tr("This plugin checks for errors in relations."));
+    }
+
+    @Override
+    public void initialize() throws Exception
+    {
+        initializePresets();
+    }
+
+    static Collection<TaggingPreset> relationpresets = new LinkedList<TaggingPreset>();
+    /**
+     * Reads the presets data.
+     *
+     */
+    public void initializePresets()
+    {
+        Collection<TaggingPreset> presets = TaggingPresetPreference.taggingPresets;
+        if(presets != null)
+        {
+            for(TaggingPreset p : presets)
+            {
+                for(TaggingPreset.Item i : p.data)
+                {
+                    if(i instanceof TaggingPreset.Roles)
+                    {
+                        relationpresets.add(p);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    public class RoleInfo
+    {
+        int total = 0;
+        int nodes = 0;
+        int ways = 0;
+        int closedways = 0;
+        int openways = 0;
+        int relations = 0;
+    }
+
+    @Override
+    public void visit(Relation n)
+    {
+        LinkedList<TaggingPreset.Role> allroles = new LinkedList<TaggingPreset.Role>();
+        for(TaggingPreset p : relationpresets)
+        {
+            boolean matches = true;
+            TaggingPreset.Roles r = null;
+            for(TaggingPreset.Item i : p.data)
+            {
+                if(i instanceof TaggingPreset.Key)
+                {
+                    TaggingPreset.Key k = (TaggingPreset.Key)i;
+                    if(!k.value.equals(n.get(k.key)))
+                    {
+                        matches = false;
+                        break;
+                    }
+                }
+                else if(i instanceof TaggingPreset.Roles)
+                    r = (TaggingPreset.Roles) i;
+            }
+            if(matches && r != null)
+                allroles.addAll(r.roles);
+        }
+        if(allroles.size() == 0)
+        {
+            errors.add( new TestError(this, Severity.WARNING, tr("Relation type is unknown"),
+                    RELATION_UNKNOWN, n) );
+
+        }
+        else
+        {
+            HashMap<String,RoleInfo> map = new HashMap<String, RoleInfo>();
+            for(RelationMember m : n.getMembers()) {
+                String s = "";
+                if(m.hasRole())
+                    s = m.getRole();
+                RoleInfo ri = map.get(s);
+                if(ri == null)
+                    ri = new RoleInfo();
+                ri.total++;
+                if(m.isRelation())
+                    ri.relations++;
+                else if(m.isWay())
+                {
+                    ri.ways++;
+                    if(m.getWay().isClosed())
+                        ri.closedways++;
+                    else
+                        ri.openways++;
+                }
+                else if(m.isNode())
+                    ri.nodes++;
+                map.put(s, ri);
+            }
+            if(map.size() == 0)
+                errors.add( new TestError(this, Severity.ERROR, tr("Relation is empty"),
+                        RELATION_EMPTY, n) );
+            else
+            {
+                LinkedList<String> done = new LinkedList<String>();
+                for(TaggingPreset.Role r : allroles)
+                {
+                    done.add(r.key);
+                    String keyname = r.key;
+                    if(keyname == "")
+                        keyname = tr("<empty>");
+                    RoleInfo ri = map.get(r.key);
+                    long count = (ri == null) ? 0 : ri.total;
+                    long vc = r.getValidCount(count);
+                    if(count != vc)
+                    {
+                        if(count == 0)
+                        {
+                            String s = marktr("Role {0} missing");
+                            errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                    tr(s, keyname), MessageFormat.format(s, keyname), ROLE_MISSING, n) );
+                        }
+                        else if(vc > count)
+                        {
+                            String s = marktr("Number of {0} roles too low ({1})");
+                            errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                    tr(s, keyname, count), MessageFormat.format(s, keyname, count), LOW_COUNT, n) );
+                        }
+                        else
+                        {
+                            String s = marktr("Number of {0} roles too high ({1})");
+                            errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                    tr(s, keyname, count), MessageFormat.format(s, keyname, count), HIGH_COUNT, n) );
+                        }
+                    }
+                    if(ri != null && ((!r.types.contains(PresetType.WAY) && (r.types.contains(PresetType.CLOSEDWAY) ? ri.openways > 0 : ri.ways > 0))
+                            || (!r.types.contains(PresetType.NODE) && ri.nodes > 0) || (!r.types.contains(PresetType.RELATION) && ri.relations > 0)))
+                    {
+                        String s = marktr("Member for role {0} of wrong type");
+                        errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                tr(s, keyname), MessageFormat.format(s, keyname), WRONG_TYPE, n) );
+                    }
+                }
+                for(String key : map.keySet())
+                {
+                    if(!done.contains(key))
+                    {
+                        if(key.length() > 0)
+                        {
+                            String s = marktr("Role {0} unknown");
+                            errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                    tr(s, key), MessageFormat.format(s, key), ROLE_UNKNOWN, n) );
+                        }
+                        else
+                        {
+                            String s = marktr("Empty role found");
+                            errors.add( new TestError(this, Severity.WARNING, tr("Role verification problem"),
+                                    tr(s), s, ROLE_EMPTY, n) );
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/SelfIntersectingWay.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/SelfIntersectingWay.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/SelfIntersectingWay.java	(revision 3669)
@@ -0,0 +1,42 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.HashSet;
+import java.util.Arrays;
+
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+
+/**
+ * Checks for self-intersecting ways.
+ */
+public class SelfIntersectingWay extends Test {
+    protected static int SELF_INTERSECT = 401;
+
+    public SelfIntersectingWay() {
+        super(tr("Self-intersecting ways"),
+              tr("This test checks for ways " +
+                "that contain some of their nodes more than once."));
+    }
+
+    @Override public void visit(Way w) {
+        HashSet<Node> nodes = new HashSet<Node>();
+
+        for (int i = 1; i < w.getNodesCount() - 1; i++) {
+            Node n = w.getNode(i);
+            if (nodes.contains(n)) {
+                errors.add(new TestError(this,
+                    Severity.WARNING, tr("Self-intersecting ways"), SELF_INTERSECT,
+                    Arrays.asList(w), Arrays.asList(n)));
+                break;
+            } else {
+                nodes.add(n);
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/SimilarNamedWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/SimilarNamedWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/SimilarNamedWays.java	(revision 3669)
@@ -0,0 +1,176 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.data.validation.util.ValUtil;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Checks for similar named ways, symptom of a possible typo. It uses the
+ * Levenshtein distance to check for similarity
+ *
+ * @author frsantos
+ */
+public class SimilarNamedWays extends Test
+{
+    protected static int SIMILAR_NAMED = 701;
+
+    /** All ways, grouped by cells */
+    Map<Point2D,List<Way>> cellWays;
+    /** The already detected errors */
+    Bag<Way, Way> errorWays;
+
+    /**
+     * Constructor
+     */
+    public SimilarNamedWays()
+    {
+        super(tr("Similarly named ways")+".",
+              tr("This test checks for ways with similar names that may have been misspelled."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        cellWays = new HashMap<Point2D,List<Way>>(1000);
+        errorWays = new Bag<Way, Way>();
+    }
+
+    @Override
+    public void endTest()
+    {
+        cellWays = null;
+        errorWays = null;
+        super.endTest();
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        if( !w.isUsable() )
+            return;
+
+        String name = w.get("name");
+        if( name == null || name.length() < 6 )
+            return;
+
+        List<List<Way>> theCellWays = ValUtil.getWaysInCell(w, cellWays);
+        for( List<Way> ways : theCellWays)
+        {
+            for( Way w2 : ways)
+            {
+                if( errorWays.contains(w, w2) || errorWays.contains(w2, w) )
+                    continue;
+
+                String name2 = w2.get("name");
+                if( name2 == null || name2.length() < 6 )
+                    continue;
+
+                int levenshteinDistance = getLevenshteinDistance(name, name2);
+                if( 0 < levenshteinDistance && levenshteinDistance <= 2 )
+                {
+                    List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>();
+                    primitives.add(w);
+                    primitives.add(w2);
+                    errors.add( new TestError(this, Severity.WARNING, tr("Similarly named ways"), SIMILAR_NAMED, primitives) );
+                    errorWays.add(w, w2);
+                }
+            }
+            ways.add(w);
+        }
+    }
+
+    /**
+     * Compute Levenshtein distance
+     *
+     * @param s First word
+     * @param t Second word
+     * @return The distance between words
+     */
+    public int getLevenshteinDistance(String s, String t)
+    {
+        int d[][]; // matrix
+        int n; // length of s
+        int m; // length of t
+        int i; // iterates through s
+        int j; // iterates through t
+        char s_i; // ith character of s
+        char t_j; // jth character of t
+        int cost; // cost
+
+        // Step 1
+
+        n = s.length();
+        m = t.length();
+        if (n == 0) return m;
+        if (m == 0) return n;
+        d = new int[n + 1][m + 1];
+
+        // Step 2
+        for (i = 0; i <= n; i++) d[i][0] = i;
+        for (j = 0; j <= m; j++) d[0][j] = j;
+
+        // Step 3
+        for (i = 1; i <= n; i++)
+        {
+            s_i = s.charAt(i - 1);
+
+            // Step 4
+            for (j = 1; j <= m; j++)
+            {
+                t_j = t.charAt(j - 1);
+
+                // Step 5
+                if (s_i == t_j)
+                {
+                    cost = 0;
+                }
+                else
+                {
+                    cost = 1;
+                }
+
+                // Step 6
+                d[i][j] = Minimum(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
+            }
+        }
+
+        // Step 7
+        return d[n][m];
+    }
+
+    /**
+     * Get minimum of three values
+     * @param a First value
+     * @param b Second value
+     * @param c Third value
+     * @return The minimum of the tre values
+     */
+    private static int Minimum(int a, int b, int c)
+    {
+        int mi = a;
+        if (b < mi)
+        {
+            mi = b;
+        }
+        if (c < mi)
+        {
+            mi = c;
+        }
+        return mi;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/TagChecker.java	(revision 3669)
@@ -0,0 +1,1046 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.ChangePropertyCommand;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.SequenceCommand;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.data.validation.util.Entities;
+import org.openstreetmap.josm.data.validation.util.ValUtil;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.gui.preferences.TaggingPresetPreference;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.tagging.TaggingPreset;
+import org.openstreetmap.josm.io.MirroredInputStream;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Check for misspelled or wrong properties
+ *
+ * @author frsantos
+ */
+public class TagChecker extends Test
+{
+    /** The default data files */
+    public static final String DATA_FILE = "resource://data/tagchecker.cfg";
+    public static final String IGNORE_FILE = "resource://data/ignoretags.cfg";
+    public static final String SPELL_FILE = "resource://data/words.cfg";
+
+    /** The spell check key substitutions: the key should be substituted by the value */
+    protected static Map<String, String> spellCheckKeyData;
+    /** The spell check preset values */
+    protected static Bag<String, String> presetsValueData;
+    /** The TagChecker data */
+    protected static List<CheckerData> checkerData = new ArrayList<CheckerData>();
+    protected static List<String> ignoreDataStartsWith = new ArrayList<String>();
+    protected static List<String> ignoreDataEquals = new ArrayList<String>();
+    protected static List<String> ignoreDataEndsWith = new ArrayList<String>();
+    protected static List<IgnoreKeyPair> ignoreDataKeyPair = new ArrayList<IgnoreKeyPair>();
+    protected static List<IgnoreTwoKeyPair> ignoreDataTwoKeyPair = new ArrayList<IgnoreTwoKeyPair>();
+
+    /** The preferences prefix */
+    protected static final String PREFIX = ValidatorPreference.PREFIX + "." + TagChecker.class.getSimpleName();
+
+    public static final String PREF_CHECK_VALUES = PREFIX + ".checkValues";
+    public static final String PREF_CHECK_KEYS = PREFIX + ".checkKeys";
+    public static final String PREF_CHECK_COMPLEX = PREFIX + ".checkComplex";
+    public static final String PREF_CHECK_FIXMES = PREFIX + ".checkFixmes";
+
+    public static final String PREF_SOURCES = PREFIX + ".sources";
+    public static final String PREF_USE_DATA_FILE = PREFIX + ".usedatafile";
+    public static final String PREF_USE_IGNORE_FILE = PREFIX + ".useignorefile";
+    public static final String PREF_USE_SPELL_FILE = PREFIX + ".usespellfile";
+
+    public static final String PREF_CHECK_KEYS_BEFORE_UPLOAD = PREF_CHECK_KEYS + "BeforeUpload";
+    public static final String PREF_CHECK_VALUES_BEFORE_UPLOAD = PREF_CHECK_VALUES + "BeforeUpload";
+    public static final String PREF_CHECK_COMPLEX_BEFORE_UPLOAD = PREF_CHECK_COMPLEX + "BeforeUpload";
+    public static final String PREF_CHECK_FIXMES_BEFORE_UPLOAD = PREF_CHECK_FIXMES + "BeforeUpload";
+
+    protected boolean checkKeys = false;
+    protected boolean checkValues = false;
+    protected boolean checkComplex = false;
+    protected boolean checkFixmes = false;
+
+    protected JCheckBox prefCheckKeys;
+    protected JCheckBox prefCheckValues;
+    protected JCheckBox prefCheckComplex;
+    protected JCheckBox prefCheckFixmes;
+    protected JCheckBox prefCheckPaint;
+
+    protected JCheckBox prefCheckKeysBeforeUpload;
+    protected JCheckBox prefCheckValuesBeforeUpload;
+    protected JCheckBox prefCheckComplexBeforeUpload;
+    protected JCheckBox prefCheckFixmesBeforeUpload;
+    protected JCheckBox prefCheckPaintBeforeUpload;
+
+    protected JCheckBox prefUseDataFile;
+    protected JCheckBox prefUseIgnoreFile;
+    protected JCheckBox prefUseSpellFile;
+
+    protected JButton addSrcButton;
+    protected JButton editSrcButton;
+    protected JButton deleteSrcButton;
+
+    protected static int EMPTY_VALUES      = 1200;
+    protected static int INVALID_KEY       = 1201;
+    protected static int INVALID_VALUE     = 1202;
+    protected static int FIXME             = 1203;
+    protected static int INVALID_SPACE     = 1204;
+    protected static int INVALID_KEY_SPACE = 1205;
+    protected static int INVALID_HTML      = 1206; /* 1207 was PAINT */
+    protected static int LONG_VALUE        = 1208;
+    protected static int LONG_KEY          = 1209;
+    protected static int LOW_CHAR_VALUE    = 1210;
+    protected static int LOW_CHAR_KEY      = 1211;
+    /** 1250 and up is used by tagcheck */
+
+    /** List of sources for spellcheck data */
+    protected JList Sources;
+
+
+    protected static Entities entities = new Entities();
+    /**
+     * Constructor
+     */
+    public TagChecker()
+    {
+        super(tr("Properties checker :"),
+                tr("This plugin checks for errors in property keys and values."));
+    }
+
+    @Override
+    public void initialize() throws Exception
+    {
+        initializeData();
+        initializePresets();
+    }
+
+    /**
+     * Reads the spellcheck file into a HashMap.
+     * The data file is a list of words, beginning with +/-. If it starts with +,
+     * the word is valid, but if it starts with -, the word should be replaced
+     * by the nearest + word before this.
+     *
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    private static void initializeData() throws IOException
+    {
+        spellCheckKeyData = new HashMap<String, String>();
+        String sources = Main.pref.get( PREF_SOURCES, "");
+        if(Main.pref.getBoolean(PREF_USE_DATA_FILE, true))
+        {
+            if( sources == null || sources.length() == 0)
+                sources = DATA_FILE;
+            else
+                sources = DATA_FILE + ";" + sources;
+        }
+        if(Main.pref.getBoolean(PREF_USE_IGNORE_FILE, true))
+        {
+            if( sources == null || sources.length() == 0)
+                sources = IGNORE_FILE;
+            else
+                sources = IGNORE_FILE + ";" + sources;
+        }
+        if(Main.pref.getBoolean(PREF_USE_SPELL_FILE, true))
+        {
+            if( sources == null || sources.length() == 0)
+                sources = SPELL_FILE;
+            else
+                sources = SPELL_FILE + ";" + sources;
+        }
+
+        String errorSources = "";
+        if(sources.length() == 0)
+            return;
+        for(String source: sources.split(";"))
+        {
+            try
+            {
+                MirroredInputStream s = new MirroredInputStream(source, ValUtil.getPluginDir(), -1);
+                InputStreamReader r;
+                try
+                {
+                    r = new InputStreamReader(s, "UTF-8");
+                }
+                catch (UnsupportedEncodingException e)
+                {
+                    r = new InputStreamReader(s);
+                }
+                BufferedReader reader = new BufferedReader(r);
+
+                String okValue = null;
+                boolean tagcheckerfile = false;
+                boolean ignorefile = false;
+                String line;
+                while((line = reader.readLine()) != null && (tagcheckerfile || line.length() != 0))
+                {
+                    if(line.startsWith("#"))
+                    {
+                        if(line.startsWith("# JOSM TagChecker"))
+                            tagcheckerfile = true;
+                        if(line.startsWith("# JOSM IgnoreTags"))
+                            ignorefile = true;
+                        continue;
+                    }
+                    else if(ignorefile)
+                    {
+                        line = line.trim();
+                        if(line.length() < 4)
+                            continue;
+
+                        String key = line.substring(0, 2);
+                        line = line.substring(2);
+
+                        if(key.equals("S:"))
+                        {
+                            ignoreDataStartsWith.add(line);
+                        }
+                        else if(key.equals("E:"))
+                        {
+                            ignoreDataEquals.add(line);
+                        }
+                        else if(key.equals("F:"))
+                        {
+                            ignoreDataEndsWith.add(line);
+                        }
+                        else if(key.equals("K:"))
+                        {
+                            IgnoreKeyPair tmp = new IgnoreKeyPair();
+                            int mid = line.indexOf("=");
+                            tmp.key = line.substring(0, mid);
+                            tmp.value = line.substring(mid+1);
+                            ignoreDataKeyPair.add(tmp);
+                        }
+                        else if(key.equals("T:"))
+                        {
+                            IgnoreTwoKeyPair tmp = new IgnoreTwoKeyPair();
+                            int mid = line.indexOf("=");
+                            int split = line.indexOf("|");
+                            tmp.key1 = line.substring(0, mid);
+                            tmp.value1 = line.substring(mid+1, split);
+                            line = line.substring(split+1);
+                            mid = line.indexOf("=");
+                            tmp.key2 = line.substring(0, mid);
+                            tmp.value2 = line.substring(mid+1);
+                            ignoreDataTwoKeyPair.add(tmp);
+                        }
+                        continue;
+                    }
+                    else if(tagcheckerfile)
+                    {
+                        if(line.length() > 0)
+                        {
+                            CheckerData d = new CheckerData();
+                            String err = d.getData(line);
+
+                            if(err == null)
+                                checkerData.add(d);
+                            else
+                                System.err.println(tr("Invalid tagchecker line - {0}: {1}", err, line));
+                        }
+                    }
+                    else if(line.charAt(0) == '+')
+                    {
+                        okValue = line.substring(1);
+                    }
+                    else if(line.charAt(0) == '-' && okValue != null)
+                    {
+                        spellCheckKeyData.put(line.substring(1), okValue);
+                    }
+                    else
+                    {
+                        System.err.println(tr("Invalid spellcheck line: {0}", line));
+                    }
+                }
+            }
+            catch (IOException e)
+            {
+                errorSources += source + "\n";
+            }
+        }
+
+        if( errorSources.length() > 0 )
+            throw new IOException( tr("Could not access data file(s):\n{0}", errorSources) );
+    }
+
+    /**
+     * Reads the presets data.
+     *
+     * @throws Exception
+     */
+    public static void initializePresets() throws Exception
+    {
+        if( !Main.pref.getBoolean(PREF_CHECK_VALUES, true) )
+            return;
+
+        Collection<TaggingPreset> presets = TaggingPresetPreference.taggingPresets;
+        if(presets != null)
+        {
+            presetsValueData = new Bag<String, String>();
+            for(String a : OsmPrimitive.getUninterestingKeys())
+                presetsValueData.add(a);
+            // TODO directionKeys are no longer in OsmPrimitive (search pattern is used instead)
+            /*  for(String a : OsmPrimitive.getDirectionKeys())
+                presetsValueData.add(a);
+             */
+            for(String a : Main.pref.getCollection(ValidatorPreference.PREFIX + ".knownkeys",
+                    Arrays.asList(new String[]{"is_in", "int_ref", "fixme", "population"})))
+                presetsValueData.add(a);
+            for(TaggingPreset p : presets)
+            {
+                for(TaggingPreset.Item i : p.data)
+                {
+                    if(i instanceof TaggingPreset.Combo)
+                    {
+                        TaggingPreset.Combo combo = (TaggingPreset.Combo) i;
+                        if(combo.values != null)
+                        {
+                            for(String value : combo.values.split(","))
+                                presetsValueData.add(combo.key, value);
+                        }
+                    }
+                    else if(i instanceof TaggingPreset.Key)
+                    {
+                        TaggingPreset.Key k = (TaggingPreset.Key) i;
+                        presetsValueData.add(k.key, k.value);
+                    }
+                    else if(i instanceof TaggingPreset.Text)
+                    {
+                        TaggingPreset.Text k = (TaggingPreset.Text) i;
+                        presetsValueData.add(k.key);
+                    }
+                    else if(i instanceof TaggingPreset.Check)
+                    {
+                        TaggingPreset.Check k = (TaggingPreset.Check) i;
+                        presetsValueData.add(k.key, "yes");
+                        presetsValueData.add(k.key, "no");
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void visit(Node n)
+    {
+        checkPrimitive(n);
+    }
+
+
+    @Override
+    public void visit(Relation n)
+    {
+        checkPrimitive(n);
+    }
+
+
+    @Override
+    public void visit(Way w)
+    {
+        checkPrimitive(w);
+    }
+
+    /**
+     * Checks given string (key or value) if it contains characters with code below 0x20 (either newline or some other special characters)
+     * @param s string to check
+     */
+    private boolean containsLow(String s) {
+        if (s==null) return false;
+        for(int i=0;i<s.length();i++) {
+            if (s.charAt(i)<0x20) return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks the primitive properties
+     * @param p The primitive to check
+     */
+    private void checkPrimitive(OsmPrimitive p)
+    {
+        // Just a collection to know if a primitive has been already marked with error
+        Bag<OsmPrimitive, String> withErrors = new Bag<OsmPrimitive, String>();
+
+        if(checkComplex)
+        {
+            Map<String, String> props = (p.getKeys() == null) ? Collections.<String, String>emptyMap() : p.getKeys();
+            for(Entry<String, String> prop: props.entrySet() )
+            {
+                boolean ignore = true;
+                String key1 = prop.getKey();
+                String value1 = prop.getValue();
+
+                for(IgnoreTwoKeyPair a : ignoreDataTwoKeyPair)
+                {
+                    if(key1.equals(a.key1) && value1.equals(a.value1))
+                    {
+                        ignore = false;
+                        for(Entry<String, String> prop2: props.entrySet() )
+                        {
+                            String key2 = prop2.getKey();
+                            String value2 = prop2.getValue();
+                            for(IgnoreTwoKeyPair b : ignoreDataTwoKeyPair)
+                            {
+                                if(key2.equals(b.key2) && value2.equals(b.value2))
+                                {
+                                    ignore = true;
+                                    break;
+                                }
+                            }
+                            if(ignore)
+                                break;
+                        }
+                    }
+                    if(ignore)
+                        break;
+                }
+
+                if(!ignore)
+                {
+                    errors.add( new TestError(this, Severity.ERROR, tr("Illegal tag/value combinations"),
+                            tr("Illegal tag/value combinations"), tr("Illegal tag/value combinations"), 1272, p) );
+                    withErrors.add(p, "TC");
+                }
+            }
+
+            Map<String, String> keys = p.getKeys();
+            for(CheckerData d : checkerData)
+            {
+                if(d.match(p, keys))
+                {
+                    errors.add( new TestError(this, d.getSeverity(), tr("Illegal tag/value combinations"),
+                            d.getDescription(), d.getDescriptionOrig(), d.getCode(), p) );
+                    withErrors.add(p, "TC");
+                }
+            }
+        }
+
+        Map<String, String> props = (p.getKeys() == null) ? Collections.<String, String>emptyMap() : p.getKeys();
+        for(Entry<String, String> prop: props.entrySet() )
+        {
+            String s = marktr("Key ''{0}'' invalid.");
+            String key = prop.getKey();
+            String value = prop.getValue();
+            if( checkValues && (containsLow(value)) && !withErrors.contains(p, "ICV"))
+            {
+                errors.add( new TestError(this, Severity.WARNING, tr("Tag value contains character with code less than 0x20"),
+                        tr(s, key), MessageFormat.format(s, key), LOW_CHAR_VALUE, p) );
+                withErrors.add(p, "ICV");
+            }
+            if( checkKeys && (containsLow(key)) && !withErrors.contains(p, "ICK"))
+            {
+                errors.add( new TestError(this, Severity.WARNING, tr("Tag key contains character with code less than 0x20"),
+                        tr(s, key), MessageFormat.format(s, key), LOW_CHAR_KEY, p) );
+                withErrors.add(p, "ICK");
+            }
+            if( checkValues && (value!=null && value.length() > 255) && !withErrors.contains(p, "LV"))
+            {
+                errors.add( new TestError(this, Severity.ERROR, tr("Tag value longer than allowed"),
+                        tr(s, key), MessageFormat.format(s, key), LONG_VALUE, p) );
+                withErrors.add(p, "LV");
+            }
+            if( checkKeys && (key!=null && key.length() > 255) && !withErrors.contains(p, "LK"))
+            {
+                errors.add( new TestError(this, Severity.ERROR, tr("Tag key longer than allowed"),
+                        tr(s, key), MessageFormat.format(s, key), LONG_KEY, p) );
+                withErrors.add(p, "LK");
+            }
+            if( checkValues && (value==null || value.trim().length() == 0) && !withErrors.contains(p, "EV"))
+            {
+                errors.add( new TestError(this, Severity.WARNING, tr("Tags with empty values"),
+                        tr(s, key), MessageFormat.format(s, key), EMPTY_VALUES, p) );
+                withErrors.add(p, "EV");
+            }
+            if( checkKeys && spellCheckKeyData.containsKey(key) && !withErrors.contains(p, "IPK"))
+            {
+                errors.add( new TestError(this, Severity.WARNING, tr("Invalid property key"),
+                        tr(s, key), MessageFormat.format(s, key), INVALID_KEY, p) );
+                withErrors.add(p, "IPK");
+            }
+            if( checkKeys && key.indexOf(" ") >= 0 && !withErrors.contains(p, "IPK"))
+            {
+                errors.add( new TestError(this, Severity.WARNING, tr("Invalid white space in property key"),
+                        tr(s, key), MessageFormat.format(s, key), INVALID_KEY_SPACE, p) );
+                withErrors.add(p, "IPK");
+            }
+            if( checkValues && value != null && (value.startsWith(" ") || value.endsWith(" ")) && !withErrors.contains(p, "SPACE"))
+            {
+                errors.add( new TestError(this, Severity.OTHER, tr("Property values start or end with white space"),
+                        tr(s, key), MessageFormat.format(s, key), INVALID_SPACE, p) );
+                withErrors.add(p, "SPACE");
+            }
+            if( checkValues && value != null && !value.equals(entities.unescape(value)) && !withErrors.contains(p, "HTML"))
+            {
+                errors.add( new TestError(this, Severity.OTHER, tr("Property values contain HTML entity"),
+                        tr(s, key), MessageFormat.format(s, key), INVALID_HTML, p) );
+                withErrors.add(p, "HTML");
+            }
+            if( checkValues && value != null && value.length() > 0 && presetsValueData != null)
+            {
+                List<String> values = presetsValueData.get(key);
+                if(values == null)
+                {
+                    boolean ignore = false;
+                    for(String a : ignoreDataStartsWith)
+                    {
+                        if(key.startsWith(a))
+                            ignore = true;
+                    }
+                    for(String a : ignoreDataEquals)
+                    {
+                        if(key.equals(a))
+                            ignore = true;
+                    }
+                    for(String a : ignoreDataEndsWith)
+                    {
+                        if(key.endsWith(a))
+                            ignore = true;
+                    }
+                    if(!ignore)
+                    {
+                        String i = marktr("Key ''{0}'' not in presets.");
+                        errors.add( new TestError(this, Severity.OTHER, tr("Presets do not contain property key"),
+                                tr(i, key), MessageFormat.format(i, key), INVALID_VALUE, p) );
+                        withErrors.add(p, "UPK");
+                    }
+                }
+                else if(values.size() > 0 && !values.contains(prop.getValue()))
+                {
+                    boolean ignore = false;
+                    for(IgnoreKeyPair a : ignoreDataKeyPair)
+                    {
+                        if(key.equals(a.key) && value.equals(a.value))
+                            ignore = true;
+                    }
+
+                    for(IgnoreTwoKeyPair a : ignoreDataTwoKeyPair)
+                    {
+                        if(key.equals(a.key2) && value.equals(a.value2))
+                            ignore = true;
+                    }
+
+                    if(!ignore)
+                    {
+                        String i = marktr("Value ''{0}'' for key ''{1}'' not in presets.");
+                        errors.add( new TestError(this, Severity.OTHER, tr("Presets do not contain property value"),
+                                tr(i, prop.getValue(), key), MessageFormat.format(i, prop.getValue(), key), INVALID_VALUE, p) );
+                        withErrors.add(p, "UPV");
+                    }
+                }
+            }
+            if (checkFixmes && value != null && value.length() > 0) {
+                if ((value.toLowerCase().contains("fixme")
+                        || value.contains("check and delete")
+                        || key.contains("todo") || key.toLowerCase().contains("fixme"))
+                        && !withErrors.contains(p, "FIXME")) {
+                    errors.add(new TestError(this, Severity.OTHER,
+                            tr("FIXMES"), FIXME, p));
+                    withErrors.add(p, "FIXME");
+                }
+            }
+        }
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        checkKeys = Main.pref.getBoolean(PREF_CHECK_KEYS, true);
+        if( isBeforeUpload )
+            checkKeys = checkKeys && Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true);
+
+        checkValues = Main.pref.getBoolean(PREF_CHECK_VALUES, true);
+        if( isBeforeUpload )
+            checkValues = checkValues && Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true);
+
+        checkComplex = Main.pref.getBoolean(PREF_CHECK_COMPLEX, true);
+        if( isBeforeUpload )
+            checkComplex = checkValues && Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true);
+
+        checkFixmes = Main.pref.getBoolean(PREF_CHECK_FIXMES, true);
+        if( isBeforeUpload )
+            checkFixmes = checkFixmes && Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true);
+    }
+
+    @Override
+    public void visit(Collection<OsmPrimitive> selection)
+    {
+        if( checkKeys || checkValues || checkComplex || checkFixmes)
+            super.visit(selection);
+    }
+
+    @Override
+    public void addGui(JPanel testPanel)
+    {
+        GBC a = GBC.eol();
+        a.anchor = GridBagConstraints.EAST;
+
+        testPanel.add( new JLabel(name), GBC.eol().insets(3,0,0,0) );
+
+        prefCheckKeys = new JCheckBox(tr("Check property keys."), Main.pref.getBoolean(PREF_CHECK_KEYS, true));
+        prefCheckKeys.setToolTipText(tr("Validate that property keys are valid checking against list of words."));
+        testPanel.add(prefCheckKeys, GBC.std().insets(20,0,0,0));
+
+        prefCheckKeysBeforeUpload = new JCheckBox();
+        prefCheckKeysBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_KEYS_BEFORE_UPLOAD, true));
+        testPanel.add(prefCheckKeysBeforeUpload, a);
+
+        prefCheckComplex = new JCheckBox(tr("Use complex property checker."), Main.pref.getBoolean(PREF_CHECK_COMPLEX, true));
+        prefCheckComplex.setToolTipText(tr("Validate property values and tags using complex rules."));
+        testPanel.add(prefCheckComplex, GBC.std().insets(20,0,0,0));
+
+        prefCheckComplexBeforeUpload = new JCheckBox();
+        prefCheckComplexBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, true));
+        testPanel.add(prefCheckComplexBeforeUpload, a);
+
+        Sources = new JList(new DefaultListModel());
+
+        String sources = Main.pref.get( PREF_SOURCES );
+        if(sources != null && sources.length() > 0)
+        {
+            for(String source : sources.split(";"))
+                ((DefaultListModel)Sources.getModel()).addElement(source);
+        }
+
+        addSrcButton = new JButton(tr("Add"));
+        addSrcButton.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                String source = JOptionPane.showInputDialog(
+                        Main.parent,
+                        tr("TagChecker source"),
+                        tr("TagChecker source"),
+                        JOptionPane.QUESTION_MESSAGE
+                );
+                if (source != null)
+                    ((DefaultListModel)Sources.getModel()).addElement(source);
+                Sources.clearSelection();
+            }
+        });
+
+        editSrcButton = new JButton(tr("Edit"));
+        editSrcButton.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                int row = Sources.getSelectedIndex();
+                if(row == -1 && Sources.getModel().getSize() == 1)
+                {
+                    Sources.setSelectedIndex(0);
+                    row = 0;
+                }
+                if (row == -1)
+                {
+                    if(Sources.getModel().getSize() == 0)
+                    {
+                        String source = JOptionPane.showInputDialog(Main.parent, tr("TagChecker source"), tr("TagChecker source"), JOptionPane.QUESTION_MESSAGE);
+                        if (source != null)
+                            ((DefaultListModel)Sources.getModel()).addElement(source);
+                    }
+                    else
+                    {
+                        JOptionPane.showMessageDialog(
+                                Main.parent,
+                                tr("Please select the row to edit."),
+                                tr("Information"),
+                                JOptionPane.INFORMATION_MESSAGE
+                        );
+                    }
+                }
+                else {
+                    String source = (String)JOptionPane.showInputDialog(Main.parent,
+                            tr("TagChecker source"),
+                            tr("TagChecker source"),
+                            JOptionPane.QUESTION_MESSAGE, null, null,
+                            Sources.getSelectedValue());
+                    if (source != null)
+                        ((DefaultListModel)Sources.getModel()).setElementAt(source, row);
+                }
+                Sources.clearSelection();
+            }
+        });
+
+        deleteSrcButton = new JButton(tr("Delete"));
+        deleteSrcButton.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                if (Sources.getSelectedIndex() == -1)
+                    JOptionPane.showMessageDialog(Main.parent, tr("Please select the row to delete."), tr("Information"), JOptionPane.QUESTION_MESSAGE);
+                else {
+                    ((DefaultListModel)Sources.getModel()).remove(Sources.getSelectedIndex());
+                }
+            }
+        });
+        Sources.setMinimumSize(new Dimension(300,50));
+        Sources.setVisibleRowCount(3);
+
+        Sources.setToolTipText(tr("The sources (URL or filename) of spell check (see http://wiki.openstreetmap.org/index.php/User:JLS/speller) or tag checking data files."));
+        addSrcButton.setToolTipText(tr("Add a new source to the list."));
+        editSrcButton.setToolTipText(tr("Edit the selected source."));
+        deleteSrcButton.setToolTipText(tr("Delete the selected source from the list."));
+
+        testPanel.add(new JLabel(tr("Data sources")), GBC.eol().insets(23,0,0,0));
+        testPanel.add(new JScrollPane(Sources), GBC.eol().insets(23,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+        final JPanel buttonPanel = new JPanel(new GridBagLayout());
+        testPanel.add(buttonPanel, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        buttonPanel.add(addSrcButton, GBC.std().insets(0,5,0,0));
+        buttonPanel.add(editSrcButton, GBC.std().insets(5,5,5,0));
+        buttonPanel.add(deleteSrcButton, GBC.std().insets(0,5,0,0));
+
+        ActionListener disableCheckActionListener = new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                handlePrefEnable();
+            }
+        };
+        prefCheckKeys.addActionListener(disableCheckActionListener);
+        prefCheckKeysBeforeUpload.addActionListener(disableCheckActionListener);
+        prefCheckComplex.addActionListener(disableCheckActionListener);
+        prefCheckComplexBeforeUpload.addActionListener(disableCheckActionListener);
+
+        handlePrefEnable();
+
+        prefCheckValues = new JCheckBox(tr("Check property values."), Main.pref.getBoolean(PREF_CHECK_VALUES, true));
+        prefCheckValues.setToolTipText(tr("Validate that property values are valid checking against presets."));
+        testPanel.add(prefCheckValues, GBC.std().insets(20,0,0,0));
+
+        prefCheckValuesBeforeUpload = new JCheckBox();
+        prefCheckValuesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_VALUES_BEFORE_UPLOAD, true));
+        testPanel.add(prefCheckValuesBeforeUpload, a);
+
+        prefCheckFixmes = new JCheckBox(tr("Check for FIXMES."), Main.pref.getBoolean(PREF_CHECK_FIXMES, true));
+        prefCheckFixmes.setToolTipText(tr("Looks for nodes or ways with FIXME in any property value."));
+        testPanel.add(prefCheckFixmes, GBC.std().insets(20,0,0,0));
+
+        prefCheckFixmesBeforeUpload = new JCheckBox();
+        prefCheckFixmesBeforeUpload.setSelected(Main.pref.getBoolean(PREF_CHECK_FIXMES_BEFORE_UPLOAD, true));
+        testPanel.add(prefCheckFixmesBeforeUpload, a);
+
+        prefUseDataFile = new JCheckBox(tr("Use default data file."), Main.pref.getBoolean(PREF_USE_DATA_FILE, true));
+        prefUseDataFile.setToolTipText(tr("Use the default data file (recommended)."));
+        testPanel.add(prefUseDataFile, GBC.eol().insets(20,0,0,0));
+
+        prefUseIgnoreFile = new JCheckBox(tr("Use default tag ignore file."), Main.pref.getBoolean(PREF_USE_IGNORE_FILE, true));
+        prefUseIgnoreFile.setToolTipText(tr("Use the default tag ignore file (recommended)."));
+        testPanel.add(prefUseIgnoreFile, GBC.eol().insets(20,0,0,0));
+
+        prefUseSpellFile = new JCheckBox(tr("Use default spellcheck file."), Main.pref.getBoolean(PREF_USE_SPELL_FILE, true));
+        prefUseSpellFile.setToolTipText(tr("Use the default spellcheck file (recommended)."));
+        testPanel.add(prefUseSpellFile, GBC.eol().insets(20,0,0,0));
+    }
+
+    public void handlePrefEnable()
+    {
+        boolean selected = prefCheckKeys.isSelected() || prefCheckKeysBeforeUpload.isSelected()
+        || prefCheckComplex.isSelected() || prefCheckComplexBeforeUpload.isSelected();
+        Sources.setEnabled( selected );
+        addSrcButton.setEnabled(selected);
+        editSrcButton.setEnabled(selected);
+        deleteSrcButton.setEnabled(selected);
+    }
+
+    @Override
+    public boolean ok()
+    {
+        enabled = prefCheckKeys.isSelected() || prefCheckValues.isSelected() || prefCheckComplex.isSelected() || prefCheckFixmes.isSelected();
+        testBeforeUpload = prefCheckKeysBeforeUpload.isSelected() || prefCheckValuesBeforeUpload.isSelected()
+        || prefCheckFixmesBeforeUpload.isSelected() || prefCheckComplexBeforeUpload.isSelected();
+
+        Main.pref.put(PREF_CHECK_VALUES, prefCheckValues.isSelected());
+        Main.pref.put(PREF_CHECK_COMPLEX, prefCheckComplex.isSelected());
+        Main.pref.put(PREF_CHECK_KEYS, prefCheckKeys.isSelected());
+        Main.pref.put(PREF_CHECK_FIXMES, prefCheckFixmes.isSelected());
+        Main.pref.put(PREF_CHECK_VALUES_BEFORE_UPLOAD, prefCheckValuesBeforeUpload.isSelected());
+        Main.pref.put(PREF_CHECK_COMPLEX_BEFORE_UPLOAD, prefCheckComplexBeforeUpload.isSelected());
+        Main.pref.put(PREF_CHECK_KEYS_BEFORE_UPLOAD, prefCheckKeysBeforeUpload.isSelected());
+        Main.pref.put(PREF_CHECK_FIXMES_BEFORE_UPLOAD, prefCheckFixmesBeforeUpload.isSelected());
+        Main.pref.put(PREF_USE_DATA_FILE, prefUseDataFile.isSelected());
+        Main.pref.put(PREF_USE_IGNORE_FILE, prefUseIgnoreFile.isSelected());
+        Main.pref.put(PREF_USE_SPELL_FILE, prefUseSpellFile.isSelected());
+        String sources = "";
+        if( Sources.getModel().getSize() > 0 )
+        {
+            String sb = "";
+            for (int i = 0; i < Sources.getModel().getSize(); ++i)
+                sb += ";"+Sources.getModel().getElementAt(i);
+            sources = sb.substring(1);
+        }
+        if(sources.length() == 0)
+            sources = null;
+        return Main.pref.put(PREF_SOURCES, sources);
+    }
+
+    @Override
+    public Command fixError(TestError testError)
+    {
+        List<Command> commands = new ArrayList<Command>(50);
+
+        int i = -1;
+        List<? extends OsmPrimitive> primitives = testError.getPrimitives();
+        for(OsmPrimitive p : primitives )
+        {
+            i++;
+            Map<String, String> tags = p.getKeys();
+            if( tags == null || tags.size() == 0 )
+                continue;
+
+            for(Entry<String, String> prop: tags.entrySet() )
+            {
+                String key = prop.getKey();
+                String value = prop.getValue();
+                if( value == null || value.trim().length() == 0 )
+                    commands.add( new ChangePropertyCommand(Collections.singleton(primitives.get(i)), key, null) );
+                else if(value.startsWith(" ") || value.endsWith(" "))
+                    commands.add( new ChangePropertyCommand(Collections.singleton(primitives.get(i)), key, value.trim()) );
+                else if(key.startsWith(" ") || key.endsWith(" "))
+                    commands.add( new ChangePropertyKeyCommand(Collections.singleton(primitives.get(i)), key, key.trim()) );
+                else
+                {
+                    String evalue = entities.unescape(value);
+                    if(!evalue.equals(value))
+                        commands.add( new ChangePropertyCommand(Collections.singleton(primitives.get(i)), key, evalue) );
+                    else
+                    {
+                        String replacementKey = spellCheckKeyData.get(key);
+                        if( replacementKey != null )
+                        {
+                            commands.add( new ChangePropertyKeyCommand(Collections.singleton(primitives.get(i)),
+                                    key, replacementKey) );
+                        }
+                    }
+                }
+            }
+        }
+
+        if( commands.size() == 0 )
+            return null;
+        else if( commands.size() == 1 )
+            return commands.get(0);
+        else
+            return new SequenceCommand(tr("Fix properties"), commands);
+    }
+
+    @Override
+    public boolean isFixable(TestError testError)
+    {
+        if( testError.getTester() instanceof TagChecker)
+        {
+            int code = testError.getCode();
+            return code == INVALID_KEY || code == EMPTY_VALUES || code == INVALID_SPACE || code == INVALID_KEY_SPACE || code == INVALID_HTML;
+        }
+
+        return false;
+    }
+
+    protected static class IgnoreTwoKeyPair {
+        public String key1;
+        public String value1;
+        public String key2;
+        public String value2;
+    }
+
+    protected static class IgnoreKeyPair {
+        public String key;
+        public String value;
+    }
+
+    protected static class CheckerData {
+        private String description;
+        private List<CheckerElement> data = new ArrayList<CheckerElement>();
+        private OsmPrimitiveType type;
+        private int code;
+        protected Severity severity;
+        protected static int TAG_CHECK_ERROR  = 1250;
+        protected static int TAG_CHECK_WARN   = 1260;
+        protected static int TAG_CHECK_INFO   = 1270;
+
+        private static class CheckerElement {
+            public Object tag;
+            public Object value;
+            public boolean noMatch;
+            public boolean tagAll = false;
+            public boolean valueAll = false;
+            public boolean valueBool = false;
+            private Pattern getPattern(String str) throws IllegalStateException, PatternSyntaxException
+            {
+                if(str.endsWith("/i"))
+                    return Pattern.compile(str.substring(1,str.length()-2), Pattern.CASE_INSENSITIVE);
+                else if(str.endsWith("/"))
+                    return Pattern.compile(str.substring(1,str.length()-1));
+                throw new IllegalStateException();
+            }
+            public CheckerElement(String exp) throws IllegalStateException, PatternSyntaxException
+            {
+                Matcher m = Pattern.compile("(.+)([!=]=)(.+)").matcher(exp);
+                m.matches();
+
+                String n = m.group(1).trim();
+                if(n.equals("*"))
+                    tagAll = true;
+                else
+                    tag = n.startsWith("/") ? getPattern(n) : n;
+                    noMatch = m.group(2).equals("!=");
+                    n = m.group(3).trim();
+                    if(n.equals("*"))
+                        valueAll = true;
+                    else if(n.equals("BOOLEAN_TRUE"))
+                    {
+                        valueBool = true;
+                        value = OsmUtils.trueval;
+                    }
+                    else if(n.equals("BOOLEAN_FALSE"))
+                    {
+                        valueBool = true;
+                        value = OsmUtils.falseval;
+                    }
+                    else
+                        value = n.startsWith("/") ? getPattern(n) : n;
+            }
+            public boolean match(OsmPrimitive osm, Map<String, String> keys) {
+                for(Entry<String, String> prop: keys.entrySet()) {
+                    String key = prop.getKey();
+                    String val = valueBool ? OsmUtils.getNamedOsmBoolean(prop.getValue()) : prop.getValue();
+                    if((tagAll || (tag instanceof Pattern ? ((Pattern)tag).matcher(key).matches() : key.equals(tag)))
+                            && (valueAll || (value instanceof Pattern ? ((Pattern)value).matcher(val).matches() : val.equals(value))))
+                        return !noMatch;
+                }
+                return noMatch;
+            }
+        };
+
+        public String getData(String str)
+        {
+            Matcher m = Pattern.compile(" *# *([^#]+) *$").matcher(str);
+            str = m.replaceFirst("").trim();
+            try
+            {
+                description = m.group(1);
+                if(description != null && description.length() == 0)
+                    description = null;
+            }
+            catch (IllegalStateException e)
+            {
+                description = null;
+            }
+            String[] n = str.split(" *: *", 3);
+            if(n[0].equals("way"))
+                type = OsmPrimitiveType.WAY;
+            else if(n[0].equals("node"))
+                type = OsmPrimitiveType.NODE;
+            else if(n[0].equals("relation"))
+                type = OsmPrimitiveType.RELATION;
+            else if(n[0].equals("*"))
+                type = null;
+            else
+                return tr("Could not find element type");
+            if (n.length != 3)
+                return tr("Incorrect number of parameters");
+
+            if(n[1].equals("W"))
+            {
+                severity = Severity.WARNING;
+                code = TAG_CHECK_WARN;
+            }
+            else if(n[1].equals("E"))
+            {
+                severity = Severity.ERROR;
+                code = TAG_CHECK_ERROR;
+            }
+            else if(n[1].equals("I"))
+            {
+                severity = Severity.OTHER;
+                code = TAG_CHECK_INFO;
+            }
+            else
+                return tr("Could not find warning level");
+            for(String exp: n[2].split(" *&& *"))
+            {
+                try
+                {
+                    data.add(new CheckerElement(exp));
+                }
+                catch(IllegalStateException e)
+                {
+                    return tr("Illegal expression ''{0}''", exp);
+                }
+                catch(PatternSyntaxException e)
+                {
+                    return tr("Illegal regular expression ''{0}''", exp);
+                }
+            }
+            return null;
+        }
+        public boolean match(OsmPrimitive osm, Map<String, String> keys)
+        {
+            if (type != null && OsmPrimitiveType.from(osm) != type)
+                return false;
+
+            for(CheckerElement ce : data) {
+                if(!ce.match(osm, keys))
+                    return false;
+            }
+            return true;
+        }
+        public String getDescription()
+        {
+            return tr(description);
+        }
+        public String getDescriptionOrig()
+        {
+            return description;
+        }
+        public Severity getSeverity()
+        {
+            return severity;
+        }
+
+        public int getCode() {
+            if (type == null) {
+                return code;
+            } else {
+                return code + type.ordinal() + 1;
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/TurnrestrictionTest.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/TurnrestrictionTest.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/TurnrestrictionTest.java	(revision 3669)
@@ -0,0 +1,185 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+
+public class TurnrestrictionTest extends Test {
+
+    protected static final int NO_VIA = 1801;
+    protected static final int NO_FROM = 1802;
+    protected static final int NO_TO = 1803;
+    protected static final int MORE_VIA = 1804;
+    protected static final int MORE_FROM = 1805;
+    protected static final int MORE_TO = 1806;
+    protected static final int UNKNOWN_ROLE = 1807;
+    protected static final int UNKNOWN_TYPE = 1808;
+    protected static final int FROM_VIA_NODE = 1809;
+    protected static final int TO_VIA_NODE = 1810;
+    protected static final int FROM_VIA_WAY = 1811;
+    protected static final int TO_VIA_WAY = 1812;
+
+    public TurnrestrictionTest() {
+        super(tr("Turnrestriction"),
+                tr("This test checks if turnrestrictions are valid"));
+    }
+
+    @Override
+    public void visit(Relation r) {
+        if (!"restriction".equals(r.get("type")))
+            return;
+
+        Way fromWay = null;
+        Way toWay = null;
+        OsmPrimitive via = null;
+
+        boolean morefrom = false;
+        boolean moreto = false;
+        boolean morevia = false;
+
+        /* find the "from", "via" and "to" elements */
+        for (RelationMember m : r.getMembers())
+        {
+            if(m.getMember().isIncomplete())
+                return;
+            else
+            {
+                ArrayList<OsmPrimitive> l = new ArrayList<OsmPrimitive>();
+                l.add(r);
+                l.add(m.getMember());
+                if(m.isWay())
+                {
+                    Way w = m.getWay();
+                    if(w.getNodesCount() < 2) {
+                        continue;
+                    }
+
+                    if("from".equals(m.getRole())) {
+                        if(fromWay != null) {
+                            morefrom = true;
+                        } else {
+                            fromWay = w;
+                        }
+                    } else if("to".equals(m.getRole())) {
+                        if(toWay != null) {
+                            moreto = true;
+                        } else {
+                            toWay = w;
+                        }
+                    } else if("via".equals(m.getRole())) {
+                        if(via != null) {
+                            morevia = true;
+                        } else {
+                            via = w;
+                        }
+                    } else {
+                        errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE,
+                        l, Collections.singletonList(m)));
+                    }
+                }
+                else if(m.isNode())
+                {
+                    Node n = m.getNode();
+                    if("via".equals(m.getRole()))
+                    {
+                        if(via != null) {
+                            morevia = true;
+                        } else {
+                            via = n;
+                        }
+                    } else {
+                        errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE,
+                        l, Collections.singletonList(m)));
+                    }
+                } else {
+                    errors.add(new TestError(this, Severity.WARNING, tr("Unknown member type"), UNKNOWN_TYPE,
+                    l, Collections.singletonList(m)));
+                }
+            }
+        }
+        if(morefrom)
+            errors.add(new TestError(this, Severity.ERROR, tr("More than one \"from\" way found"), MORE_FROM, r));
+        if(moreto)
+            errors.add(new TestError(this, Severity.ERROR, tr("More than one \"to\" way found"), MORE_TO, r));
+        if(morevia)
+            errors.add(new TestError(this, Severity.ERROR, tr("More than one \"via\" way found"), MORE_VIA, r));
+
+        if (fromWay == null) {
+            errors.add(new TestError(this, Severity.ERROR, tr("No \"from\" way found"), NO_FROM, r));
+            return;
+        }
+        if (toWay == null) {
+            errors.add(new TestError(this, Severity.ERROR, tr("No \"to\" way found"), NO_TO, r));
+            return;
+        }
+        if (via == null) {
+            errors.add(new TestError(this, Severity.ERROR, tr("No \"via\" node or way found"), NO_VIA, r));
+            return;
+        }
+
+        Node viaNode;
+        if(via instanceof Node)
+        {
+            viaNode = (Node) via;
+            if(!fromWay.isFirstLastNode(viaNode)) {
+                errors.add(new TestError(this, Severity.ERROR,
+                tr("The \"from\" way does not start or end at a \"via\" node"), FROM_VIA_NODE, r));
+                return;
+            }
+            if(!toWay.isFirstLastNode(viaNode)) {
+                errors.add(new TestError(this, Severity.ERROR,
+                tr("The \"to\" way does not start or end at a \"via\" node"), TO_VIA_NODE, r));
+            }
+        }
+        else
+        {
+            Way viaWay = (Way) via;
+            Node firstNode = viaWay.firstNode();
+            Node lastNode = viaWay.lastNode();
+            Boolean onewayvia = false;
+
+            String onewayviastr = viaWay.get("oneway");
+            if(onewayviastr != null)
+            {
+                if("-1".equals(onewayviastr)) {
+                    onewayvia = true;
+                    Node tmp = firstNode;
+                    firstNode = lastNode;
+                    lastNode = tmp;
+                } else {
+                    onewayvia = OsmUtils.getOsmBoolean(onewayviastr);
+                    if (onewayvia == null) {
+                        onewayvia = false;
+                    }
+                }
+            }
+
+            if(fromWay.isFirstLastNode(firstNode))
+                viaNode = firstNode;
+            else if(!onewayvia && fromWay.isFirstLastNode(lastNode))
+                viaNode = lastNode;
+            else
+            {
+                errors.add(new TestError(this, Severity.ERROR,
+                tr("The \"from\" way does not start or end at a \"via\" way."), FROM_VIA_WAY, r));
+                return;
+            }
+            if(!toWay.isFirstLastNode(viaNode == firstNode ? lastNode : firstNode)) {
+                errors.add(new TestError(this, Severity.ERROR,
+                tr("The \"to\" way does not start or end at a \"via\" way."), TO_VIA_WAY, r));
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/UnclosedWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/UnclosedWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/UnclosedWays.java	(revision 3669)
@@ -0,0 +1,128 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Check area type ways for errors
+ *
+ * @author stoecker
+ */
+public class UnclosedWays extends Test {
+    /** The already detected errors */
+    Bag<Way, Way> _errorWays;
+
+    /**
+     * Constructor
+     */
+    public UnclosedWays() {
+        super(tr("Unclosed Ways."), tr("This tests if ways which should be circular are closed."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor) {
+        super.startTest(monitor);
+        _errorWays = new Bag<Way, Way>();
+    }
+
+    @Override
+    public void endTest() {
+        _errorWays = null;
+        super.endTest();
+    }
+
+    private String type;
+    private String etype;
+    private int mode;
+
+    public void set(int m, String text, String desc) {
+        etype = MessageFormat.format(text, desc);
+        type = tr(text, tr(desc));
+        mode = m;
+    }
+
+    public void set(int m, String text) {
+        etype = text;
+        type = tr(text);
+        mode = m;
+    }
+
+    @Override
+    public void visit(Way w) {
+        String test;
+        type = etype = null;
+        mode = 0;
+
+        if (!w.isUsable())
+            return;
+
+        test = w.get("natural");
+        if (test != null && !"coastline".equals(test) && !"cliff".equals(test))
+            set(1101, marktr("natural type {0}"), test);
+        test = w.get("landuse");
+        if (test != null)
+            set(1102, marktr("landuse type {0}"), test);
+        test = w.get("amenities");
+        if (test != null)
+            set(1103, marktr("amenities type {0}"), test);
+        test = w.get("sport");
+        if (test != null && !test.equals("water_slide"))
+            set(1104, marktr("sport type {0}"), test);
+        test = w.get("tourism");
+        if (test != null)
+            set(1105, marktr("tourism type {0}"), test);
+        test = w.get("shop");
+        if (test != null)
+            set(1106, marktr("shop type {0}"), test);
+        test = w.get("leisure");
+        if (test != null)
+            set(1107, marktr("leisure type {0}"), test);
+        test = w.get("waterway");
+        if (test != null && test.equals("riverbank"))
+            set(1108, marktr("waterway type {0}"), test);
+        Boolean btest = OsmUtils.getOsmBoolean(w.get("building"));
+        if (btest != null && btest)
+            set(1120, marktr("building"));
+        btest = OsmUtils.getOsmBoolean(w.get("area"));
+        if (btest != null && btest)
+            set(1130, marktr("area"));
+
+        if (type != null && !w.isClosed()) {
+            for (OsmPrimitive parent: w.getReferrers()) {
+                if (parent instanceof Relation && "multipolygon".equals(parent.get("type")))
+                    return;
+            }
+            Node f = w.firstNode();
+            Node l = w.lastNode();
+
+            List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>();
+            List<OsmPrimitive> highlight = new ArrayList<OsmPrimitive>();
+            primitives.add(w);
+
+            // The important parts of an unclosed way are the first and
+            // the last node which should be connected, therefore we highlight them
+            highlight.add(f);
+            highlight.add(l);
+
+            errors.add(new TestError(this, Severity.WARNING, tr("Unclosed way"),
+                            type, etype, mode, primitives, highlight));
+            _errorWays.add(w, w);
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/UnconnectedWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/UnconnectedWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/UnconnectedWays.java	(revision 3669)
@@ -0,0 +1,388 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.geom.Area;
+import java.awt.geom.Line2D;
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.BBox;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.QuadBuckets;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Tests if there are segments that crosses in the same layer
+ *
+ * @author frsantos
+ */
+public class UnconnectedWays extends Test
+{
+    protected static int UNCONNECTED_WAYS = 1301;
+    protected static final String PREFIX = ValidatorPreference.PREFIX + "." + UnconnectedWays.class.getSimpleName();
+
+    Set<MyWaySegment> ways;
+    Set<Node> endnodes; // nodes at end of way
+    Set<Node> endnodes_highway; // nodes at end of way
+    Set<Node> middlenodes; // nodes in middle of way
+    Set<Node> othernodes; // nodes appearing at least twice
+    //NodeSearchCache nodecache;
+    QuadBuckets<Node> nodecache;
+    Area ds_area;
+    DataSet ds;
+
+    double mindist;
+    double minmiddledist;
+    /**
+     * Constructor
+     */
+    public UnconnectedWays()
+    {
+        super(tr("Unconnected ways."),
+              tr("This test checks if a way has an endpoint very near to another way."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        ways = new HashSet<MyWaySegment>();
+        endnodes = new HashSet<Node>();
+        endnodes_highway = new HashSet<Node>();
+        middlenodes = new HashSet<Node>();
+        othernodes = new HashSet<Node>();
+        mindist = Main.pref.getDouble(PREFIX + ".node_way_distance", 10.0)/6378135.0;
+        minmiddledist = Main.pref.getDouble(PREFIX + ".way_way_distance", 0.0)/6378135.0;
+        this.ds = Main.main.getCurrentDataSet();
+        this.ds_area = ds.getDataSourceArea();
+    }
+
+    @Override
+    public void endTest()
+    {
+        //Area a = Main.ds.getDataSourceArea();
+        Map<Node, Way> map = new HashMap<Node, Way>();
+        long last = -1;
+        for (int iter = 0; iter < 1; iter++) {
+        last = System.currentTimeMillis();
+        long last_print = -1;
+        int nr = 0;
+        Collection<MyWaySegment> tmp_ways = ways;
+        for(MyWaySegment s : tmp_ways) {
+            nr++;
+            long now = System.currentTimeMillis();
+            if (now - last_print > 200) {
+                //System.err.println("processing segment nr: " + nr + " of " + ways.size());
+                last_print = now;
+            }
+            for(Node en : s.nearbyNodes(mindist)) {
+                if (en == null)
+                    continue;
+                if(!s.highway)
+                    continue;
+                if (!endnodes_highway.contains(en))
+                    continue;
+                if("turning_circle".equals(en.get("highway")) 
+                    || "bus_stop".equals(en.get("highway")) 
+                    || OsmUtils.isTrue(en.get("noexit"))
+                    || en.hasKey("barrier"))
+                    continue;
+                // There's a small false-positive here.  Imagine an intersection
+                // like a 't'.  If the top part of the 't' is short enough, it
+                // will trigger the node at the very top of the 't' to be unconnected
+                // to the way that "crosses" the 't'.  We should probably check that
+                // the ways to which 'en' belongs are not connected to 's.w'.
+                map.put(en, s.w);
+            }
+        }
+        //System.out.println("p1 elapsed: " + (System.currentTimeMillis()-last));
+        last = System.currentTimeMillis();
+        }
+        for(Map.Entry<Node, Way> error : map.entrySet())
+        {
+            errors.add(new TestError(this, Severity.WARNING,
+            tr("Way end node near other highway"), UNCONNECTED_WAYS,
+            Arrays.asList(error.getKey(), error.getValue())));
+        }
+        map.clear();
+        for(MyWaySegment s : ways)
+        {
+            for(Node en : s.nearbyNodes(mindist))
+            {
+                if (endnodes_highway.contains(en) && !s.highway && !s.isArea()) {
+                    map.put(en, s.w);
+                } else if (endnodes.contains(en) && !s.isArea()) {
+                    map.put(en, s.w);
+                }
+            }
+        }
+        //System.out.println("p2 elapsed: " + (System.currentTimeMillis()-last));
+        last = System.currentTimeMillis();
+        for(Map.Entry<Node, Way> error : map.entrySet())
+        {
+            errors.add(new TestError(this, Severity.WARNING,
+            tr("Way end node near other way"), UNCONNECTED_WAYS,
+            Arrays.asList(error.getKey(), error.getValue())));
+        }
+        /* the following two use a shorter distance */
+        if(minmiddledist > 0.0)
+        {
+            map.clear();
+            for(MyWaySegment s : ways)
+            {
+                for(Node en : s.nearbyNodes(minmiddledist))
+                {
+                    if (!middlenodes.contains(en))
+                        continue;
+                    map.put(en, s.w);
+                }
+            }
+            //System.out.println("p3 elapsed: " + (System.currentTimeMillis()-last));
+            last = System.currentTimeMillis();
+            for(Map.Entry<Node, Way> error : map.entrySet())
+            {
+                errors.add(new TestError(this, Severity.OTHER,
+                tr("Way node near other way"), UNCONNECTED_WAYS,
+                Arrays.asList(error.getKey(), error.getValue())));
+            }
+            map.clear();
+            for(MyWaySegment s : ways)
+            {
+                for(Node en : s.nearbyNodes(minmiddledist))
+                {
+                    if (!othernodes.contains(en))
+                        continue;
+                    map.put(en, s.w);
+                }
+            }
+            //System.out.println("p4 elapsed: " + (System.currentTimeMillis()-last));
+            last = System.currentTimeMillis();
+            for(Map.Entry<Node, Way> error : map.entrySet())
+            {
+                errors.add(new TestError(this, Severity.OTHER,
+                tr("Connected way end node near other way"), UNCONNECTED_WAYS,
+                Arrays.asList(error.getKey(), error.getValue())));
+            }
+        }
+        ways = null;
+        endnodes = null;
+        super.endTest();
+        //System.out.println("p99 elapsed: " + (System.currentTimeMillis()-last));
+        last = System.currentTimeMillis();
+    }
+
+    private class MyWaySegment
+    {
+        private final Line2D line;
+        public final Way w;
+        public final boolean isAbandoned;
+        public final boolean isBoundary;
+        public final boolean highway;
+        private final double len;
+        private Set<Node> nearbyNodeCache;
+        double nearbyNodeCacheDist = -1.0;
+        final Node n1;
+        final Node n2;
+
+        public MyWaySegment(Way w, Node n1, Node n2)
+        {
+            this.w = w;
+            String railway = w.get("railway");
+            String highway = w.get("highway");
+            this.isAbandoned = "abandoned".equals(railway) || OsmUtils.isTrue(w.get("disused"));
+            this.highway = (highway != null || railway != null) && !isAbandoned;
+            this.isBoundary = !this.highway && "administrative".equals(w.get("boundary"));
+            line = new Line2D.Double(n1.getEastNorth().east(), n1.getEastNorth().north(),
+                                     n2.getEastNorth().east(), n2.getEastNorth().north());
+            len = line.getP1().distance(line.getP2());
+            this.n1 = n1;
+            this.n2 = n2;
+        }
+
+        public boolean nearby(Node n, double dist)
+        {
+//            return !w.containsNode(n)
+//            && line.ptSegDist(n.getEastNorth().east(), n.getEastNorth().north()) < dist;
+            if (w == null) {
+                Main.debug("way null");
+                return false;
+            }
+            if (w.containsNode(n))
+                return false;
+            EastNorth coord = n.getEastNorth();
+            if (coord == null)
+                return false;
+            Point2D p = new Point2D.Double(coord.east(), coord.north());
+            if (line.getP1().distance(p) > len+dist)
+                return false;
+            if (line.getP2().distance(p) > len+dist)
+                return false;
+            return line.ptSegDist(p) < dist;
+        }
+        public List<LatLon> getBounds(double fudge)
+        {
+            double x1 = n1.getCoor().lon();
+            double x2 = n2.getCoor().lon();
+            if (x1 > x2) {
+                double tmpx = x1;
+                x1 = x2;
+                x2 = tmpx;
+            }
+            double y1 = n1.getCoor().lat();
+            double y2 = n2.getCoor().lat();
+            if (y1 > y2) {
+                double tmpy = y1;
+                y1 = y2;
+                y2 = tmpy;
+            }
+            LatLon topLeft  = new LatLon(y2+fudge, x1-fudge);
+            LatLon botRight = new LatLon(y1-fudge, x2+fudge);
+            List<LatLon> ret = new ArrayList<LatLon>();
+            ret.add(topLeft);
+            ret.add(botRight);
+            return ret;
+        }
+
+        public Collection<Node> nearbyNodes(double dist)
+        {
+            // If you're looking for nodes that are farther
+            // away that we looked for last time, the cached
+            // result is no good
+            if (dist > nearbyNodeCacheDist) {
+                //if (nearbyNodeCacheDist != -1)
+                //    System.out.println("destroyed MyWaySegment nearby node cache:" + dist + " > " +  nearbyNodeCacheDist);
+                nearbyNodeCache = null;
+            }
+            if (nearbyNodeCache != null) {
+                // If we've cached an aread greater than the
+                // one now being asked for...
+                if (nearbyNodeCacheDist > dist) {
+                    //System.out.println("had to trim MyWaySegment nearby node cache.");
+                    // Used the cached result and trim out
+                    // the nodes that are not in the smaller
+                    // area, but keep the old larger cache.
+                    Set<Node> trimmed = new HashSet<Node>(nearbyNodeCache);
+                    for (Node n : new HashSet<Node>(nearbyNodeCache)) {
+                        if (!nearby(n, dist))
+                            trimmed.remove(n);
+                    }
+                    return trimmed;
+                }
+                return nearbyNodeCache;
+            }
+            /*
+             * We know that any point near the line must be at
+             * least as close as the other end of the line, plus
+             * a little fudge for the distance away ('dist').
+             */
+
+            // This needs to be a hash set because the searches
+            // overlap a bit and can return duplicate nodes.
+            nearbyNodeCache = null;
+            List<LatLon> bounds = this.getBounds(dist);
+            List<Node> found_nodes = ds.searchNodes(new BBox(bounds.get(0), bounds.get(1)));
+            if (found_nodes == null)
+                return Collections.emptySet();
+
+            for (Node n : found_nodes) {
+                if (!nearby(n, dist) ||
+                     (ds_area != null && !ds_area.contains(n.getCoor())))
+                    continue;
+                // It is actually very rare for us to find a node
+                // so defer as much of the work as possible, like
+                // allocating the hash set
+                if (nearbyNodeCache == null)
+                    nearbyNodeCache = new HashSet<Node>();
+                nearbyNodeCache.add(n);
+            }
+            nearbyNodeCacheDist = dist;
+            if (nearbyNodeCache == null)
+                nearbyNodeCache = Collections.emptySet();
+            return nearbyNodeCache;
+        }
+
+        public boolean isArea() {
+            return w.hasKey("landuse")
+                || w.hasKey("leisure")
+                || w.hasKey("amenity")
+                || w.hasKey("building");
+        }
+    }
+
+    List<MyWaySegment> getWaySegments(Way w)
+    {
+        List<MyWaySegment> ret = new ArrayList<MyWaySegment>();
+        if (!w.isUsable()
+            || w.hasKey("barrier")
+            || "cliff".equals(w.get("natural")))
+            return ret;
+
+        int size = w.getNodesCount();
+        if(size < 2)
+            return ret;
+        for(int i = 1; i < size; ++i)
+        {
+            if(i < size-1)
+                addNode(w.getNode(i), middlenodes);
+            MyWaySegment ws = new MyWaySegment(w, w.getNode(i-1), w.getNode(i));
+            if (ws.isBoundary || ws.isAbandoned)
+                continue;
+            ret.add(ws);
+        }
+        return ret;
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        ways.addAll(getWaySegments(w));
+        Set<Node> set = endnodes;
+        if(w.hasKey("highway") || w.hasKey("railway"))
+            set = endnodes_highway;
+        addNode(w.firstNode(), set);
+        addNode(w.lastNode(), set);
+    }
+    @Override
+    public void visit(Node n)
+    {
+    }
+    private void addNode(Node n, Set<Node> s)
+    {
+        boolean m = middlenodes.contains(n);
+        boolean e = endnodes.contains(n);
+        boolean eh = endnodes_highway.contains(n);
+        boolean o = othernodes.contains(n);
+        if(!m && !e && !o && !eh)
+            s.add(n);
+        else if(!o)
+        {
+            othernodes.add(n);
+            if(e)
+                endnodes.remove(n);
+            else if(eh)
+                endnodes_highway.remove(n);
+            else
+                middlenodes.remove(n);
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedNode.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedNode.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedNode.java	(revision 3669)
@@ -0,0 +1,135 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Checks for nodes with uninteresting tags that are in no way
+ *
+ * @author frsantos
+ */
+public class UntaggedNode extends Test
+{
+    protected static final int UNTAGGED_NODE_BLANK = 201;
+    protected static final int UNTAGGED_NODE_FIXME = 202;
+    protected static final int UNTAGGED_NODE_NOTE = 203;
+    protected static final int UNTAGGED_NODE_CREATED_BY = 204;
+    protected static final int UNTAGGED_NODE_WATCH = 205;
+    protected static final int UNTAGGED_NODE_SOURCE = 206;
+    protected static final int UNTAGGED_NODE_OTHER = 207;
+
+    /**
+     * Constructor
+     */
+    public UntaggedNode()
+    {
+        super(tr("Untagged and unconnected nodes")+".",
+                tr("This test checks for untagged nodes that are not part of any way."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+    }
+
+    @Override
+    public void visit(Collection<OsmPrimitive> selection)
+    {
+        for (OsmPrimitive p : selection) {
+            if (p.isUsable() && p instanceof Node) {
+                p.visit(this);
+            }
+        }
+    }
+
+    @Override
+    public void visit(Node n)
+    {
+        if(n.isUsable() && !n.isTagged() && n.getReferrers().isEmpty()) {
+            if (!n.hasKeys()) {
+                String msg = marktr("No tags");
+                errors.add(new TestError(this, Severity.OTHER, tr("Unconnected nodes without physical tags"), tr(msg), msg, UNTAGGED_NODE_BLANK, n));
+                return;
+            }
+            for (Map.Entry<String, String> tag : n.getKeys().entrySet()) {
+                String key = tag.getKey();
+                String value = tag.getValue();
+                if (contains(tag, "fixme") || contains(tag, "FIXME")) {
+                    /* translation note: don't translate quoted words */
+                    String msg = marktr("Has tag containing ''fixme'' or ''FIXME''");
+                    errors.add(new TestError(this, Severity.OTHER, tr("Unconnected nodes without physical tags"),
+                                tr(msg), msg, UNTAGGED_NODE_FIXME, n));
+                    return;
+                }
+
+                String msg = null;
+                int code = 0;
+                if (key.startsWith("note") || key.startsWith("comment") || key.startsWith("description")) {
+                    /* translation note: don't translate quoted words */
+                    msg = marktr("Has key ''note'' or ''comment'' or ''description''");
+                    code = UNTAGGED_NODE_NOTE;
+                } else if (key.startsWith("created_by") || key.startsWith("converted_by")) {
+                    /* translation note: don't translate quoted words */
+                    msg = marktr("Has key ''created_by'' or ''converted_by''");
+                    code = UNTAGGED_NODE_CREATED_BY;
+                } else if (key.startsWith("watch")) {
+                    /* translation note: don't translate quoted words */
+                    msg = marktr("Has key ''watch''");
+                    code = UNTAGGED_NODE_WATCH;
+                } else if (key.startsWith("source")) {
+                    /* translation note: don't translate quoted words */
+                    msg = marktr("Has key ''source''");
+                    code = UNTAGGED_NODE_SOURCE;
+                }
+                if (msg != null) {
+                    errors.add(new TestError(this, Severity.OTHER, tr("Unconnected nodes without physical tags"),
+                                tr(msg), msg, code, n));
+                    return;
+                }
+            }
+            // Does not happen, but just to be sure. Maybe definition of uninteresting tags changes in future.
+            errors.add(new TestError(this, Severity.OTHER, tr("Unconnected nodes without physical tags"),
+                        tr("Other"), "Other", UNTAGGED_NODE_OTHER, n));
+        }
+    }
+
+    private boolean contains(Map.Entry<String, String> tag, String s) {
+        return tag.getKey().indexOf(s) != -1 || tag.getValue().indexOf(s) != -1;
+    }
+
+    @Override
+    public Command fixError(TestError testError)
+    {
+        return DeleteCommand.delete(Main.map.mapView.getEditLayer(), testError.getPrimitives());
+    }
+
+    @Override
+    public boolean isFixable(TestError testError) {
+        if (testError.getTester() instanceof UntaggedNode) {
+            int code = testError.getCode();
+            switch (code) {
+                case UNTAGGED_NODE_BLANK:
+                case UNTAGGED_NODE_CREATED_BY:
+                case UNTAGGED_NODE_WATCH:
+                case UNTAGGED_NODE_SOURCE:
+                    return true;
+            }
+        }
+        return false;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedWay.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedWay.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/UntaggedWay.java	(revision 3669)
@@ -0,0 +1,163 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Checks for untagged ways
+ *
+ * @author frsantos
+ */
+public class UntaggedWay extends Test
+{
+    /** Empty way error */
+    protected static final int EMPTY_WAY    = 301;
+    /** Untagged way error */
+    protected static final int UNTAGGED_WAY = 302;
+    /** Unnamed way error */
+    protected static final int UNNAMED_WAY  = 303;
+    /** One node way error */
+    protected static final int ONE_NODE_WAY = 304;
+    /** Unnamed junction error */
+    protected static final int UNNAMED_JUNCTION  = 305;
+    /** Untagged, but commented way error */
+    protected static final int COMMENTED_WAY = 306;
+
+    private LinkedList<Way> multipolygonways;
+
+    /** Ways that must have a name */
+    public static final Set<String> NAMED_WAYS = new HashSet<String>();
+    static
+    {
+        NAMED_WAYS.add( "motorway" );
+        NAMED_WAYS.add( "trunk" );
+        NAMED_WAYS.add( "primary" );
+        NAMED_WAYS.add( "secondary" );
+        NAMED_WAYS.add( "tertiary" );
+        NAMED_WAYS.add( "residential" );
+        NAMED_WAYS.add( "pedestrian" ); ;
+    }
+
+    /**
+     * Constructor
+     */
+    public UntaggedWay()
+    {
+        super(tr("Untagged, empty and one node ways."),
+              tr("This test checks for untagged, empty and one node ways."));
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        if (!w.isUsable()) return;
+
+        Map<String, String> tags = w.getKeys();
+        if( tags.size() != 0 )
+        {
+            String highway = tags.get("highway");
+            if(highway != null && NAMED_WAYS.contains(highway))
+            {
+                if( !tags.containsKey("name") && !tags.containsKey("ref") )
+                {
+                    boolean isRoundabout = false;
+                    boolean hasName = false;
+                    for( String key : w.keySet())
+                    {
+                        hasName = key.startsWith("name:") || key.endsWith("_name") || key.endsWith("_ref");
+                        if( hasName )
+                            break;
+                        if(key.equals("junction"))
+                        {
+                            isRoundabout = w.get("junction").equals("roundabout");
+                            break;
+                        }
+                    }
+
+                    if( !hasName && !isRoundabout)
+                        errors.add( new TestError(this, Severity.WARNING, tr("Unnamed ways"), UNNAMED_WAY, w) );
+                    else if(isRoundabout)
+                        errors.add( new TestError(this, Severity.WARNING, tr("Unnamed junction"), UNNAMED_JUNCTION, w) );
+                }
+            }
+        }
+
+        if(!w.isTagged() && !multipolygonways.contains(w))
+        {
+            if(w.hasKeys())
+                errors.add( new TestError(this, Severity.WARNING, tr("Untagged ways (commented)"), COMMENTED_WAY, w) );
+            else
+                errors.add( new TestError(this, Severity.WARNING, tr("Untagged ways"), UNTAGGED_WAY, w) );
+        }
+
+        if( w.getNodesCount() == 0 )
+        {
+            errors.add( new TestError(this, Severity.ERROR, tr("Empty ways"), EMPTY_WAY, w) );
+        }
+        else if( w.getNodesCount() == 1 )
+        {
+            errors.add( new TestError(this, Severity.ERROR, tr("One node ways"), ONE_NODE_WAY, w) );
+        }
+
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        multipolygonways = new LinkedList<Way>();
+        for (Relation r : Main.main.getCurrentDataSet().getRelations())
+        {
+            if(r.isUsable() && "multipolygon".equals(r.get("type")))
+            {
+                for (RelationMember m : r.getMembers())
+                {
+                    if(m.getMember() != null && m.getMember() instanceof Way &&
+                    m.getMember().isUsable() && !m.getMember().isTagged())
+                        multipolygonways.add((Way)m.getMember());
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endTest()
+    {
+        multipolygonways = null;
+        super.endTest();
+    }
+
+    @Override
+    public boolean isFixable(TestError testError)
+    {
+        if( testError.getTester() instanceof UntaggedWay )
+        {
+            return testError.getCode() == EMPTY_WAY
+                || testError.getCode() == ONE_NODE_WAY;
+        }
+
+        return false;
+    }
+
+    @Override
+    public Command fixError(TestError testError)
+    {
+        return DeleteCommand.delete(Main.map.mapView.getEditLayer(), testError.getPrimitives());
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/WronglyOrderedWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/WronglyOrderedWays.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/WronglyOrderedWays.java	(revision 3669)
@@ -0,0 +1,116 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+
+/**
+ * Check cyclic ways for errors
+ *
+ * @author jrreid
+ */
+public class WronglyOrderedWays extends Test  {
+    protected static int WRONGLY_ORDERED_COAST = 1001;
+    protected static int WRONGLY_ORDERED_WATER = 1002;
+    protected static int WRONGLY_ORDERED_LAND  = 1003;
+
+    /** The already detected errors */
+    Bag<Way, Way> _errorWays;
+
+    /**
+     * Constructor
+     */
+    public WronglyOrderedWays()
+    {
+        super(tr("Wrongly Ordered Ways."),
+              tr("This test checks the direction of water, land and coastline ways."));
+    }
+
+    @Override
+    public void startTest(ProgressMonitor monitor)
+    {
+        super.startTest(monitor);
+        _errorWays = new Bag<Way, Way>();
+    }
+
+    @Override
+    public void endTest()
+    {
+        _errorWays = null;
+        super.endTest();
+    }
+
+    @Override
+    public void visit(Way w)
+    {
+        String errortype = "";
+        int type;
+
+        if( !w.isUsable() )
+            return;
+        if (w.getNodesCount() <= 0)
+            return;
+
+        String natural = w.get("natural");
+        if( natural == null)
+            return;
+
+        if( natural.equals("coastline") )
+        {
+            errortype = tr("Reversed coastline: land not on left side");
+            type= WRONGLY_ORDERED_COAST;
+        }
+        else if(natural.equals("water") )
+        {
+            errortype = tr("Reversed water: land not on left side");
+            type= WRONGLY_ORDERED_WATER;
+        }
+        else if( natural.equals("land") )
+        {
+            errortype = tr("Reversed land: land not on left side");
+            type= WRONGLY_ORDERED_LAND;
+        }
+        else
+            return;
+
+
+        /**
+         * Test the directionality of the way
+         *
+         * Assuming a closed non-looping way, compute twice the area
+         * of the polygon using the formula 2*a = sum (Xn * Yn+1 - Xn+1 * Yn)
+         * If the area is negative the way is ordered in a clockwise direction
+         *
+         */
+
+        if(w.getNode(0) == w.getNode(w.getNodesCount()-1))
+        {
+            double area2 = 0;
+
+            for (int node = 1; node < w.getNodesCount(); node++)
+            {
+                area2 += (w.getNode(node-1).getCoor().lon() * w.getNode(node).getCoor().lat()
+                - w.getNode(node).getCoor().lon() * w.getNode(node-1).getCoor().lat());
+            }
+
+            if(((natural.equals("coastline") || natural.equals("land")) && area2 < 0.)
+            || (natural.equals("water") && area2 > 0.))
+            {
+                List<OsmPrimitive> primitives = new ArrayList<OsmPrimitive>();
+                primitives.add(w);
+                errors.add( new TestError(this, Severity.OTHER, errortype, type, primitives) );
+                _errorWays.add(w,w);
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/AgregatePrimitivesVisitor.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/AgregatePrimitivesVisitor.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/AgregatePrimitivesVisitor.java	(revision 3669)
@@ -0,0 +1,65 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.util;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+
+/**
+ * A visitor that aggregates all primitives it visits.
+ * <p>
+ * The primitives are sorted according to their type: first nodes, then ways.
+ *
+ * @author frsantos
+ */
+public class AgregatePrimitivesVisitor extends AbstractVisitor
+{
+    /** Aggregated data */
+    final Collection<OsmPrimitive> aggregatedData = new HashSet<OsmPrimitive>();
+
+    /**
+     * Visits a collection of primitives
+     * @param data The collection of primitives
+     * @return The aggregated primitives
+     */
+    public Collection<OsmPrimitive> visit(Collection<OsmPrimitive> data)
+    {
+        for (OsmPrimitive osm : data)
+        {
+            osm.visit(this);
+        }
+
+        return aggregatedData;
+    }
+
+    public void visit(Node n)
+    {
+        if(!aggregatedData.contains(n))
+            aggregatedData.add(n);
+    }
+
+    public void visit(Way w)
+    {
+        if(!aggregatedData.contains(w))
+        {
+            aggregatedData.add(w);
+            for (Node n : w.getNodes())
+                visit(n);
+        }
+    }
+
+    public void visit(Relation r) {
+        if (!aggregatedData.contains(r)) {
+            aggregatedData.add(r);
+            for (RelationMember m : r.getMembers()) {
+                m.getMember().visit(this);
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/Bag.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/Bag.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/Bag.java	(revision 3669)
@@ -0,0 +1,94 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ *
+ * A very simple bag to store multiple occurences of a same key.
+ * <p>
+ * The bag will keep, for each key, a list of values.
+ *
+ * @author frsantos
+ *
+ * @param <K> The key class
+ * @param <V> The value class
+ */
+public class Bag<K,V> extends HashMap<K, List<V>>
+{
+    /** Serializable ID */
+    private static final long serialVersionUID = 5374049172859211610L;
+
+    /**
+     * Returns the list of elements with the same key
+     * @param key The key to obtain the elements
+     * @return the list of elements with the same key
+     */
+    public List<V> get(K key)
+    {
+        return super.get(key);
+    }
+
+    /**
+     * Adds an element to the bag
+     * @param key The key of the element
+     * @param value The element to add
+     */
+    public void add(K key, V value)
+    {
+        List<V> values = get(key);
+        if( values == null )
+        {
+            values = new ArrayList<V>();
+            put(key, values);
+        }
+        values.add(value);
+    }
+
+    /**
+     * Adds an element to the bag
+     * @param key The key of the element
+     * @param value The element to add
+     */
+    public void add(K key)
+    {
+        List<V> values = get(key);
+        if( values == null )
+        {
+            values = new ArrayList<V>();
+            put(key, values);
+        }
+    }
+
+    /**
+     * Constructor
+     */
+    public Bag()
+    {
+        super();
+    }
+
+    /**
+     * Constructor
+     *
+     * @param initialCapacity The initial capacity
+     */
+    public Bag(int initialCapacity)
+    {
+        super(initialCapacity);
+    }
+
+    /**
+     * Returns true if the bag contains a value for a key
+     * @param key The key
+     * @param value The value
+     * @return true if the key contains the value
+     */
+    public boolean contains(K key, V value)
+    {
+        List<V> values = get(key);
+        return (values == null) ? false : values.contains(value);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/Entities.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/Entities.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/Entities.java	(revision 3669)
@@ -0,0 +1,410 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* Taken from: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/java/org/apache/commons/lang/Entities.java?revision=636641 */
+package org.openstreetmap.josm.data.validation.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * <p>
+ * Provides HTML and XML entity utilities.
+ * </p>
+ * @see <a href="http://hotwired.lycos.com/webmonkey/reference/special_characters/">ISO Entities</a>
+ * @see <a href="http://www.w3.org/TR/REC-html32#latin1">HTML 3.2 Character Entities for ISO Latin-1</a>
+ * @see <a href="http://www.w3.org/TR/REC-html40/sgml/entities.html">HTML 4.0 Character entity references</a>
+ * @see <a href="http://www.w3.org/TR/html401/charset.html#h-5.3">HTML 4.01 Character References</a>
+ * @see <a href="http://www.w3.org/TR/html401/charset.html#code-position">HTML 4.01 Code positions</a>
+ */
+public class Entities {
+    private static final String[][] ARRAY = {
+        /* BASIC */
+        {"quot", "34"}, // " - double-quote
+        {"amp", "38"}, // & - ampersand
+        {"lt", "60"}, // < - less-than
+        {"gt", "62"}, // > - greater-than
+        /* XML */
+        {"apos", "39"}, // XML apostrophe
+        /* ISO8859_1 */
+        {"nbsp", "160"}, // non-breaking space
+        {"iexcl", "161"}, // inverted exclamation mark
+        {"cent", "162"}, // cent sign
+        {"pound", "163"}, // pound sign
+        {"curren", "164"}, // currency sign
+        {"yen", "165"}, // yen sign = yuan sign
+        {"brvbar", "166"}, // broken bar = broken vertical bar
+        {"sect", "167"}, // section sign
+        {"uml", "168"}, // diaeresis = spacing diaeresis
+        {"copy", "169"}, // © - copyright sign
+        {"ordf", "170"}, // feminine ordinal indicator
+        {"laquo", "171"}, // left-pointing double angle quotation mark = left pointing guillemet
+        {"not", "172"}, // not sign
+        {"shy", "173"}, // soft hyphen = discretionary hyphen
+        {"reg", "174"}, // ® - registered trademark sign
+        {"macr", "175"}, // macron = spacing macron = overline = APL overbar
+        {"deg", "176"}, // degree sign
+        {"plusmn", "177"}, // plus-minus sign = plus-or-minus sign
+        {"sup2", "178"}, // superscript two = superscript digit two = squared
+        {"sup3", "179"}, // superscript three = superscript digit three = cubed
+        {"acute", "180"}, // acute accent = spacing acute
+        {"micro", "181"}, // micro sign
+        {"para", "182"}, // pilcrow sign = paragraph sign
+        {"middot", "183"}, // middle dot = Georgian comma = Greek middle dot
+        {"cedil", "184"}, // cedilla = spacing cedilla
+        {"sup1", "185"}, // superscript one = superscript digit one
+        {"ordm", "186"}, // masculine ordinal indicator
+        {"raquo", "187"}, // right-pointing double angle quotation mark = right pointing guillemet
+        {"frac14", "188"}, // vulgar fraction one quarter = fraction one quarter
+        {"frac12", "189"}, // vulgar fraction one half = fraction one half
+        {"frac34", "190"}, // vulgar fraction three quarters = fraction three quarters
+        {"iquest", "191"}, // inverted question mark = turned question mark
+        {"Agrave", "192"}, // À - uppercase A, grave accent
+        {"Aacute", "193"}, // Á - uppercase A, acute accent
+        {"Acirc", "194"}, // Â - uppercase A, circumflex accent
+        {"Atilde", "195"}, // Ã - uppercase A, tilde
+        {"Auml", "196"}, // Ä - uppercase A, umlaut
+        {"Aring", "197"}, // Å - uppercase A, ring
+        {"AElig", "198"}, // Æ - uppercase AE
+        {"Ccedil", "199"}, // Ç - uppercase C, cedilla
+        {"Egrave", "200"}, // È - uppercase E, grave accent
+        {"Eacute", "201"}, // É - uppercase E, acute accent
+        {"Ecirc", "202"}, // Ê - uppercase E, circumflex accent
+        {"Euml", "203"}, // Ë - uppercase E, umlaut
+        {"Igrave", "204"}, // Ì - uppercase I, grave accent
+        {"Iacute", "205"}, // Í - uppercase I, acute accent
+        {"Icirc", "206"}, // Î - uppercase I, circumflex accent
+        {"Iuml", "207"}, // Ï - uppercase I, umlaut
+        {"ETH", "208"}, // Ð - uppercase Eth, Icelandic
+        {"Ntilde", "209"}, // Ñ - uppercase N, tilde
+        {"Ograve", "210"}, // Ò - uppercase O, grave accent
+        {"Oacute", "211"}, // Ó - uppercase O, acute accent
+        {"Ocirc", "212"}, // Ô - uppercase O, circumflex accent
+        {"Otilde", "213"}, // Õ - uppercase O, tilde
+        {"Ouml", "214"}, // Ö - uppercase O, umlaut
+        {"times", "215"}, // multiplication sign
+        {"Oslash", "216"}, // Ø - uppercase O, slash
+        {"Ugrave", "217"}, // Ù - uppercase U, grave accent
+        {"Uacute", "218"}, // Ú - uppercase U, acute accent
+        {"Ucirc", "219"}, // Û - uppercase U, circumflex accent
+        {"Uuml", "220"}, // Ü - uppercase U, umlaut
+        {"Yacute", "221"}, // Ý - uppercase Y, acute accent
+        {"THORN", "222"}, // Þ - uppercase THORN, Icelandic
+        {"szlig", "223"}, // ß - lowercase sharps, German
+        {"agrave", "224"}, // à - lowercase a, grave accent
+        {"aacute", "225"}, // á - lowercase a, acute accent
+        {"acirc", "226"}, // â - lowercase a, circumflex accent
+        {"atilde", "227"}, // ã - lowercase a, tilde
+        {"auml", "228"}, // ä - lowercase a, umlaut
+        {"aring", "229"}, // å - lowercase a, ring
+        {"aelig", "230"}, // æ - lowercase ae
+        {"ccedil", "231"}, // ç - lowercase c, cedilla
+        {"egrave", "232"}, // è - lowercase e, grave accent
+        {"eacute", "233"}, // é - lowercase e, acute accent
+        {"ecirc", "234"}, // ê - lowercase e, circumflex accent
+        {"euml", "235"}, // ë - lowercase e, umlaut
+        {"igrave", "236"}, // ì - lowercase i, grave accent
+        {"iacute", "237"}, // í - lowercase i, acute accent
+        {"icirc", "238"}, // î - lowercase i, circumflex accent
+        {"iuml", "239"}, // ï - lowercase i, umlaut
+        {"eth", "240"}, // ð - lowercase eth, Icelandic
+        {"ntilde", "241"}, // ñ - lowercase n, tilde
+        {"ograve", "242"}, // ò - lowercase o, grave accent
+        {"oacute", "243"}, // ó - lowercase o, acute accent
+        {"ocirc", "244"}, // ô - lowercase o, circumflex accent
+        {"otilde", "245"}, // õ - lowercase o, tilde
+        {"ouml", "246"}, // ö - lowercase o, umlaut
+        {"divide", "247"}, // division sign
+        {"oslash", "248"}, // ø - lowercase o, slash
+        {"ugrave", "249"}, // ù - lowercase u, grave accent
+        {"uacute", "250"}, // ú - lowercase u, acute accent
+        {"ucirc", "251"}, // û - lowercase u, circumflex accent
+        {"uuml", "252"}, // ü - lowercase u, umlaut
+        {"yacute", "253"}, // ý - lowercase y, acute accent
+        {"thorn", "254"}, // þ - lowercase thorn, Icelandic
+        {"yuml", "255"}, // ÿ - lowercase y, umlaut
+        /* HTML 40 */
+        // <!-- Latin Extended-B -->
+        {"fnof", "402"}, // latin small f with hook = function= florin, U+0192 ISOtech -->
+        // <!-- Greek -->
+        {"Alpha", "913"}, // greek capital letter alpha, U+0391 -->
+        {"Beta", "914"}, // greek capital letter beta, U+0392 -->
+        {"Gamma", "915"}, // greek capital letter gamma,U+0393 ISOgrk3 -->
+        {"Delta", "916"}, // greek capital letter delta,U+0394 ISOgrk3 -->
+        {"Epsilon", "917"}, // greek capital letter epsilon, U+0395 -->
+        {"Zeta", "918"}, // greek capital letter zeta, U+0396 -->
+        {"Eta", "919"}, // greek capital letter eta, U+0397 -->
+        {"Theta", "920"}, // greek capital letter theta,U+0398 ISOgrk3 -->
+        {"Iota", "921"}, // greek capital letter iota, U+0399 -->
+        {"Kappa", "922"}, // greek capital letter kappa, U+039A -->
+        {"Lambda", "923"}, // greek capital letter lambda,U+039B ISOgrk3 -->
+        {"Mu", "924"}, // greek capital letter mu, U+039C -->
+        {"Nu", "925"}, // greek capital letter nu, U+039D -->
+        {"Xi", "926"}, // greek capital letter xi, U+039E ISOgrk3 -->
+        {"Omicron", "927"}, // greek capital letter omicron, U+039F -->
+        {"Pi", "928"}, // greek capital letter pi, U+03A0 ISOgrk3 -->
+        {"Rho", "929"}, // greek capital letter rho, U+03A1 -->
+        // <!-- there is no Sigmaf, and no U+03A2 character either -->
+        {"Sigma", "931"}, // greek capital letter sigma,U+03A3 ISOgrk3 -->
+        {"Tau", "932"}, // greek capital letter tau, U+03A4 -->
+        {"Upsilon", "933"}, // greek capital letter upsilon,U+03A5 ISOgrk3 -->
+        {"Phi", "934"}, // greek capital letter phi,U+03A6 ISOgrk3 -->
+        {"Chi", "935"}, // greek capital letter chi, U+03A7 -->
+        {"Psi", "936"}, // greek capital letter psi,U+03A8 ISOgrk3 -->
+        {"Omega", "937"}, // greek capital letter omega,U+03A9 ISOgrk3 -->
+        {"alpha", "945"}, // greek small letter alpha,U+03B1 ISOgrk3 -->
+        {"beta", "946"}, // greek small letter beta, U+03B2 ISOgrk3 -->
+        {"gamma", "947"}, // greek small letter gamma,U+03B3 ISOgrk3 -->
+        {"delta", "948"}, // greek small letter delta,U+03B4 ISOgrk3 -->
+        {"epsilon", "949"}, // greek small letter epsilon,U+03B5 ISOgrk3 -->
+        {"zeta", "950"}, // greek small letter zeta, U+03B6 ISOgrk3 -->
+        {"eta", "951"}, // greek small letter eta, U+03B7 ISOgrk3 -->
+        {"theta", "952"}, // greek small letter theta,U+03B8 ISOgrk3 -->
+        {"iota", "953"}, // greek small letter iota, U+03B9 ISOgrk3 -->
+        {"kappa", "954"}, // greek small letter kappa,U+03BA ISOgrk3 -->
+        {"lambda", "955"}, // greek small letter lambda,U+03BB ISOgrk3 -->
+        {"mu", "956"}, // greek small letter mu, U+03BC ISOgrk3 -->
+        {"nu", "957"}, // greek small letter nu, U+03BD ISOgrk3 -->
+        {"xi", "958"}, // greek small letter xi, U+03BE ISOgrk3 -->
+        {"omicron", "959"}, // greek small letter omicron, U+03BF NEW -->
+        {"pi", "960"}, // greek small letter pi, U+03C0 ISOgrk3 -->
+        {"rho", "961"}, // greek small letter rho, U+03C1 ISOgrk3 -->
+        {"sigmaf", "962"}, // greek small letter final sigma,U+03C2 ISOgrk3 -->
+        {"sigma", "963"}, // greek small letter sigma,U+03C3 ISOgrk3 -->
+        {"tau", "964"}, // greek small letter tau, U+03C4 ISOgrk3 -->
+        {"upsilon", "965"}, // greek small letter upsilon,U+03C5 ISOgrk3 -->
+        {"phi", "966"}, // greek small letter phi, U+03C6 ISOgrk3 -->
+        {"chi", "967"}, // greek small letter chi, U+03C7 ISOgrk3 -->
+        {"psi", "968"}, // greek small letter psi, U+03C8 ISOgrk3 -->
+        {"omega", "969"}, // greek small letter omega,U+03C9 ISOgrk3 -->
+        {"thetasym", "977"}, // greek small letter theta symbol,U+03D1 NEW -->
+        {"upsih", "978"}, // greek upsilon with hook symbol,U+03D2 NEW -->
+        {"piv", "982"}, // greek pi symbol, U+03D6 ISOgrk3 -->
+        // <!-- General Punctuation -->
+        {"bull", "8226"}, // bullet = black small circle,U+2022 ISOpub -->
+        // <!-- bullet is NOT the same as bullet operator, U+2219 -->
+        {"hellip", "8230"}, // horizontal ellipsis = three dot leader,U+2026 ISOpub -->
+        {"prime", "8242"}, // prime = minutes = feet, U+2032 ISOtech -->
+        {"Prime", "8243"}, // double prime = seconds = inches,U+2033 ISOtech -->
+        {"oline", "8254"}, // overline = spacing overscore,U+203E NEW -->
+        {"frasl", "8260"}, // fraction slash, U+2044 NEW -->
+        // <!-- Letterlike Symbols -->
+        {"weierp", "8472"}, // script capital P = power set= Weierstrass p, U+2118 ISOamso -->
+        {"image", "8465"}, // blackletter capital I = imaginary part,U+2111 ISOamso -->
+        {"real", "8476"}, // blackletter capital R = real part symbol,U+211C ISOamso -->
+        {"trade", "8482"}, // trade mark sign, U+2122 ISOnum -->
+        {"alefsym", "8501"}, // alef symbol = first transfinite cardinal,U+2135 NEW -->
+        // <!-- alef symbol is NOT the same as hebrew letter alef,U+05D0 although the
+        // same glyph could be used to depict both characters -->
+        // <!-- Arrows -->
+        {"larr", "8592"}, // leftwards arrow, U+2190 ISOnum -->
+        {"uarr", "8593"}, // upwards arrow, U+2191 ISOnum-->
+        {"rarr", "8594"}, // rightwards arrow, U+2192 ISOnum -->
+        {"darr", "8595"}, // downwards arrow, U+2193 ISOnum -->
+        {"harr", "8596"}, // left right arrow, U+2194 ISOamsa -->
+        {"crarr", "8629"}, // downwards arrow with corner leftwards= carriage return, U+21B5 NEW -->
+        {"lArr", "8656"}, // leftwards double arrow, U+21D0 ISOtech -->
+        // <!-- ISO 10646 does not say that lArr is the same as the 'is implied by'
+        // arrow but also does not have any other character for that function.
+        // So ? lArr canbe used for 'is implied by' as ISOtech suggests -->
+        {"uArr", "8657"}, // upwards double arrow, U+21D1 ISOamsa -->
+        {"rArr", "8658"}, // rightwards double arrow,U+21D2 ISOtech -->
+        // <!-- ISO 10646 does not say this is the 'implies' character but does not
+        // have another character with this function so ?rArr can be used for
+        // 'implies' as ISOtech suggests -->
+        {"dArr", "8659"}, // downwards double arrow, U+21D3 ISOamsa -->
+        {"hArr", "8660"}, // left right double arrow,U+21D4 ISOamsa -->
+        // <!-- Mathematical Operators -->
+        {"forall", "8704"}, // for all, U+2200 ISOtech -->
+        {"part", "8706"}, // partial differential, U+2202 ISOtech -->
+        {"exist", "8707"}, // there exists, U+2203 ISOtech -->
+        {"empty", "8709"}, // empty set = null set = diameter,U+2205 ISOamso -->
+        {"nabla", "8711"}, // nabla = backward difference,U+2207 ISOtech -->
+        {"isin", "8712"}, // element of, U+2208 ISOtech -->
+        {"notin", "8713"}, // not an element of, U+2209 ISOtech -->
+        {"ni", "8715"}, // contains as member, U+220B ISOtech -->
+        // <!-- should there be a more memorable name than 'ni'? -->
+        {"prod", "8719"}, // n-ary product = product sign,U+220F ISOamsb -->
+        // <!-- prod is NOT the same character as U+03A0 'greek capital letter pi'
+        // though the same glyph might be used for both -->
+        {"sum", "8721"}, // n-ary summation, U+2211 ISOamsb -->
+        // <!-- sum is NOT the same character as U+03A3 'greek capital letter sigma'
+        // though the same glyph might be used for both -->
+        {"minus", "8722"}, // minus sign, U+2212 ISOtech -->
+        {"lowast", "8727"}, // asterisk operator, U+2217 ISOtech -->
+        {"radic", "8730"}, // square root = radical sign,U+221A ISOtech -->
+        {"prop", "8733"}, // proportional to, U+221D ISOtech -->
+        {"infin", "8734"}, // infinity, U+221E ISOtech -->
+        {"ang", "8736"}, // angle, U+2220 ISOamso -->
+        {"and", "8743"}, // logical and = wedge, U+2227 ISOtech -->
+        {"or", "8744"}, // logical or = vee, U+2228 ISOtech -->
+        {"cap", "8745"}, // intersection = cap, U+2229 ISOtech -->
+        {"cup", "8746"}, // union = cup, U+222A ISOtech -->
+        {"int", "8747"}, // integral, U+222B ISOtech -->
+        {"there4", "8756"}, // therefore, U+2234 ISOtech -->
+        {"sim", "8764"}, // tilde operator = varies with = similar to,U+223C ISOtech -->
+        // <!-- tilde operator is NOT the same character as the tilde, U+007E,although
+        // the same glyph might be used to represent both -->
+        {"cong", "8773"}, // approximately equal to, U+2245 ISOtech -->
+        {"asymp", "8776"}, // almost equal to = asymptotic to,U+2248 ISOamsr -->
+        {"ne", "8800"}, // not equal to, U+2260 ISOtech -->
+        {"equiv", "8801"}, // identical to, U+2261 ISOtech -->
+        {"le", "8804"}, // less-than or equal to, U+2264 ISOtech -->
+        {"ge", "8805"}, // greater-than or equal to,U+2265 ISOtech -->
+        {"sub", "8834"}, // subset of, U+2282 ISOtech -->
+        {"sup", "8835"}, // superset of, U+2283 ISOtech -->
+        // <!-- note that nsup, 'not a superset of, U+2283' is not covered by the
+        // Symbol font encoding and is not included. Should it be, for symmetry?
+        // It is in ISOamsn --> <!ENTITY nsub", "8836"},
+        // not a subset of, U+2284 ISOamsn -->
+        {"sube", "8838"}, // subset of or equal to, U+2286 ISOtech -->
+        {"supe", "8839"}, // superset of or equal to,U+2287 ISOtech -->
+        {"oplus", "8853"}, // circled plus = direct sum,U+2295 ISOamsb -->
+        {"otimes", "8855"}, // circled times = vector product,U+2297 ISOamsb -->
+        {"perp", "8869"}, // up tack = orthogonal to = perpendicular,U+22A5 ISOtech -->
+        {"sdot", "8901"}, // dot operator, U+22C5 ISOamsb -->
+        // <!-- dot operator is NOT the same character as U+00B7 middle dot -->
+        // <!-- Miscellaneous Technical -->
+        {"lceil", "8968"}, // left ceiling = apl upstile,U+2308 ISOamsc -->
+        {"rceil", "8969"}, // right ceiling, U+2309 ISOamsc -->
+        {"lfloor", "8970"}, // left floor = apl downstile,U+230A ISOamsc -->
+        {"rfloor", "8971"}, // right floor, U+230B ISOamsc -->
+        {"lang", "9001"}, // left-pointing angle bracket = bra,U+2329 ISOtech -->
+        // <!-- lang is NOT the same character as U+003C 'less than' or U+2039 'single left-pointing angle quotation
+        // mark' -->
+        {"rang", "9002"}, // right-pointing angle bracket = ket,U+232A ISOtech -->
+        // <!-- rang is NOT the same character as U+003E 'greater than' or U+203A
+        // 'single right-pointing angle quotation mark' -->
+        // <!-- Geometric Shapes -->
+        {"loz", "9674"}, // lozenge, U+25CA ISOpub -->
+        // <!-- Miscellaneous Symbols -->
+        {"spades", "9824"}, // black spade suit, U+2660 ISOpub -->
+        // <!-- black here seems to mean filled as opposed to hollow -->
+        {"clubs", "9827"}, // black club suit = shamrock,U+2663 ISOpub -->
+        {"hearts", "9829"}, // black heart suit = valentine,U+2665 ISOpub -->
+        {"diams", "9830"}, // black diamond suit, U+2666 ISOpub -->
+
+        // <!-- Latin Extended-A -->
+        {"OElig", "338"}, // -- latin capital ligature OE,U+0152 ISOlat2 -->
+        {"oelig", "339"}, // -- latin small ligature oe, U+0153 ISOlat2 -->
+        // <!-- ligature is a misnomer, this is a separate character in some languages -->
+        {"Scaron", "352"}, // -- latin capital letter S with caron,U+0160 ISOlat2 -->
+        {"scaron", "353"}, // -- latin small letter s with caron,U+0161 ISOlat2 -->
+        {"Yuml", "376"}, // -- latin capital letter Y with diaeresis,U+0178 ISOlat2 -->
+        // <!-- Spacing Modifier Letters -->
+        {"circ", "710"}, // -- modifier letter circumflex accent,U+02C6 ISOpub -->
+        {"tilde", "732"}, // small tilde, U+02DC ISOdia -->
+        // <!-- General Punctuation -->
+        {"ensp", "8194"}, // en space, U+2002 ISOpub -->
+        {"emsp", "8195"}, // em space, U+2003 ISOpub -->
+        {"thinsp", "8201"}, // thin space, U+2009 ISOpub -->
+        {"zwnj", "8204"}, // zero width non-joiner,U+200C NEW RFC 2070 -->
+        {"zwj", "8205"}, // zero width joiner, U+200D NEW RFC 2070 -->
+        {"lrm", "8206"}, // left-to-right mark, U+200E NEW RFC 2070 -->
+        {"rlm", "8207"}, // right-to-left mark, U+200F NEW RFC 2070 -->
+        {"ndash", "8211"}, // en dash, U+2013 ISOpub -->
+        {"mdash", "8212"}, // em dash, U+2014 ISOpub -->
+        {"lsquo", "8216"}, // left single quotation mark,U+2018 ISOnum -->
+        {"rsquo", "8217"}, // right single quotation mark,U+2019 ISOnum -->
+        {"sbquo", "8218"}, // single low-9 quotation mark, U+201A NEW -->
+        {"ldquo", "8220"}, // left double quotation mark,U+201C ISOnum -->
+        {"rdquo", "8221"}, // right double quotation mark,U+201D ISOnum -->
+        {"bdquo", "8222"}, // double low-9 quotation mark, U+201E NEW -->
+        {"dagger", "8224"}, // dagger, U+2020 ISOpub -->
+        {"Dagger", "8225"}, // double dagger, U+2021 ISOpub -->
+        {"permil", "8240"}, // per mille sign, U+2030 ISOtech -->
+        {"lsaquo", "8249"}, // single left-pointing angle quotation mark,U+2039 ISO proposed -->
+        // <!-- lsaquo is proposed but not yet ISO standardized -->
+        {"rsaquo", "8250"}, // single right-pointing angle quotation mark,U+203A ISO proposed -->
+        // <!-- rsaquo is proposed but not yet ISO standardized -->
+        {"euro", "8364"}, // -- euro sign, U+20AC NEW -->
+    };
+
+    private static Map<String, String> mapNameToValue = null;
+
+    public String unescape(String str) {
+        int firstAmp = str.indexOf('&');
+        if (firstAmp < 0)
+            return str;
+        String res = str.substring(0, firstAmp);
+        int len = str.length();
+        for (int i = firstAmp; i < len; i++) {
+            char c = str.charAt(i);
+            if (c == '&') {
+                int nextIdx = i + 1;
+                int semiColonIdx = str.indexOf(';', nextIdx);
+                if (semiColonIdx == -1) {
+                    res += c;
+                    continue;
+                }
+                int amphersandIdx = str.indexOf('&', i + 1);
+                if (amphersandIdx != -1 && amphersandIdx < semiColonIdx) {
+                    // Then the text looks like &...&...;
+                    res += c;
+                    continue;
+                }
+                String entityContent = str.substring(nextIdx, semiColonIdx);
+                int entityValue = -1;
+                int entityContentLen = entityContent.length();
+                if (entityContentLen > 0) {
+                    if (entityContent.charAt(0) == '#') { // escaped value content is an integer (decimal or
+                        // hexidecimal)
+                        if (entityContentLen > 1) {
+                            char isHexChar = entityContent.charAt(1);
+                            try {
+                                switch (isHexChar) {
+                                    case 'X' :
+                                    case 'x' : {
+                                        entityValue = Integer.parseInt(entityContent.substring(2), 16);
+                                        break;
+                                    }
+                                    default : {
+                                        entityValue = Integer.parseInt(entityContent.substring(1), 10);
+                                    }
+                                }
+                                if (entityValue > 0xFFFF) {
+                                    entityValue = -1;
+                                }
+                            } catch (NumberFormatException e) {
+                                entityValue = -1;
+                            }
+                        }
+                    } else { // escaped value content is an entity name
+                        if(mapNameToValue == null)
+                        {
+                            mapNameToValue = new HashMap<String, String>();
+                            for (int in = 0; in < ARRAY.length; ++in)
+                                mapNameToValue.put(ARRAY[in][0], ARRAY[in][1]);
+                        }
+                        String value = mapNameToValue.get(entityContent);
+                        entityValue = (value == null ? -1 : Integer.parseInt(value));
+                    }
+                }
+
+                if (entityValue == -1) {
+                    res += '&' + entityContent + ';';
+                } else {
+                    res += (char) entityValue;
+                }
+                i = semiColonIdx; // move index up to the semi-colon
+            } else {
+                res += c;
+            }
+        }
+        return res;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/MultipleNameVisitor.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/MultipleNameVisitor.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/MultipleNameVisitor.java	(revision 3669)
@@ -0,0 +1,106 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.util;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import java.util.Collection;
+
+import javax.swing.Icon;
+import javax.swing.JLabel;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Able to create a name and an icon for a collection of elements.
+ *
+ * @author frsantos
+ */
+public class MultipleNameVisitor extends NameVisitor
+{
+    /** The class name of the combined primitives */
+    String multipleClassname;
+    /* name to be displayed */
+    String displayName;
+    /** Size of the collection */
+    int size;
+
+    /**
+     * Visits a collection of primitives
+     * @param data The collection of primitives
+     */
+    public void visit(Collection<? extends OsmPrimitive> data)
+    {
+        String multipleName = null;
+        String multiplePluralClassname = null;
+        String firstName = null;
+        boolean initializedname = false;
+        size = data.size();
+
+        multipleClassname = null;
+        for (OsmPrimitive osm : data)
+        {
+            String name = osm.get("name");
+            if(name == null) name = osm.get("ref");
+            if(!initializedname)
+            {
+                multipleName = name; initializedname = true;
+            }
+            else if(multipleName != null && (name == null  || !name.equals(multipleName)))
+            {
+                multipleName = null;
+            }
+
+            if(firstName == null && name != null)
+                firstName = name;
+            osm.visit(this);
+            if (multipleClassname == null)
+            {
+                multipleClassname = className;
+                multiplePluralClassname = classNamePlural;
+            }
+            else if (!multipleClassname.equals(className))
+            {
+                multipleClassname = "object";
+                multiplePluralClassname = trn("object", "objects", 2);
+            }
+        }
+
+        if( size == 1 )
+            displayName = name;
+        else if(multipleName != null)
+            displayName = size + " " + trn(multipleClassname, multiplePluralClassname, size) + ": " + multipleName;
+        else if(firstName != null)
+            displayName = size + " " + trn(multipleClassname, multiplePluralClassname, size) + ": " + tr("{0}, ...", firstName);
+        else
+            displayName = size + " " + trn(multipleClassname, multiplePluralClassname, size);
+    }
+
+    @Override
+    public JLabel toLabel()
+    {
+        return new JLabel(getText(), getIcon(), JLabel.HORIZONTAL);
+    }
+
+    /**
+     * Gets the name of the items
+     * @return the name of the items
+     */
+    public String getText()
+    {
+        return displayName;
+    }
+
+    /**
+     * Gets the icon of the items
+     * @return the icon of the items
+     */
+    public Icon getIcon()
+    {
+        if( size == 1 )
+            return icon;
+        else
+            return ImageProvider.get("data", multipleClassname);
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/NameVisitor.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/NameVisitor.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/NameVisitor.java	(revision 3669)
@@ -0,0 +1,84 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.util;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
+
+import javax.swing.Icon;
+import javax.swing.JLabel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
+import org.openstreetmap.josm.gui.DefaultNameFormatter;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Able to create a name and an icon for each data element.
+ *
+ * @author imi
+ */
+//TODO This class used to be in JOSM but it was removed. MultipleNameVisitor depends on it so I copied it here, but MultipleNameVisitor should be refactored instead of using this class
+public class NameVisitor extends AbstractVisitor {
+
+    /**
+     * The name of the item class
+     */
+    public String className;
+    public String classNamePlural;
+    /**
+     * The name of this item.
+     */
+    public String name;
+    /**
+     * The icon of this item.
+     */
+    public Icon icon;
+
+    /**
+     * If the node has a name-key or id-key, this is displayed. If not, (lat,lon)
+     * is displayed.
+     */
+    public void visit(Node n) {
+        name = n.getDisplayName(DefaultNameFormatter.getInstance());
+        addId(n);
+        icon = ImageProvider.get("data", "node");
+        className = "node";
+        classNamePlural = trn("node", "nodes", 2);
+    }
+
+    /**
+     * If the way has a name-key or id-key, this is displayed. If not, (x nodes)
+     * is displayed with x being the number of nodes in the way.
+     */
+    public void visit(Way w) {
+        name = w.getDisplayName(DefaultNameFormatter.getInstance());
+        addId(w);
+        icon = ImageProvider.get("data", "way");
+        className = "way";
+        classNamePlural = trn("way", "ways", 2);
+    }
+
+    /**
+     */
+    public void visit(Relation e) {
+        name = e.getDisplayName(DefaultNameFormatter.getInstance());
+        addId(e);
+        icon = ImageProvider.get("data", "relation");
+        className = "relation";
+        classNamePlural = trn("relation", "relations", 2);
+    }
+
+    public JLabel toLabel() {
+        return new JLabel(name, icon, JLabel.HORIZONTAL);
+    }
+
+
+    private void addId(OsmPrimitive osm) {
+        if (Main.pref.getBoolean("osm-primitives.showid"))
+            name += tr(" [id: {0}]", osm.getId());
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java	(revision 3669)
@@ -0,0 +1,176 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.data.validation.util;
+
+import java.awt.geom.Point2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+
+/**
+ * Utility class
+ *
+ * @author frsantos
+ */
+public class ValUtil
+{
+    /**
+     * Returns the plugin's directory of the plugin
+     *
+     * @return The directory of the plugin
+     */
+    public static String getPluginDir()
+    {
+        return Main.pref.getPreferencesDir() + "plugins/validator/";
+    }
+
+    /**
+     * Returns the start and end cells of a way.
+     * @param w The way
+     * @param cellWays The map with all cells
+     * @return A list with all the cells the way starts or ends
+     */
+    public static List<List<Way>> getWaysInCell(Way w, Map<Point2D,List<Way>> cellWays)
+    {
+        if (w.getNodesCount() == 0)
+            return Collections.emptyList();
+
+        Node n1 = w.getNode(0);
+        Node n2 = w.getNode(w.getNodesCount() - 1);
+
+        List<List<Way>> cells = new ArrayList<List<Way>>(2);
+        Set<Point2D> cellNodes = new HashSet<Point2D>();
+        Point2D cell;
+
+        // First, round coordinates
+        long x0 = Math.round(n1.getEastNorth().east()  * OsmValidator.griddetail);
+        long y0 = Math.round(n1.getEastNorth().north() * OsmValidator.griddetail);
+        long x1 = Math.round(n2.getEastNorth().east()  * OsmValidator.griddetail);
+        long y1 = Math.round(n2.getEastNorth().north() * OsmValidator.griddetail);
+
+        // Start of the way
+        cell = new Point2D.Double(x0, y0);
+        cellNodes.add(cell);
+        List<Way> ways = cellWays.get( cell );
+        if( ways == null )
+        {
+            ways = new ArrayList<Way>();
+            cellWays.put(cell, ways);
+        }
+        cells.add(ways);
+
+        // End of the way
+        cell = new Point2D.Double(x1, y1);
+        if( !cellNodes.contains(cell) )
+        {
+            cellNodes.add(cell);
+            ways = cellWays.get( cell );
+            if( ways == null )
+            {
+                ways = new ArrayList<Way>();
+                cellWays.put(cell, ways);
+            }
+            cells.add(ways);
+        }
+
+        // Then floor coordinates, in case the way is in the border of the cell.
+        x0 = (long)Math.floor(n1.getEastNorth().east()  * OsmValidator.griddetail);
+        y0 = (long)Math.floor(n1.getEastNorth().north() * OsmValidator.griddetail);
+        x1 = (long)Math.floor(n2.getEastNorth().east()  * OsmValidator.griddetail);
+        y1 = (long)Math.floor(n2.getEastNorth().north() * OsmValidator.griddetail);
+
+        // Start of the way
+        cell = new Point2D.Double(x0, y0);
+        if( !cellNodes.contains(cell) )
+        {
+            cellNodes.add(cell);
+            ways = cellWays.get( cell );
+            if( ways == null )
+            {
+                ways = new ArrayList<Way>();
+                cellWays.put(cell, ways);
+            }
+            cells.add(ways);
+        }
+
+        // End of the way
+        cell = new Point2D.Double(x1, y1);
+        if( !cellNodes.contains(cell) )
+        {
+            cellNodes.add(cell);
+            ways = cellWays.get( cell );
+            if( ways == null )
+            {
+                ways = new ArrayList<Way>();
+                cellWays.put(cell, ways);
+            }
+            cells.add(ways);
+        }
+
+        return cells;
+    }
+
+    /**
+     * Returns the coordinates of all cells in a grid that a line between 2
+     * nodes intersects with.
+     *
+     * @param n1 The first node.
+     * @param n2 The second node.
+     * @param gridDetail The detail of the grid. Bigger values give smaller
+     * cells, but a bigger number of them.
+     * @return A list with the coordinates of all cells
+     */
+    public static List<Point2D> getSegmentCells(Node n1, Node n2, double gridDetail)
+    {
+        List<Point2D> cells = new ArrayList<Point2D>();
+        double x0 = n1.getEastNorth().east() * gridDetail;
+        double x1 = n2.getEastNorth().east() * gridDetail;
+        double y0 = n1.getEastNorth().north() * gridDetail + 1;
+        double y1 = n2.getEastNorth().north() * gridDetail + 1;
+
+        if( x0 > x1 )
+        {
+            // Move to 1st-4th cuadrants
+            double aux;
+            aux = x0; x0 = x1; x1 = aux;
+            aux = y0; y0 = y1; y1 = aux;
+        }
+
+        double dx  = x1 - x0;
+        double dy  = y1 - y0;
+        long stepY = y0 <= y1 ? 1 : -1;
+        long gridX0 = (long)Math.floor(x0);
+        long gridX1 = (long)Math.floor(x1);
+        long gridY0 = (long)Math.floor(y0);
+        long gridY1 = (long)Math.floor(y1);
+
+        long maxSteps = (gridX1 - gridX0) + Math.abs(gridY1 - gridY0) + 1;
+        while( (gridX0 <= gridX1 && (gridY0 - gridY1)*stepY <= 0) && maxSteps-- > 0)
+        {
+            cells.add( new Point2D.Double(gridX0, gridY0) );
+
+            // Is the cross between the segment and next vertical line nearer than the cross with next horizontal line?
+            // Note: segment line formula: y=dy/dx(x-x1)+y1
+            // Note: if dy < 0, must use *bottom* line. If dy > 0, must use upper line
+            double scanY = dy/dx * (gridX0 + 1 - x1) + y1 + (dy < 0 ? -1 : 0);
+            double scanX = dx/dy * (gridY0 + (dy < 0 ? 0 : 1)*stepY - y1) + x1;
+
+            double distX = Math.pow(gridX0 + 1 - x0, 2) + Math.pow(scanY - y0, 2);
+            double distY = Math.pow(scanX - x0, 2) + Math.pow(gridY0 + stepY - y0, 2);
+
+            if( distX < distY)
+                gridX0 += 1;
+            else
+                gridY0 += stepY;
+        }
+
+        return cells;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/MainMenu.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/gui/MainMenu.java	(revision 3669)
@@ -77,4 +77,5 @@
 import org.openstreetmap.josm.actions.ZoomOutAction;
 import org.openstreetmap.josm.actions.OrthogonalizeAction.Undo;
+import org.openstreetmap.josm.actions.ValidateAction;
 import org.openstreetmap.josm.actions.audio.AudioBackAction;
 import org.openstreetmap.josm.actions.audio.AudioFasterAction;
Index: /trunk/src/org/openstreetmap/josm/gui/MapFrame.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MapFrame.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/gui/MapFrame.java	(revision 3669)
@@ -55,4 +55,5 @@
 import org.openstreetmap.josm.gui.dialogs.ToggleDialog;
 import org.openstreetmap.josm.gui.dialogs.UserListDialog;
+import org.openstreetmap.josm.gui.dialogs.ValidatorDialog;
 import org.openstreetmap.josm.gui.dialogs.properties.PropertiesDialog;
 import org.openstreetmap.josm.gui.layer.Layer;
@@ -92,4 +93,5 @@
     public FilterDialog filterDialog;
     public RelationListDialog relationListDialog;
+    public ValidatorDialog validatorDialog;
     public SelectionListDialog selectionListDialog;
     /**
@@ -180,4 +182,5 @@
         addToggleDialog(new HistoryDialog());
         addToggleDialog(conflictDialog = new ConflictDialog());
+        addToggleDialog(validatorDialog = new ValidatorDialog());
         addToggleDialog(filterDialog = new FilterDialog());
         addToggleDialog(new ChangesetDialog(this));
Index: /trunk/src/org/openstreetmap/josm/gui/MapView.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/gui/MapView.java	(revision 3669)
@@ -113,8 +113,9 @@
 
     /**
-     * Adds a edit layer change listener
+     * Adds an edit layer change listener
      *
      * @param listener the listener. Ignored if null or already registered.
-     * @param initialFire Fire an edit-layer-changed-event right after adding the listener.
+     * @param initialFire Fire an edit-layer-changed-event right after adding 
+     * the listener in case there is an edit layer present
      */
     public static void addEditLayerChangeListener(EditLayerChangeListener listener, boolean initialFire) {
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java	(revision 3669)
@@ -0,0 +1,572 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.gui.dialogs;
+
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreePath;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.AutoScaleAction;
+import org.openstreetmap.josm.command.Command;
+import org.openstreetmap.josm.data.SelectionChangedListener;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.WaySegment;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.ValidatorVisitor;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.EditLayerChangeListener;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.gui.SideButton;
+import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
+import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.tools.Shortcut;
+import org.xml.sax.SAXException;
+
+/**
+ * A small tool dialog for displaying the current errors. The selection manager
+ * respects clicks into the selection list. Ctrl-click will remove entries from
+ * the list while single click will make the clicked entry the only selection.
+ *
+ * @author frsantos
+ */
+public class ValidatorDialog extends ToggleDialog implements ActionListener, SelectionChangedListener, LayerChangeListener {
+    /** Serializable ID */
+    private static final long serialVersionUID = 2952292777351992696L;
+
+    /** The display tree */
+    public ValidatorTreePanel tree;
+
+    private SideButton fixButton;
+    /** The fix button */
+    private SideButton ignoreButton;
+    /** The ignore button */
+    private SideButton selectButton;
+    /** The select button */
+
+    private JPopupMenu popupMenu;
+    private TestError popupMenuError = null;
+
+    /** Last selected element */
+    private DefaultMutableTreeNode lastSelectedNode = null;
+
+    /**
+     * Constructor
+     */
+    public ValidatorDialog() {
+        super(tr("Validation errors"), "validator", tr("Open the validation window."),
+                Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation errors")),
+                        KeyEvent.VK_V, Shortcut.GROUP_LAYER, Shortcut.SHIFT_DEFAULT), 150);
+
+        popupMenu = new JPopupMenu();
+
+        JMenuItem zoomTo = new JMenuItem(tr("Zoom to problem"));
+        zoomTo.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                zoomToProblem();
+            }
+        });
+        popupMenu.add(zoomTo);
+
+        tree = new ValidatorTreePanel();
+        tree.addMouseListener(new ClickWatch());
+        tree.addTreeSelectionListener(new SelectionWatch());
+
+        add(new JScrollPane(tree), BorderLayout.CENTER);
+
+        JPanel buttonPanel = new JPanel(new GridLayout(1, 3));
+
+        selectButton = new SideButton(marktr("Select"), "select", "Validator",
+                tr("Set the selected elements on the map to the selected items in the list above."), this);
+        selectButton.setEnabled(false);
+        buttonPanel.add(selectButton);
+        buttonPanel.add(new SideButton(Main.main.validator.validateAction), "refresh");
+        fixButton = new SideButton(marktr("Fix"), "fix", "Validator", tr("Fix the selected errors."), this);
+        fixButton.setEnabled(false);
+        buttonPanel.add(fixButton);
+        if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
+            ignoreButton = new SideButton(marktr("Ignore"), "delete", "Validator",
+                    tr("Ignore the selected errors next time."), this);
+            ignoreButton.setEnabled(false);
+            buttonPanel.add(ignoreButton);
+        } else {
+            ignoreButton = null;
+        }
+        add(buttonPanel, BorderLayout.SOUTH);
+
+    }
+
+    @Override
+    public void showNotify() {
+        DataSet.addSelectionListener(this);
+        DataSet ds = Main.main.getCurrentDataSet();
+        if (ds != null) {
+            updateSelection(ds.getSelected());
+        }
+        MapView.addLayerChangeListener(this);
+        Layer activeLayer = Main.map.mapView.getActiveLayer();
+        if (activeLayer != null) {
+            activeLayerChange(null, activeLayer);
+        }
+    }
+
+    @Override
+    public void hideNotify() {
+        MapView.removeLayerChangeListener(this);
+        DataSet.removeSelectionListener(this);
+    }
+
+    @Override
+    public void setVisible(boolean v) {
+        if (tree != null)
+            tree.setVisible(v);
+        super.setVisible(v);
+        Main.map.repaint();
+    }
+
+    /**
+     * Fix selected errors
+     *
+     * @param e
+     */
+    @SuppressWarnings("unchecked")
+    private void fixErrors(ActionEvent e) {
+        TreePath[] selectionPaths = tree.getSelectionPaths();
+        if (selectionPaths == null)
+            return;
+
+        Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
+
+        LinkedList<TestError> errorsToFix = new LinkedList<TestError>();
+        for (TreePath path : selectionPaths) {
+            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+            if (node == null)
+                continue;
+
+            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
+            while (children.hasMoreElements()) {
+                DefaultMutableTreeNode childNode = children.nextElement();
+                if (processedNodes.contains(childNode))
+                    continue;
+
+                processedNodes.add(childNode);
+                Object nodeInfo = childNode.getUserObject();
+                if (nodeInfo instanceof TestError) {
+                    errorsToFix.add((TestError)nodeInfo);
+                }
+            }
+        }
+
+        // run fix task asynchronously
+        //
+        FixTask fixTask = new FixTask(errorsToFix);
+        Main.worker.submit(fixTask);
+    }
+
+    /**
+     * Set selected errors to ignore state
+     *
+     * @param e
+     */
+    @SuppressWarnings("unchecked")
+    private void ignoreErrors(ActionEvent e) {
+        int asked = JOptionPane.DEFAULT_OPTION;
+        boolean changed = false;
+        TreePath[] selectionPaths = tree.getSelectionPaths();
+        if (selectionPaths == null)
+            return;
+
+        Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
+        for (TreePath path : selectionPaths) {
+            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+            if (node == null)
+                continue;
+
+            Object mainNodeInfo = node.getUserObject();
+            if (!(mainNodeInfo instanceof TestError)) {
+                Set<String> state = new HashSet<String>();
+                // ask if the whole set should be ignored
+                if (asked == JOptionPane.DEFAULT_OPTION) {
+                    String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
+                    asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
+                            tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
+                            a, a[1]);
+                }
+                if (asked == JOptionPane.YES_NO_OPTION) {
+                    Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
+                    while (children.hasMoreElements()) {
+                        DefaultMutableTreeNode childNode = children.nextElement();
+                        if (processedNodes.contains(childNode))
+                            continue;
+
+                        processedNodes.add(childNode);
+                        Object nodeInfo = childNode.getUserObject();
+                        if (nodeInfo instanceof TestError) {
+                            TestError err = (TestError) nodeInfo;
+                            err.setIgnored(true);
+                            changed = true;
+                            state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
+                        }
+                    }
+                    for (String s : state) {
+                        OsmValidator.addIgnoredError(s);
+                    }
+                    continue;
+                } else if (asked == JOptionPane.CANCEL_OPTION)
+                    continue;
+            }
+
+            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
+            while (children.hasMoreElements()) {
+                DefaultMutableTreeNode childNode = children.nextElement();
+                if (processedNodes.contains(childNode))
+                    continue;
+
+                processedNodes.add(childNode);
+                Object nodeInfo = childNode.getUserObject();
+                if (nodeInfo instanceof TestError) {
+                    TestError error = (TestError) nodeInfo;
+                    String state = error.getIgnoreState();
+                    if (state != null) {
+                        OsmValidator.addIgnoredError(state);
+                    }
+                    changed = true;
+                    error.setIgnored(true);
+                }
+            }
+        }
+        if (changed) {
+            tree.resetErrors();
+            OsmValidator.saveIgnoredErrors();
+            Main.map.repaint();
+        }
+    }
+
+    private void showPopupMenu(MouseEvent e) {
+        if (!e.isPopupTrigger())
+            return;
+        popupMenuError = null;
+        TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
+        if (selPath == null)
+            return;
+        DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
+        if (!(node.getUserObject() instanceof TestError))
+            return;
+        popupMenuError = (TestError) node.getUserObject();
+        popupMenu.show(e.getComponent(), e.getX(), e.getY());
+    }
+
+    private void zoomToProblem() {
+        if (popupMenuError == null)
+            return;
+        ValidatorBoundingXYVisitor bbox = new ValidatorBoundingXYVisitor();
+        popupMenuError.visitHighlighted(bbox);
+        if (bbox.getBounds() == null)
+            return;
+        bbox.enlargeBoundingBox();
+        Main.map.mapView.recalculateCenterScale(bbox);
+    }
+
+    /**
+     * Sets the selection of the map to the current selected items.
+     */
+    @SuppressWarnings("unchecked")
+    private void setSelectedItems() {
+        if (tree == null)
+            return;
+
+        Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(40);
+
+        TreePath[] selectedPaths = tree.getSelectionPaths();
+        if (selectedPaths == null)
+            return;
+
+        for (TreePath path : selectedPaths) {
+            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+            Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
+            while (children.hasMoreElements()) {
+                DefaultMutableTreeNode childNode = children.nextElement();
+                Object nodeInfo = childNode.getUserObject();
+                if (nodeInfo instanceof TestError) {
+                    TestError error = (TestError) nodeInfo;
+                    sel.addAll(error.getPrimitives());
+                }
+            }
+        }
+
+        Main.main.getCurrentDataSet().setSelected(sel);
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        String actionCommand = e.getActionCommand();
+        if (actionCommand.equals("Select"))
+            setSelectedItems();
+        else if (actionCommand.equals("Fix"))
+            fixErrors(e);
+        else if (actionCommand.equals("Ignore"))
+            ignoreErrors(e);
+    }
+
+    /**
+     * Checks for fixes in selected element and, if needed, adds to the sel
+     * parameter all selected elements
+     *
+     * @param sel
+     *            The collection where to add all selected elements
+     * @param addSelected
+     *            if true, add all selected elements to collection
+     * @return whether the selected elements has any fix
+     */
+    @SuppressWarnings("unchecked")
+    private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
+        boolean hasFixes = false;
+
+        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
+        if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
+            Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
+            while (children.hasMoreElements()) {
+                DefaultMutableTreeNode childNode = children.nextElement();
+                Object nodeInfo = childNode.getUserObject();
+                if (nodeInfo instanceof TestError) {
+                    TestError error = (TestError) nodeInfo;
+                    error.setSelected(false);
+                }
+            }
+        }
+
+        lastSelectedNode = node;
+        if (node == null)
+            return hasFixes;
+
+        Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
+        while (children.hasMoreElements()) {
+            DefaultMutableTreeNode childNode = children.nextElement();
+            Object nodeInfo = childNode.getUserObject();
+            if (nodeInfo instanceof TestError) {
+                TestError error = (TestError) nodeInfo;
+                error.setSelected(true);
+
+                hasFixes = hasFixes || error.isFixable();
+                if (addSelected) {
+                    sel.addAll(error.getPrimitives());
+                }
+            }
+        }
+        selectButton.setEnabled(true);
+        if (ignoreButton != null)
+            ignoreButton.setEnabled(true);
+
+        return hasFixes;
+    }
+
+    @Override
+    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+        if (newLayer instanceof OsmDataLayer) {
+            tree.setErrorList(((OsmDataLayer) newLayer).validationErrors);
+        }
+    }
+
+    @Override
+    public void layerAdded(Layer newLayer) {}
+
+    @Override
+    public void layerRemoved(Layer oldLayer) {}
+
+    /**
+     * Watches for clicks.
+     */
+    public class ClickWatch extends MouseAdapter {
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            fixButton.setEnabled(false);
+            if (ignoreButton != null)
+                ignoreButton.setEnabled(false);
+            selectButton.setEnabled(false);
+
+            boolean isDblClick = e.getClickCount() > 1;
+
+            Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
+
+            boolean hasFixes = setSelection(sel, isDblClick);
+            fixButton.setEnabled(hasFixes);
+
+            if (isDblClick) {
+                Main.main.getCurrentDataSet().setSelected(sel);
+                if(Main.pref.getBoolean("validator.autozoom", false))
+                    AutoScaleAction.zoomTo(sel);
+            }
+        }
+
+        @Override
+        public void mousePressed(MouseEvent e) {
+            showPopupMenu(e);
+        }
+
+        @Override
+        public void mouseReleased(MouseEvent e) {
+            showPopupMenu(e);
+        }
+
+    }
+
+    /**
+     * Watches for tree selection.
+     */
+    public class SelectionWatch implements TreeSelectionListener {
+        public void valueChanged(TreeSelectionEvent e) {
+            fixButton.setEnabled(false);
+            if (ignoreButton != null)
+                ignoreButton.setEnabled(false);
+            selectButton.setEnabled(false);
+
+            if (e.getSource() instanceof JScrollPane) {
+                System.out.println(e.getSource());
+                return;
+            }
+
+            boolean hasFixes = setSelection(null, false);
+            fixButton.setEnabled(hasFixes);
+            Main.map.repaint();
+        }
+    }
+
+    public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
+
+        public void visit(OsmPrimitive p) {
+            if (p.isUsable()) {
+                p.visit(this);
+            }
+        }
+
+        public void visit(WaySegment ws) {
+            if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
+                return;
+            visit(ws.way.getNodes().get(ws.lowerIndex));
+            visit(ws.way.getNodes().get(ws.lowerIndex + 1));
+        }
+
+        public void visit(List<Node> nodes) {
+            for (Node n: nodes) {
+                visit(n);
+            }
+        }
+    }
+
+    public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
+        if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
+            return;
+        if (newSelection.isEmpty())
+            tree.setFilter(null);
+        HashSet<OsmPrimitive> filter = new HashSet<OsmPrimitive>(newSelection);
+        tree.setFilter(filter);
+    }
+
+    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
+        updateSelection(newSelection);
+    }
+
+    /**
+     * Task for fixing a collection of {@see TestError}s. Can be run asynchronously.
+     *
+     *
+     */
+    class FixTask extends PleaseWaitRunnable {
+        private Collection<TestError> testErrors;
+        private boolean canceled;
+
+        public FixTask(Collection<TestError> testErrors) {
+            super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
+            this.testErrors = testErrors == null ? new ArrayList<TestError> (): testErrors;
+        }
+
+        @Override
+        protected void cancel() {
+            this.canceled = true;
+        }
+
+        @Override
+        protected void finish() {
+            // do nothing
+        }
+
+        @Override
+        protected void realRun() throws SAXException, IOException,
+        OsmTransferException {
+            ProgressMonitor monitor = getProgressMonitor();
+            try {
+                monitor.setTicksCount(testErrors.size());
+                int i=0;
+                for (TestError error: testErrors) {
+                    i++;
+                    monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(),error.getMessage()));
+                    if (this.canceled)
+                        return;
+                    final Command fixCommand = error.getFix();
+                    if (fixCommand != null) {
+                        SwingUtilities.invokeAndWait(
+                                new Runnable() {
+                                    public void run() {
+                                        Main.main.undoRedo.addNoRedraw(fixCommand);
+                                    }
+                                }
+                        );
+                        error.setIgnored(true);
+                    }
+                    monitor.worked(1);
+                }
+                monitor.subTask(tr("Updating map ..."));
+                SwingUtilities.invokeAndWait(new Runnable() {
+                    public void run() {
+                        Main.main.undoRedo.afterAdd();
+                        Main.map.repaint();
+                        tree.resetErrors();
+                        Main.main.getCurrentDataSet().fireSelectionChanged();
+                    }
+                });
+            } catch(InterruptedException e) {
+                // FIXME: signature of realRun should have a generic checked exception we
+                // could throw here
+                throw new RuntimeException(e);
+            } catch(InvocationTargetException e) {
+                throw new RuntimeException(e);
+            } finally {
+                monitor.finishTask();
+            }
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java	(revision 3669)
@@ -0,0 +1,338 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.gui.dialogs.validator;
+
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import javax.swing.JTree;
+import javax.swing.ToolTipManager;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
+
+/**
+ * A panel that displays the error tree. The selection manager
+ * respects clicks into the selection list. Ctrl-click will remove entries from
+ * the list while single click will make the clicked entry the only selection.
+ *
+ * @author frsantos
+ */
+
+public class ValidatorTreePanel extends JTree {
+    /** Serializable ID */
+    private static final long serialVersionUID = 2952292777351992696L;
+
+    /**
+     * The validation data.
+     */
+    protected DefaultTreeModel treeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
+
+    /** The list of errors shown in the tree */
+    private List<TestError> errors = new ArrayList<TestError>();
+
+    /**
+     * If {@link #filter} is not <code>null</code> only errors are displayed
+     * that refer to one of the primitives in the filter.
+     */
+    private Set<OsmPrimitive> filter = null;
+
+    private int updateCount;
+
+    /**
+     * Constructor
+     * @param errors The list of errors
+     */
+    public ValidatorTreePanel(List<TestError> errors) {
+        ToolTipManager.sharedInstance().registerComponent(this);
+        this.setModel(treeModel);
+        this.setRootVisible(false);
+        this.setShowsRootHandles(true);
+        this.expandRow(0);
+        this.setVisibleRowCount(8);
+        this.setCellRenderer(new ValidatorTreeRenderer());
+        this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
+        setErrorList(errors);
+    }
+
+    @Override
+    public String getToolTipText(MouseEvent e) {
+        String res = null;
+        TreePath path = getPathForLocation(e.getX(), e.getY());
+        if (path != null) {
+            DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+            Object nodeInfo = node.getUserObject();
+
+            if (nodeInfo instanceof TestError) {
+                TestError error = (TestError) nodeInfo;
+                MultipleNameVisitor v = new MultipleNameVisitor();
+                v.visit(error.getPrimitives());
+                res = "<html>" + v.getText() + "<br>" + error.getMessage();
+                String d = error.getDescription();
+                if (d != null)
+                    res += "<br>" + d;
+                res += "</html>";
+            } else
+                res = node.toString();
+        }
+        return res;
+    }
+
+    /** Constructor */
+    public ValidatorTreePanel() {
+        this(null);
+    }
+
+    @Override
+    public void setVisible(boolean v) {
+        if (v)
+            buildTree();
+        else
+            treeModel.setRoot(new DefaultMutableTreeNode());
+        super.setVisible(v);
+    }
+
+    /**
+     * Builds the errors tree
+     */
+    public void buildTree() {
+        updateCount++;
+        DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
+
+        if (errors == null || errors.isEmpty()) {
+            treeModel.setRoot(rootNode);
+            return;
+        }
+
+        // Remember the currently expanded rows
+        Set<Object> oldSelectedRows = new HashSet<Object>();
+        Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
+        if (expanded != null) {
+            while (expanded.hasMoreElements()) {
+                TreePath path = expanded.nextElement();
+                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+                Object userObject = node.getUserObject();
+                if (userObject instanceof Severity)
+                    oldSelectedRows.add(userObject);
+                else if (userObject instanceof String) {
+                    String msg = (String) userObject;
+                    msg = msg.substring(0, msg.lastIndexOf(" ("));
+                    oldSelectedRows.add(msg);
+                }
+            }
+        }
+
+        Map<Severity, Bag<String, TestError>> errorTree = new HashMap<Severity, Bag<String, TestError>>();
+        Map<Severity, HashMap<String, Bag<String, TestError>>> errorTreeDeep = new HashMap<Severity, HashMap<String, Bag<String, TestError>>>();
+        for (Severity s : Severity.values()) {
+            errorTree.put(s, new Bag<String, TestError>(20));
+            errorTreeDeep.put(s, new HashMap<String, Bag<String, TestError>>());
+        }
+
+        for (TestError e : errors) {
+            if (e.getIgnored())
+                continue;
+            Severity s = e.getSeverity();
+            String d = e.getDescription();
+            String m = e.getMessage();
+            if (filter != null) {
+                boolean found = false;
+                for (OsmPrimitive p : e.getPrimitives()) {
+                    if (filter.contains(p)) {
+                        found = true;
+                        break;
+                    }
+                }
+                if (!found)
+                    continue;
+            }
+            if (d != null) {
+                Bag<String, TestError> b = errorTreeDeep.get(s).get(m);
+                if (b == null) {
+                    b = new Bag<String, TestError>(20);
+                    errorTreeDeep.get(s).put(m, b);
+                }
+                b.add(d, e);
+            } else
+                errorTree.get(s).add(m, e);
+        }
+
+        List<TreePath> expandedPaths = new ArrayList<TreePath>();
+        for (Severity s : Severity.values()) {
+            Bag<String, TestError> severityErrors = errorTree.get(s);
+            Map<String, Bag<String, TestError>> severityErrorsDeep = errorTreeDeep.get(s);
+            if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty())
+                continue;
+
+            // Severity node
+            DefaultMutableTreeNode severityNode = new DefaultMutableTreeNode(s);
+            rootNode.add(severityNode);
+
+            if (oldSelectedRows.contains(s))
+                expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode }));
+
+            for (Entry<String, List<TestError>> msgErrors : severityErrors.entrySet()) {
+                // Message node
+                List<TestError> errors = msgErrors.getValue();
+                String msg = msgErrors.getKey() + " (" + errors.size() + ")";
+                DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
+                severityNode.add(messageNode);
+
+                if (oldSelectedRows.contains(msgErrors.getKey())) {
+                    expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
+                }
+
+                for (TestError error : errors) {
+                    // Error node
+                    DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
+                    messageNode.add(errorNode);
+                }
+            }
+            for (Entry<String, Bag<String, TestError>> bag : severityErrorsDeep.entrySet()) {
+                // Group node
+                Bag<String, TestError> errorlist = bag.getValue();
+                DefaultMutableTreeNode groupNode = null;
+                if (errorlist.size() > 1) {
+                    String nmsg = bag.getKey() + " (" + errorlist.size() + ")";
+                    groupNode = new DefaultMutableTreeNode(nmsg);
+                    severityNode.add(groupNode);
+                    if (oldSelectedRows.contains(bag.getKey())) {
+                        expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode }));
+                    }
+                }
+
+                for (Entry<String, List<TestError>> msgErrors : errorlist.entrySet()) {
+                    // Message node
+                    List<TestError> errors = msgErrors.getValue();
+                    String msg;
+                    if (groupNode != null)
+                        msg = msgErrors.getKey() + " (" + errors.size() + ")";
+                    else
+                        msg = bag.getKey() + " - " + msgErrors.getKey() + " (" + errors.size() + ")";
+                    DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
+                    if (groupNode != null)
+                        groupNode.add(messageNode);
+                    else
+                        severityNode.add(messageNode);
+
+                    if (oldSelectedRows.contains(msgErrors.getKey())) {
+                        if (groupNode != null) {
+                            expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode,
+                                    messageNode }));
+                        } else {
+                            expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
+                        }
+                    }
+
+                    for (TestError error : errors) {
+                        // Error node
+                        DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
+                        messageNode.add(errorNode);
+                    }
+                }
+            }
+        }
+
+        treeModel.setRoot(rootNode);
+        for (TreePath path : expandedPaths) {
+            this.expandPath(path);
+        }
+    }
+
+    /**
+     * Sets the errors list used by a data layer
+     * @param errors The error list that is used by a data layer
+     */
+    public void setErrorList(List<TestError> errors) {
+        this.errors = errors;
+        if (isVisible())
+            buildTree();
+    }
+
+    /**
+     * Clears the current error list and adds these errors to it
+     * @param errors The validation errors
+     */
+    public void setErrors(List<TestError> newerrors) {
+        if (errors == null)
+            return;
+        errors.clear();
+        for (TestError error : newerrors) {
+            if (!error.getIgnored())
+                errors.add(error);
+        }
+        if (isVisible())
+            buildTree();
+    }
+
+    /**
+     * Returns the errors of the tree
+     * @return  the errors of the tree
+     */
+    public List<TestError> getErrors() {
+        return errors != null ? errors : Collections.<TestError> emptyList();
+    }
+
+    public Set<OsmPrimitive> getFilter() {
+        return filter;
+    }
+
+    public void setFilter(Set<OsmPrimitive> filter) {
+        if (filter != null && filter.size() == 0)
+            this.filter = null;
+        else
+            this.filter = filter;
+        if (isVisible())
+            buildTree();
+    }
+
+    /**
+     * Updates the current errors list
+     * @param errors The validation errors
+     */
+    public void resetErrors() {
+        List<TestError> e = new ArrayList<TestError>(errors);
+        setErrors(e);
+    }
+
+    /**
+     * Expands all tree
+     */
+    @SuppressWarnings("unchecked")
+    public void expandAll() {
+        DefaultMutableTreeNode root = getRoot();
+
+        int row = 0;
+        Enumeration<DefaultMutableTreeNode> children = root.breadthFirstEnumeration();
+        while (children.hasMoreElements()) {
+            children.nextElement();
+            expandRow(row++);
+        }
+    }
+
+    /**
+     * Returns the root node model.
+     * @return The root node model
+     */
+    public DefaultMutableTreeNode getRoot() {
+        return (DefaultMutableTreeNode) treeModel.getRoot();
+    }
+
+    public int getUpdateCount() {
+        return updateCount;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java	(revision 3669)
@@ -0,0 +1,50 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.gui.dialogs.validator;
+
+import java.awt.Component;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeCellRenderer;
+
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * Tree renderer for displaying errors
+ * @author frsantos
+ */
+public class ValidatorTreeRenderer extends DefaultTreeCellRenderer
+{
+    /** Serializable ID */
+    private static final long serialVersionUID = 5567632718124640198L;
+
+    @Override
+    public Component getTreeCellRendererComponent(JTree tree, Object value,
+            boolean selected, boolean expanded, boolean leaf, int row,
+            boolean hasFocus)
+    {
+        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
+
+        DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
+        Object nodeInfo = node.getUserObject();
+
+        if (nodeInfo instanceof Severity)
+        {
+            Severity s = (Severity)nodeInfo;
+            setIcon(ImageProvider.get("data", s.getIcon()));
+        }
+        else if (nodeInfo instanceof TestError)
+        {
+            TestError error = (TestError)nodeInfo;
+            MultipleNameVisitor v = new MultipleNameVisitor();
+            v.visit(error.getPrimitives());
+            setText(v.getText());
+            setIcon(v.getIcon());
+        }
+
+        return this;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java	(revision 3669)
@@ -24,4 +24,5 @@
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 
@@ -63,4 +64,5 @@
 import org.openstreetmap.josm.data.osm.visitor.paint.PaintVisitor;
 import org.openstreetmap.josm.data.osm.visitor.paint.SimplePaintVisitor;
+import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
 import org.openstreetmap.josm.gui.MapView;
@@ -86,4 +88,6 @@
     private boolean isChanged = true;
     private int highlightUpdateCount;
+
+    public List<TestError> validationErrors = new ArrayList<TestError>();
 
     protected void setRequiresSaveToFile(boolean newValue) {
@@ -483,5 +487,5 @@
             }
 
-            ArrayList<WayPoint> trkseg = null;
+            List<WayPoint> trkseg = null;
             for (Node n : w.getNodes()) {
                 if (!n.isUsable()) {
Index: /trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/ValidatorLayer.java	(revision 3669)
@@ -0,0 +1,153 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.gui.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Graphics2D;
+import java.util.Enumeration;
+import java.util.List;
+
+import javax.swing.Action;
+import javax.swing.Icon;
+import javax.swing.tree.DefaultMutableTreeNode;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.data.validation.util.Bag;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
+import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
+import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * A layer showing error messages.
+ *
+ * @author frsantos
+ */
+public class ValidatorLayer extends Layer implements LayerChangeListener {
+
+    private int updateCount = -1;
+
+
+    public ValidatorLayer() {
+        super(tr("Validation errors"));
+        MapView.addLayerChangeListener(this);
+    }
+
+    /**
+     * Return a static icon.
+     */
+    @Override
+    public Icon getIcon() {
+        return ImageProvider.get("layer", "validator_small");
+    }
+
+    /**
+     * Draw all primitives in this layer but do not draw modified ones (they
+     * are drawn by the edit layer).
+     * Draw nodes last to overlap the ways they belong to.
+     */
+    @SuppressWarnings("unchecked")
+    @Override
+    public void paint(final Graphics2D g, final MapView mv, Bounds bounds) {
+        updateCount = Main.map.validatorDialog.tree.getUpdateCount();
+        DefaultMutableTreeNode root = Main.map.validatorDialog.tree.getRoot();
+        if (root == null || root.getChildCount() == 0)
+            return;
+
+        DefaultMutableTreeNode severity = (DefaultMutableTreeNode) root.getLastChild();
+        while (severity != null) {
+            Enumeration<DefaultMutableTreeNode> errorMessages = severity.breadthFirstEnumeration();
+            while (errorMessages.hasMoreElements()) {
+                Object tn = errorMessages.nextElement().getUserObject();
+                if (tn instanceof TestError)
+                    ((TestError) tn).paint(g, mv);
+            }
+
+            // Severities in inverse order
+            severity = severity.getPreviousSibling();
+        }
+    }
+
+    @Override
+    public String getToolTipText() {
+        Bag<Severity, TestError> errorTree = new Bag<Severity, TestError>();
+        List<TestError> errors = Main.map.validatorDialog.tree.getErrors();
+        for (TestError e : errors) {
+            errorTree.add(e.getSeverity(), e);
+        }
+
+        StringBuilder b = new StringBuilder();
+        for (Severity s : Severity.values()) {
+            if (errorTree.containsKey(s))
+                b.append(tr(s.toString())).append(": ").append(errorTree.get(s).size()).append("<br>");
+        }
+
+        if (b.length() == 0)
+            return "<html>" + tr("No validation errors") + "</html>";
+        else
+            return "<html>" + tr("Validation errors") + ":<br>" + b + "</html>";
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {
+    }
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override
+    public boolean isChanged() {
+        return updateCount != Main.map.validatorDialog.tree.getUpdateCount();
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+    }
+
+    @Override
+    public Object getInfoComponent() {
+        return getToolTipText();
+    }
+
+    @Override
+    public Action[] getMenuEntries() {
+        return new Action[] {
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                new RenameLayerAction(null, this),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this) };
+    }
+
+    @Override
+    public void destroy() {
+    }
+
+    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+    }
+
+    public void layerAdded(Layer newLayer) {
+    }
+
+    /**
+     * If layer is the OSM Data layer, remove all errors
+     */
+    public void layerRemoved(Layer oldLayer) {
+        if (oldLayer instanceof OsmDataLayer &&  Main.map.mapView.getEditLayer() == null) {
+            Main.map.mapView.removeLayer(this);
+        } else if (oldLayer == this) {
+            MapView.removeLayerChangeListener(this);
+            OsmValidator.errorLayer = null;
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/PreferenceTabbedPane.java	(revision 3669)
@@ -44,6 +44,6 @@
 
     /**
-     * Allows PreferenceSettings to do validation of entered values when ok was pressed. If data are invalid then event can
-     * return false to cancel closing of preferences dialog
+     * Allows PreferenceSettings to do validation of entered values when ok was pressed. 
+     * If data is invalid then event can return false to cancel closing of preferences dialog.
      *
      */
@@ -168,5 +168,5 @@
                 // to restart JOSM
                 //
-                StringBuffer sb = new StringBuffer();
+                StringBuilder sb = new StringBuilder();
                 sb.append("<html>");
                 if (task != null && !task.isCanceled()) {
@@ -222,5 +222,5 @@
 
     public void buildGui() {
-        for (PreferenceSettingFactory factory:settingsFactory) {
+        for (PreferenceSettingFactory factory : settingsFactory) {
             // logger.info("creating settings: " + factory);
             PreferenceSetting setting = factory.createPreferenceSetting();
@@ -276,4 +276,5 @@
         settingsFactory.add(new AudioPreference.Factory());
         settingsFactory.add(new ShortcutPreference.Factory());
+        settingsFactory.add(new ValidatorPreference.Factory());
 
         PluginHandler.getPreferenceSetting(settingsFactory);
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/ValidatorPreference.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/ValidatorPreference.java	(revision 3669)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/ValidatorPreference.java	(revision 3669)
@@ -0,0 +1,132 @@
+// License: GPL. See LICENSE file for details.
+package org.openstreetmap.josm.gui.preferences;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+import java.util.Collection;
+
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.validation.OsmValidator;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.tools.GBC;
+
+/**
+ * Preference settings for the validator
+ *
+ * @author frsantos
+ */
+public class ValidatorPreference implements PreferenceSetting
+{
+
+    public static class Factory implements PreferenceSettingFactory {
+        public PreferenceSetting createPreferenceSetting() {
+            return new ValidatorPreference();
+        }
+    }
+
+    /** The preferences prefix */
+    public static final String PREFIX = "validator";
+
+    /** The preferences key for debug preferences */
+    public static final String PREF_DEBUG = PREFIX + ".debug";
+
+    /** The preferences key for debug preferences */
+    public static final String PREF_LAYER = PREFIX + ".layer";
+
+    /** The preferences key for enabled tests */
+    public static final String PREF_TESTS = PREFIX + ".tests";
+
+    /** The preferences key for enabled tests */
+    public static final String PREF_USE_IGNORE = PREFIX + ".ignore";
+
+    /** The preferences key for enabled tests before upload*/
+    public static final String PREF_TESTS_BEFORE_UPLOAD = PREFIX + ".testsBeforeUpload";
+
+    /** The preferences key for ignored severity other on upload */
+    public static final String PREF_OTHER_UPLOAD = PREFIX + ".otherUpload";
+
+    /**
+     * The preferences key for enabling the permanent filtering
+     * of the displayed errors in the tree regarding the current selection
+     */
+    public static final String PREF_FILTER_BY_SELECTION = PREFIX + ".selectionFilter";
+
+    private JCheckBox prefUseIgnore;
+    private JCheckBox prefUseLayer;
+    private JCheckBox prefOtherUpload;
+
+    /** The list of all tests */
+    private Collection<Test> allTests;
+
+    public void addGui(PreferenceTabbedPane gui)
+    {
+        JPanel testPanel = new JPanel(new GridBagLayout());
+        testPanel.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
+
+        prefUseIgnore = new JCheckBox(tr("Use ignore list."), Main.pref.getBoolean(PREF_USE_IGNORE, true));
+        prefUseIgnore.setToolTipText(tr("Use the ignore list to suppress warnings."));
+        testPanel.add(prefUseIgnore, GBC.eol());
+
+        prefUseLayer = new JCheckBox(tr("Use error layer."), Main.pref.getBoolean(PREF_LAYER, true));
+        prefUseLayer.setToolTipText(tr("Use the error layer to display problematic elements."));
+        testPanel.add(prefUseLayer, GBC.eol());
+
+        prefOtherUpload = new JCheckBox(tr("Show informational level on upload."), Main.pref.getBoolean(PREF_OTHER_UPLOAD, false));
+        prefOtherUpload.setToolTipText(tr("Show the informational tests in the upload check windows."));
+        testPanel.add(prefOtherUpload, GBC.eol());
+
+        GBC a = GBC.eol().insets(-5,0,0,0);
+        a.anchor = GBC.EAST;
+        testPanel.add( new JLabel(tr("On demand")), GBC.std() );
+        testPanel.add( new JLabel(tr("On upload")), a );
+
+        allTests = OsmValidator.getTests();
+        for(Test test: allTests)
+        {
+            test.addGui(testPanel);
+        }
+
+        JScrollPane testPane = new JScrollPane(testPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+        testPane.setBorder(null);
+
+        String description = tr("An OSM data validator that checks for common errors made by users and editor programs.");
+        JPanel tab = gui.createPreferenceTab("validator", tr("Data validator"), description);
+        tab.add(testPane, GBC.eol().fill(GBC.BOTH));
+        tab.add(GBC.glue(0,10), a);
+    }
+
+    public boolean ok()
+    {
+        StringBuilder tests = new StringBuilder();
+        StringBuilder testsBeforeUpload = new StringBuilder();
+        Boolean res = false;
+
+        for (Test test : allTests)
+        {
+            if(test.ok())
+                res = false;
+            String name = test.getClass().getSimpleName();
+            tests.append( ',' ).append( name ).append( '=' ).append( test.enabled );
+            testsBeforeUpload.append( ',' ).append( name ).append( '=' ).append( test.testBeforeUpload );
+        }
+
+        if (tests.length() > 0 ) tests = tests.deleteCharAt(0);
+        if (testsBeforeUpload.length() > 0 ) testsBeforeUpload = testsBeforeUpload.deleteCharAt(0);
+
+        OsmValidator.initializeTests( allTests );
+
+        Main.pref.put( PREF_TESTS, tests.toString());
+        Main.pref.put( PREF_TESTS_BEFORE_UPLOAD, testsBeforeUpload.toString());
+        Main.pref.put( PREF_USE_IGNORE, prefUseIgnore.isSelected());
+        Main.pref.put( PREF_OTHER_UPLOAD, prefOtherUpload.isSelected());
+        Main.pref.put( PREF_LAYER, prefUseLayer.isSelected());
+        return false;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java	(revision 3669)
@@ -28,4 +28,6 @@
 import java.util.Set;
 import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -67,9 +69,19 @@
 public class PluginHandler {
 
-    final public static String [] DEPRECATED_PLUGINS = new String[] {"mappaint", "unglueplugin",
-        "lang-de", "lang-en_GB", "lang-fr", "lang-it", "lang-pl", "lang-ro",
-        "lang-ru", "ewmsplugin", "ywms", "tways-0.2", "geotagged", "landsat",
-        "namefinder", "waypoints", "slippy_map_chooser", "tcx-support", "usertools",
-        "AgPifoJ", "utilsplugin", "ghost"};
+    /* deprecated plugins that are removed on start
+       key - plugin name; value - explanation for deprecation (optional, can be null) */
+    public final static Map<String, String> DEPRECATED_PLUGINS = new TreeMap<String, String>();
+    static {
+        String IN_CORE = tr("integrated into main program");
+        for (String[] depr : new String[][] {
+            {"mappaint"}, {"unglueplugin"}, {"lang-de"}, {"lang-en_GB"}, {"lang-fr"},
+            {"lang-it"}, {"lang-pl"}, {"lang-ro"}, {"lang-ru"}, {"ewmsplugin"},
+            {"ywms"}, {"tways-0.2"}, {"geotagged"}, {"landsat"}, {"namefinder"},
+            {"waypoints"}, {"slippy_map_chooser"}, {"tcx-support"}, {"usertools"},
+            {"AgPifoJ", IN_CORE}, {"utilsplugin", IN_CORE}, {"ghost"},
+            {"validator", IN_CORE}}) {
+            DEPRECATED_PLUGINS.put(depr[0], depr.length >= 2 ? depr[1] : null);
+        }
+    }
 
     final public static String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"};
@@ -90,6 +102,6 @@
      */
     private static void filterDeprecatedPlugins(Window parent, Collection<String> plugins) {
-        Set<String> removedPlugins = new HashSet<String>();
-        for (String p : DEPRECATED_PLUGINS) {
+        Set<String> removedPlugins = new TreeSet<String>();
+        for (String p : DEPRECATED_PLUGINS.keySet()) {
             if (plugins.contains(p)) {
                 plugins.remove(p);
@@ -103,5 +115,5 @@
         // notify user about removed deprecated plugins
         //
-        StringBuffer sb = new StringBuffer();
+        StringBuilder sb = new StringBuilder();
         sb.append("<html>");
         sb.append(trn(
@@ -112,5 +124,10 @@
         sb.append("<ul>");
         for (String name: removedPlugins) {
-            sb.append("<li>").append(name).append("</li>");
+            sb.append("<li>").append(name);
+            String explanation = DEPRECATED_PLUGINS.get(name);
+            if (explanation != null) {
+                sb.append(" ("+explanation+")");
+            }
+            sb.append("</li>");
         }
         sb.append("</ul>");
Index: /trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/plugins/ReadLocalPluginInformationTask.java	(revision 3669)
@@ -201,5 +201,5 @@
 
     protected void filterOldPlugins() {
-        for (String p : PluginHandler.DEPRECATED_PLUGINS) {
+        for (String p : PluginHandler.DEPRECATED_PLUGINS.keySet()) {
             if (canceled)return;
             if (availablePlugins.containsKey(p)) {
Index: /trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java	(revision 3668)
+++ /trunk/src/org/openstreetmap/josm/plugins/ReadRemotePluginInformationTask.java	(revision 3669)
@@ -295,5 +295,5 @@
     protected List<PluginInformation> filterDeprecatedPlugins(List<PluginInformation> plugins) {
         List<PluginInformation> ret = new ArrayList<PluginInformation>(plugins.size());
-        HashSet<String> deprecatedPluginNames = new HashSet<String>(Arrays.asList(PluginHandler.DEPRECATED_PLUGINS));
+        HashSet<String> deprecatedPluginNames = new HashSet<String>(PluginHandler.DEPRECATED_PLUGINS.keySet());
         for (PluginInformation plugin: plugins) {
             if (deprecatedPluginNames.contains(plugin.name)) {
