Index: applications/editors/josm/plugins/imagery/.classpath
===================================================================
--- applications/editors/josm/plugins/imagery/.classpath	(revision 24501)
+++ applications/editors/josm/plugins/imagery/.classpath	(revision 24501)
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/JOSM"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/remotecontrol"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
Index: applications/editors/josm/plugins/imagery/.project
===================================================================
--- applications/editors/josm/plugins/imagery/.project	(revision 24501)
+++ applications/editors/josm/plugins/imagery/.project	(revision 24501)
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>imagery</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
Index: applications/editors/josm/plugins/imagery/GPL-v2.0.txt
===================================================================
--- applications/editors/josm/plugins/imagery/GPL-v2.0.txt	(revision 24501)
+++ applications/editors/josm/plugins/imagery/GPL-v2.0.txt	(revision 24501)
@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
Index: applications/editors/josm/plugins/imagery/GPL-v3.0.txt
===================================================================
--- applications/editors/josm/plugins/imagery/GPL-v3.0.txt	(revision 24501)
+++ applications/editors/josm/plugins/imagery/GPL-v3.0.txt	(revision 24501)
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
Index: applications/editors/josm/plugins/imagery/README
===================================================================
--- applications/editors/josm/plugins/imagery/README	(revision 24501)
+++ applications/editors/josm/plugins/imagery/README	(revision 24501)
@@ -0,0 +1,27 @@
+This plugin is an union of slippymap plugin and wmsplugin.
+Combined by Upliner, licensed under the GNU GPL v2 or later.
+
+WMSPlugin authors:
+==========================================================================
+This plugin has been created by tim <chippy2005@gmail.com>
+and has received major contributions from Frederik Ramm
+<frederik@remote.org>. It is based on the "Landsat" plugin
+by Nick Whitelegg <Nick.Whitelegg@solent.ac.uk> and includes
+some code from Jonathan Stott <jonathan@jstott.me.uk>, Gabriel Ebner
+<ge@gabrielebner.at> and Ulf Lamping <ulf.lamping@web.de>.
+The automatic tiles downloading and Yahoo downloader made by Petr Dlouhý <petr.dlouhy@email.cz>
+
+This plugin is licensed under the GNU GPL v2 or later.
+==========================================================================
+
+Slippymap plugin authors:
+==========================================================================
+A plugin for displaying a slippy map grid, with various server interaction
+options (download tiles, request tile updates etc.)
+
+Author: Frederik Ramm <frederik@remote.org>
+        Lubomir Varga <lubomir.varga@freemap.sk> or <luvar@plaintext.sk>
+Public Domain.
+
+Software with a little bit of customisation, fade background feature, autozoom, autoload tiles e.t.c. Just a begining of the best plugin for josm ;-)
+==========================================================================
Index: applications/editors/josm/plugins/imagery/build.xml
===================================================================
--- applications/editors/josm/plugins/imagery/build.xml	(revision 24501)
+++ applications/editors/josm/plugins/imagery/build.xml	(revision 24501)
@@ -0,0 +1,258 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+** This is a template build file for a JOSM  plugin.
+**
+** Maintaining versions
+** ====================
+** see README.template
+**
+** Usage
+** =====
+** To build it run
+**
+**    > ant  dist
+**
+** To install the generated plugin locally (in you default plugin directory) run
+**
+**    > ant  install
+**
+** The generated plugin jar is not automatically available in JOSMs plugin configuration
+** dialog. You have to check it in first.
+**
+** Use the ant target 'publish' to check in the plugin and make it available to other
+** JOSM users:
+**    set the properties commit.message and plugin.main.version
+** and run
+**    > ant  publish
+**
+**
+-->
+<project name="imagery" default="dist" basedir=".">
+
+    <!-- enter the SVN commit message -->
+    <property name="commit.message" value="Commit message" />
+    <!-- enter the *lowest* JOSM version this plugin is currently compatible with -->
+    <property name="plugin.main.version" value="3687" />
+
+
+    <!--
+      ************************************************
+      ** should not be necessary to change the following properties
+     -->
+    <property name="josm"                   location="../../core/dist/josm-custom.jar"/>
+    <property name="remotecontrol" location="../../dist/remotecontrol.jar" />
+    <property name="plugin.build.dir"       value="build"/>
+    <property name="plugin.src.dir"         value="src"/>
+    <!-- this is the directory where the plugin jar is copied to -->
+    <property name="plugin.dist.dir"        value="../../dist"/>
+    <property name="ant.build.javac.target" value="1.5"/>
+    <property name="plugin.dist.dir"        value="../../dist"/>
+    <property name="plugin.jar"             value="${plugin.dist.dir}/${ant.project.name}.jar"/>
+
+    <!--
+    **********************************************************
+    ** init - initializes the build
+    **********************************************************
+    -->
+    <target name="init">
+        <mkdir dir="${plugin.build.dir}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** compile - complies the source tree
+    **********************************************************
+    -->
+    <target name="compile" depends="init">
+        <echo message="compiling sources for  ${plugin.jar} ... "/>
+        <javac srcdir="src" classpath="${josm};${remotecontrol}" debug="true" destdir="${plugin.build.dir}">
+            <compilerarg value="-Xlint:deprecation"/>
+            <compilerarg value="-Xlint:unchecked"/>
+        </javac>
+    </target>
+
+    <!--
+    **********************************************************
+    ** dist - creates the plugin jar
+    **********************************************************
+    -->
+    <target name="dist" depends="compile,revision">
+        <echo message="creating ${ant.project.name}.jar ... "/>
+        <copy todir="${plugin.build.dir}/resources">
+            <fileset dir="resources"/>
+        </copy>
+        <copy todir="${plugin.build.dir}/images">
+            <fileset dir="images"/>
+        </copy>
+        <copy todir="${plugin.build.dir}">
+            <fileset dir=".">
+                <include name="README" />
+                <include name="LICENSE" />
+            </fileset>
+        </copy>
+        <jar destfile="${plugin.jar}" basedir="${plugin.build.dir}">
+            <!--
+        ************************************************
+        ** configure these properties. Most of them will be copied to the plugins
+        ** manifest file. Property values will also show up in the list available
+        ** plugins: http://josm.openstreetmap.de/wiki/Plugins.
+        **
+        ************************************************
+    -->
+            <manifest>
+                <attribute name="Author" value="Tim Waters, Petr Dlouhý, Frederik Ramm, Upliner and others"/>
+                <attribute name="Plugin-Class" value="org.openstreetmap.josm.plugins.imagery.ImageryPlugin"/>
+                <attribute name="Plugin-Date" value="${version.entry.commit.date}"/>
+                <attribute name="Plugin-Description" value="Experimental union of SlippyMap plugin and WMSPlugin"/>
+                <attribute name="Plugin-Icon" value="images/wms.png"/>
+                <attribute name="Plugin-Link" value="http://wiki.openstreetmap.org/wiki/JOSM/Plugins/WMSPlugin" />
+                <attribute name="Plugin-Mainversion" value="${plugin.main.version}"/>
+                <attribute name="Plugin-Version" value="${version.entry.commit.revision}"/>
+            </manifest>
+        </jar>
+    </target>
+
+    <!--
+    **********************************************************
+    ** revision - extracts the current revision number for the
+    **    file build.number and stores it in the XML property
+    **    version.*
+    **********************************************************
+    -->
+    <target name="revision">
+
+        <exec append="false" output="REVISION" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="."/>
+        </exec>
+        <xmlproperty file="REVISION" prefix="version" keepRoot="false" collapseAttributes="true"/>
+        <delete file="REVISION"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** clean - clean up the build environment
+    **********************************************************
+    -->
+    <target name="clean">
+        <delete dir="${plugin.build.dir}"/>
+        <delete file="${plugin.jar}"/>
+    </target>
+
+    <!--
+    **********************************************************
+    ** install - install the plugin in your local JOSM installation
+    **********************************************************
+    -->
+    <target name="install" depends="dist">
+        <property environment="env"/>
+        <condition property="josm.plugins.dir" value="${env.APPDATA}/JOSM/plugins" else="${user.home}/.josm/plugins">
+            <and>
+                <os family="windows"/>
+            </and>
+        </condition>
+        <copy file="${plugin.jar}" todir="${josm.plugins.dir}"/>
+    </target>
+
+
+    <!--
+    ************************** Publishing the plugin *********************************** 
+    -->
+    <!--
+        ** extracts the JOSM release for the JOSM version in ../core and saves it in the 
+        ** property ${coreversion.info.entry.revision}
+        **
+        -->
+    <target name="core-info">
+        <exec append="false" output="core.info.xml" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="info"/>
+            <arg value="--xml"/>
+            <arg value="../../core"/>
+        </exec>
+        <xmlproperty file="core.info.xml" prefix="coreversion" keepRoot="true" collapseAttributes="true"/>
+        <echo>Building against core revision ${coreversion.info.entry.revision}.</echo>
+        <echo>Plugin-Mainversion is set to ${plugin.main.version}.</echo>
+        <delete file="core.info.xml" />
+    </target>
+
+    <!--
+        ** commits the source tree for this plugin
+        -->
+    <target name="commit-current">
+        <echo>Commiting the plugin source with message '${commit.message}' ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="commit"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="."/>
+        </exec>
+    </target>
+
+    <!--
+        ** updates (svn up) the source tree for this plugin
+        -->
+    <target name="update-current">
+        <echo>Updating plugin source ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="."/>
+        </exec>
+        <echo>Updating ${plugin.jar} ...</echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="up"/>
+            <arg value="../dist/${plugin.jar}"/>
+        </exec>
+    </target>
+
+    <!--
+        ** commits the plugin.jar 
+        -->
+    <target name="commit-dist">
+        <echo>
+    ***** Properties of published ${plugin.jar} *****
+    Commit message    : '${commit.message}'                    
+    Plugin-Mainversion: ${plugin.main.version}
+    JOSM build version: ${coreversion.info.entry.revision}
+    Plugin-Version    : ${version.entry.commit.revision}
+    ***** / Properties of published ${plugin.jar} *****                    
+                        
+    Now commiting ${plugin.jar} ...
+    </echo>
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false">
+            <env key="LANG" value="C"/>
+            <arg value="-m '${commit.message}'"/>
+            <arg value="commit"/>
+            <arg value="${plugin.jar}"/>
+        </exec>
+    </target>
+
+    <!-- ** make sure svn is present as a command line tool ** -->
+    <target name="ensure-svn-present">
+        <exec append="true" output="svn.log" executable="svn" failifexecutionfails="false" failonerror="false" resultproperty="svn.exit.code">
+            <env key="LANG" value="C" />
+            <arg value="--version" />
+        </exec>
+        <fail message="Fatal: command 'svn --version' failed. Please make sure svn is installed on your system.">
+            <!-- return code not set at all? Most likely svn isn't installed -->
+            <condition>
+                <not>
+                    <isset property="svn.exit.code" />
+                </not>
+            </condition>
+        </fail>
+        <fail message="Fatal: command 'svn --version' failed. Please make sure a working copy of svn is installed on your system.">
+            <!-- error code from SVN? Most likely svn is not what we are looking on this system -->
+            <condition>
+                <isfailure code="${svn.exit.code}" />
+            </condition>
+        </fail>
+    </target>
+
+    <target name="publish" depends="ensure-svn-present,core-info,commit-current,update-current,clean,dist,commit-dist">
+    </target>
+</project>
Index: applications/editors/josm/plugins/imagery/sources.cfg
===================================================================
--- applications/editors/josm/plugins/imagery/sources.cfg	(revision 24501)
+++ applications/editors/josm/plugins/imagery/sources.cfg	(revision 24501)
@@ -0,0 +1,69 @@
+# OUTDATED - only for old plugins
+# See http://josm.openstreetmap.de/wiki/Maps for newer data.
+#
+# FORMAT
+# default(true or false);Name;URL
+# NOTE: default items should be common and worldwide
+#
+true;Landsat;wms:http://onearth.jpl.nasa.gov/wms.cgi?request=GetMap&layers=global_mosaic&styles=&format=image/jpeg&
+true;Landsat (mirror);wms:http://irs.gis-lab.info/?layers=landsat&
+false;Open Aerial Map;wms:http://openaerialmap.org/wms/?VERSION=1.0&request=GetMap&layers=world&styles=&format=image/jpeg&
+#
+# different forms of imagery
+true;Bing sat;bing:bing
+true;Yahoo Sat;html:http://josm.openstreetmap.de/wmsplugin/YahooDirect.html?
+true;OpenStreetMap;tms:http://tile.openstreetmap.org/
+false;OpenCycleMap;tms:http://tile.opencyclemap.org/cycle/
+false;TilesAtHome;tms:http://tah.openstreetmap.org/Tiles/tile/
+#
+#
+# only for Germany
+false;Streets NRW Geofabrik.de;wms:http://tools.geofabrik.de/osmi/view/strassennrw/josmwms?
+#
+#
+# only for North America
+# Terraserver USCG - High resolution maps
+false;Terraserver Topo;wms:http://terraservice.net/ogcmap.ashx?version=1.1.1&request=GetMap&Layers=drg&styles=&format=image/jpeg&
+false;Terraserver Urban;wms:http://terraservice.net/ogcmap.ashx?version=1.1.1&request=GetMap&Layers=urbanarea&styles=&format=image/jpeg&
+#
+#
+# only for Czech Republic
+false;Czech CUZK:KM;wms:http://wms.cuzk.cz/wms.asp?service=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=parcelni_cisla_i,obrazy_parcel_i,RST_KMD_I,hranice_parcel_i,DEF_BUDOVY,RST_KN_I,dalsi_p_mapy_i,prehledka_kat_prac,prehledka_kat_uz,prehledka_kraju-linie&FORMAT=image/png&transparent=TRUE&
+false;Czech UHUL:ORTOFOTO;wms:http://geoportal2.uhul.cz/cgi-bin/oprl.asp?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=Ortofoto_cb&STYLES=default&FORMAT=image/jpeg&TRANSPARENT=TRUE&
+#
+#
+# only for GB
+# fails with division by zero error
+false;NPE Maps;wms:http://nick.dev.openstreetmap.org/openpaths/freemap.php?layers=npe&
+false;NPE Maps (Tim);wms:http://dev.openstreetmap.org/~timsc/wms2/map.php?
+false;7th Series (OS7);wms:http://ooc.openstreetmap.org/wms/map.php?source=os7&
+#
+#
+# only for Japan
+false;MLIT Japan (ORTHO);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO01);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO01&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO02);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO02&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+false;MLIT Japan (ORTHO03);wms:http://orthophoto.mlit.go.jp:8888/wms/service/wmsRasterTileMap?VERSION=1.3.0&REQUEST=GetMap&LAYERS=ORTHO03&STYLES=Default&CRS=EPSG:4612&BBOX={s},{w},{n},{e}&WIDTH={width}&HEIGHT={height}&FORMAT=image/png&BGCOLOR=OxFFFFFF
+#
+#
+# only for Italy
+false;Lodi - Italy;wms:http://sit.provincia.lodi.it/mapserver/mapserv.exe?map=ortofoto_wgs84.map&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&LAYERS=Terraitaly%20Ortofoto%202007&STYLES=%2C%2C&FORMAT=image/png&TRANSPARENT=TRUE&
+false;Sicily - Italy;wms:http://88.53.214.52/sitr/services/WGS84_F33/Ortofoto_ATA20072008_f33/MapServer/WMSServer?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=CRS:84&LAYERS=0&STYLES=default&FORMAT=image/jpeg&
+false;PCN 2006 - Italy;wms:http://wms.pcn.minambiente.it/cgi-bin/mapserv.exe?map=/ms_ogc/service/ortofoto_colore_06.map&LAYERS=ortofoto_colore_06_32,ortofoto_colore_06_33&REQUEST=GetMap&VERSION=1.1.1&FORMAT=image%2Fjpeg&
+#
+# only for France
+false;SPOTMaps (France);wms:http://spotmaps.youmapps.org/cgi-bin/mapserv?map=/home/ortho/ortho.map&service=wms&version=1.1.1&srs=EPSG:4326&request=GetMap&layers=spotmaps4osm&format=image/jpeg&FORMAT=image/jpeg&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&Layers=demo&;http://www.youmapps.org/licenses/EULA-OSM-J-{lang}.html
+#
+#
+# URLS must be designed to append arguments directly behind. So the URLS should either end with '?' or '&'
+# Following arguments are added: width, height, bbox, srs (projection method)
+# srs is only added when no srs is given already (In this case the projection is checked
+# and an error is issued when they mismatch).
+#
+# If more specific URL design is needed, then patterns are supported as well. If
+# patterns are found no other arguments are added:
+# {proj} is replaced by projection
+# {bbox} is replaced by bounding box using projected coordinates
+# {width} is requested display width
+# {height} is requested display height
+# {w},{s},{n},{e} are replaced by corresponding coordinates
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/AddImageryLayerAction.java	(revision 24501)
@@ -0,0 +1,25 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+
+public class AddImageryLayerAction extends JosmAction {
+
+    private final ImageryInfo info;
+
+    public AddImageryLayerAction(ImageryInfo info) {
+        super(info.getMenuName(), "wmsmenu", tr("Add imagery layer {0}",info.getName()), null, false);
+        putValue("toolbar", "imagery_" + info.getToolbarName());
+        this.info = info;
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        ImageryLayer wmsLayer = ImageryLayer.create(info);
+        Main.main.addLayer(wmsLayer);
+    }
+};
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryAdjustAction.java	(revision 24501)
@@ -0,0 +1,206 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.GridBagLayout;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.List;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.Icon;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.mapmode.MapMode;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+
+public class ImageryAdjustAction extends MapMode implements MouseListener, MouseMotionListener{
+
+    boolean mouseDown;
+    EastNorth prevEastNorth;
+    private ImageryLayer adjustingLayer;
+
+    public ImageryAdjustAction(MapFrame mapFrame) {
+        super(tr("Adjust imagery"), "adjustwms",
+                tr("Adjust the position of the selected imagery layer"), mapFrame,
+                ImageProvider.getCursor("normal", "move"));
+    }
+
+    @Override public void enterMode() {
+        super.enterMode();
+        if (!hasLayersToAdjust()) {
+            warnNoImageryLayers();
+            return;
+        }
+        List<ImageryLayer> imageryLayers = Main.map.mapView.getLayersOfType(ImageryLayer.class);
+        if (imageryLayers.size() == 1) {
+            adjustingLayer = imageryLayers.get(0);
+        } else {
+            adjustingLayer = (ImageryLayer)askAdjustLayer(Main.map.mapView.getLayersOfType(ImageryLayer.class));
+        }
+        if (adjustingLayer == null)
+            return;
+        if (!adjustingLayer.isVisible()) {
+            adjustingLayer.setVisible(true);
+        }
+        Main.map.mapView.addMouseListener(this);
+        Main.map.mapView.addMouseMotionListener(this);
+    }
+
+    @Override public void exitMode() {
+        super.exitMode();
+        Main.map.mapView.removeMouseListener(this);
+        Main.map.mapView.removeMouseMotionListener(this);
+        adjustingLayer = null;
+    }
+
+    @Override public void mousePressed(MouseEvent e) {
+        if (e.getButton() != MouseEvent.BUTTON1)
+            return;
+
+        if (adjustingLayer.isVisible()) {
+            prevEastNorth=Main.map.mapView.getEastNorth(e.getX(),e.getY());
+                Main.map.mapView.setCursor
+                (Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+        }
+    }
+
+    @Override public void mouseDragged(MouseEvent e) {
+        EastNorth eastNorth =
+            Main.map.mapView.getEastNorth(e.getX(),e.getY());
+        adjustingLayer.displace(
+                eastNorth.east()-prevEastNorth.east(),
+                eastNorth.north()-prevEastNorth.north()
+        );
+        prevEastNorth = eastNorth;
+        Main.map.mapView.repaint();
+    }
+
+    @Override public void mouseReleased(MouseEvent e) {
+        Main.map.mapView.repaint();
+        Main.map.mapView.setCursor(Cursor.getDefaultCursor());
+        prevEastNorth = null;
+    }
+
+    @Override
+    public void mouseEntered(MouseEvent e) {
+    }
+
+    @Override
+    public void mouseExited(MouseEvent e) {
+    }
+
+    @Override
+    public void mouseMoved(MouseEvent e) {
+    }
+
+    @Override public void mouseClicked(MouseEvent e) {
+    }
+
+    @Override public boolean layerIsSupported(Layer l) {
+        return hasLayersToAdjust();
+    }
+
+    /**
+     * the list cell renderer used to render layer list entries
+     *
+     */
+    static public class LayerListCellRenderer extends DefaultListCellRenderer {
+
+        protected boolean isActiveLayer(Layer layer) {
+            if (Main.map == null)
+                return false;
+            if (Main.map.mapView == null)
+                return false;
+            return Main.map.mapView.getActiveLayer() == layer;
+        }
+
+        @Override
+        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
+                boolean cellHasFocus) {
+            Layer layer = (Layer) value;
+            JLabel label = (JLabel) super.getListCellRendererComponent(list, layer.getName(), index, isSelected,
+                    cellHasFocus);
+            Icon icon = layer.getIcon();
+            label.setIcon(icon);
+            label.setToolTipText(layer.getToolTipText());
+            return label;
+        }
+    }
+
+    /**
+     * Prompts the user with a list of imagery layers which can be adjusted
+     *
+     * @param adjustableLayers the list of adjustable layers
+     * @return  the selected layer; null, if no layer was selected
+     */
+    protected Layer askAdjustLayer(List<? extends Layer> adjustableLayers) {
+        JComboBox layerList = new JComboBox();
+        layerList.setRenderer(new LayerListCellRenderer());
+        layerList.setModel(new DefaultComboBoxModel(adjustableLayers.toArray()));
+        layerList.setSelectedIndex(0);
+
+        JPanel pnl = new JPanel();
+        pnl.setLayout(new GridBagLayout());
+        pnl.add(new JLabel(tr("Please select the imagery layer to adjust.")), GBC.eol());
+        pnl.add(layerList, GBC.eol());
+
+        ExtendedDialog diag = new ExtendedDialog(
+                Main.parent,
+                tr("Select imagery layer"),
+                new String[] { tr("Start adjusting"),tr("Cancel") }
+        );
+        diag.setContent(pnl);
+        diag.setButtonIcons(new String[] { "mapmode/adjustwms", "cancel" });
+        diag.showDialog();
+        int decision = diag.getValue();
+        if (decision != 1)
+            return null;
+        Layer adjustLayer = (Layer) layerList.getSelectedItem();
+        return adjustLayer;
+    }
+
+    /**
+     * Displays a warning message if there are no imagery layers to adjust
+     *
+     */
+    protected void warnNoImageryLayers() {
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                tr("There are currently no imagery layer to adjust."),
+                tr("No layers to adjust"),
+                JOptionPane.WARNING_MESSAGE
+        );
+    }
+
+    /**
+     * Replies true if there is at least one WMS layer
+     *
+     * @return true if there is at least one WMS layer
+     */
+    protected boolean hasLayersToAdjust() {
+        if (Main.map == null) return false;
+        if (Main.map.mapView == null) return false;
+        return ! Main.map.mapView.getLayersOfType(ImageryLayer.class).isEmpty();
+    }
+
+    @Override
+    protected void updateEnabledState() {
+        setEnabled(hasLayersToAdjust());
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryInfo.java	(revision 24501)
@@ -0,0 +1,173 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Class that stores info about a WMS server.
+ *
+ * @author Frederik Ramm <frederik@remote.org>
+ */
+public class ImageryInfo implements Comparable<ImageryInfo> {
+    public enum ImageryType {
+        WMS("wms"),
+        TMS("tms"),
+        HTML("html"),
+        BING("bing");
+
+        private String urlString;
+        ImageryType(String urlString) {
+            this.urlString = urlString;
+        }
+        public String getUrlString() {
+            return urlString;
+        }
+    }
+
+    String name;
+    String url=null;
+    String cookies = null;
+    String eulaAcceptanceRequired = null;
+    ImageryType imageryType = ImageryType.WMS;
+    double pixelPerDegree = 0.0;
+
+    public ImageryInfo(String name) {
+        this.name=name;
+    }
+
+    public ImageryInfo(String name, String url) {
+        this.name=name;
+        setURL(url);
+    }
+
+    public ImageryInfo(String name, String url, String eulaAcceptanceRequired) {
+        this.name=name;
+        setURL(url);
+        this.eulaAcceptanceRequired = eulaAcceptanceRequired;
+    }
+
+    public ImageryInfo(String name, String url, String eulaAcceptanceRequired, String cookies) {
+        this.name=name;
+        setURL(url);
+        this.cookies=cookies;
+    }
+
+    public ImageryInfo(String name, String url, String cookies, double pixelPerDegree) {
+        this.name=name;
+        setURL(url);
+        this.cookies=cookies;
+        this.pixelPerDegree=pixelPerDegree;
+    }
+
+    public ArrayList<String> getInfoArray() {
+        String e2 = null;
+        String e3 = null;
+        String e4 = null;
+        if(url != null && !url.isEmpty()) e2 = getFullURL();
+        if(cookies != null && !cookies.isEmpty()) e3 = cookies;
+        if(pixelPerDegree != 0.0) e4 = String.valueOf(pixelPerDegree);
+        if(e4 != null && e3 == null) e3 = "";
+        if(e3 != null && e2 == null) e2 = "";
+
+        ArrayList<String> res = new ArrayList<String>();
+        res.add(name);
+        if(e2 != null) res.add(e2);
+        if(e3 != null) res.add(e3);
+        if(e4 != null) res.add(e4);
+        return res;
+    }
+
+    public ImageryInfo(Collection<String> list) {
+        ArrayList<String> array = new ArrayList<String>(list);
+        this.name=array.get(0);
+        if(array.size() >= 2) setURL(array.get(1));
+        if(array.size() >= 3) this.cookies=array.get(2);
+        if(array.size() >= 4) this.pixelPerDegree=Double.valueOf(array.get(3));
+    }
+
+    public ImageryInfo(ImageryInfo i) {
+        this.name=i.name;
+        this.url=i.url;
+        this.cookies=i.cookies;
+        this.imageryType=i.imageryType;
+        this.pixelPerDegree=i.pixelPerDegree;
+    }
+
+    @Override
+    public int compareTo(ImageryInfo in)
+    {
+        int i = name.compareTo(in.name);
+        if(i == 0)
+            i = url.compareTo(in.url);
+        if(i == 0)
+            i = Double.compare(pixelPerDegree, in.pixelPerDegree);
+        return i;
+    }
+
+    public boolean equalsBaseValues(ImageryInfo in)
+    {
+        return url.equals(in.url);
+    }
+
+    public void setPixelPerDegree(double ppd) {
+        this.pixelPerDegree = ppd;
+    }
+
+    public void setURL(String url) {
+        for (ImageryType type : ImageryType.values()) {
+            if (url.startsWith(type.getUrlString() + ":")) {
+                this.url = url.substring(type.getUrlString().length() + 1);
+                this.imageryType = type;
+                return;
+            }
+        }
+
+        // Default imagery type is WMS
+        this.url = url;
+        this.imageryType = ImageryType.WMS;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getURL() {
+        return this.url;
+    }
+
+    public String getCookies() {
+        return this.cookies;
+    }
+
+    public double getPixelPerDegree() {
+        return this.pixelPerDegree;
+    }
+
+    public String getFullURL() {
+        return imageryType.getUrlString() + ":" + url;
+    }
+
+    public String getToolbarName()
+    {
+        String res = name;
+        if(pixelPerDegree != 0.0)
+            res += "#PPD="+pixelPerDegree;
+        return res;
+    }
+
+    public String getMenuName()
+    {
+        String res = name;
+        if(pixelPerDegree != 0.0)
+            res += " ("+pixelPerDegree+")";
+        return res;
+    }
+
+    public ImageryType getImageryType() {
+        return imageryType;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayer.java	(revision 24501)
@@ -0,0 +1,74 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import java.awt.Toolkit;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.tms.TMSLayer;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSLayer;
+
+public abstract class ImageryLayer extends Layer {
+
+    protected MapView mv;
+
+    protected double dx = 0.0;
+    protected double dy = 0.0;
+
+    public ImageryLayer(String name) {
+        super(name);
+    }
+
+
+    public double getPPD(){
+        ProjectionBounds bounds = mv.getProjectionBounds();
+        return mv.getWidth() / (bounds.max.east() - bounds.min.east());
+    }
+
+    public void displace(double dx, double dy) {
+        this.dx += dx;
+        this.dy += dy;
+    }
+
+    public double getDx() {
+        return dx;
+    }
+
+    public double getDy() {
+        return dy;
+    }
+
+    protected static final Icon icon =
+        new ImageIcon(Toolkit.getDefaultToolkit().createImage(ImageryPlugin.class.getResource("/images/wms_small.png")));
+
+    @Override
+    public Icon getIcon() {
+        return icon;
+    }
+
+    @Override
+    public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override
+    public void mergeFrom(Layer from) {
+    }
+
+    @Override
+    public Object getInfoComponent() {
+        return getToolTipText();
+    }
+
+    public static ImageryLayer create(ImageryInfo info) {
+        if (info.imageryType == ImageryType.WMS || info.imageryType == ImageryType.HTML) {
+            return new WMSLayer(info);
+        } else if (info.imageryType == ImageryType.TMS || info.imageryType == ImageryType.BING) {
+            return new TMSLayer(info);
+        } else throw new AssertionError();
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryLayerInfo.java	(revision 24501)
@@ -0,0 +1,155 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.TreeSet;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.io.MirroredInputStream;
+
+public class ImageryLayerInfo {
+    ArrayList<ImageryInfo> layers = new ArrayList<ImageryInfo>();
+    ArrayList<ImageryInfo> defaultLayers = new ArrayList<ImageryInfo>();
+    private final static String[] DEFAULT_LAYER_SITES
+        = { "http://svn.openstreetmap.org/applications/editors/josm/plugins/imagery/sources.cfg"};
+
+    public void load() {
+        layers.clear();
+        Collection<String> defaults = Main.pref.getCollection(
+            "imagery.layers.default", Collections.<String>emptySet());
+        for(Collection<String> c : Main.pref.getArray("imagery.layers",
+        Collections.<Collection<String>>emptySet())) {
+            layers.add(new ImageryInfo(c));
+        }
+
+        { /* REMOVE following old block in spring 2011 */
+            defaults = new LinkedList<String>(defaults);
+            Map<String,String> prefs = Main.pref.getAllPrefix("imagery.layers.default.");
+            for(String s : prefs.keySet()) {
+                Main.pref.put(s, null);
+                defaults.add(s.substring(18));
+            }
+            prefs = Main.pref.getAllPrefix("imagery.layers.url.");
+            for(String s : prefs.keySet()) {
+                Main.pref.put(s, null);
+            }
+            TreeSet<String> keys = new TreeSet<String>(prefs.keySet());
+
+            // And then the names+urls of imagery layers
+            int prefid = 0;
+            String name = null;
+            String url = null;
+            String cookies = null;
+            double pixelPerDegree = 0.0;
+            int lastid = -1;
+            for (String key : keys) {
+                String[] elements = key.split("\\.");
+                if (elements.length != 4) continue;
+                try {
+                    prefid = Integer.parseInt(elements[2]);
+                } catch(NumberFormatException e) {
+                    continue;
+                }
+                if (prefid != lastid) {
+                    name = url = cookies = null; pixelPerDegree = 0.0; lastid = prefid;
+                }
+                if (elements[3].equals("name")) {
+                    name = prefs.get(key);
+                    int codeIndex = name.indexOf("#PPD=");
+                    if (codeIndex != -1) {
+                        pixelPerDegree = Double.valueOf(name.substring(codeIndex+5));
+                        name = name.substring(0, codeIndex);
+                    }
+                }
+                else if (elements[3].equals("url"))
+                {
+                    url = prefs.get(key);
+                }
+                else if (elements[3].equals("cookies"))
+                    cookies = prefs.get(key);
+                if (name != null && url != null)
+                    layers.add(new ImageryInfo(name, url, cookies, pixelPerDegree));
+            }
+        }
+        ArrayList<String> defaultsSave = new ArrayList<String>();
+        for(String source : Main.pref.getCollection("imagery.layers.sites", Arrays.asList(DEFAULT_LAYER_SITES)))
+        {
+            try
+            {
+                MirroredInputStream s = new MirroredInputStream(source, ImageryPlugin.instance.getPluginDir(), -1);
+                InputStreamReader r;
+                try
+                {
+                    r = new InputStreamReader(s, "UTF-8");
+                }
+                catch (UnsupportedEncodingException e)
+                {
+                    r = new InputStreamReader(s);
+                }
+                BufferedReader reader = new BufferedReader(r);
+                String line;
+                while((line = reader.readLine()) != null)
+                {
+                    String val[] = line.split(";");
+                    if(!line.startsWith("#") && (val.length == 3 || val.length == 4)) {
+                        boolean force = "true".equals(val[0]);
+                        String name = tr(val[1]);
+                        String url = val[2];
+                        String eulaAcceptanceRequired = null;
+                        if (val.length == 4) {
+                            // 4th parameter optional for license agreement (EULA)
+                            eulaAcceptanceRequired = val[3];
+                        }
+                        defaultLayers.add(new ImageryInfo(name, url, eulaAcceptanceRequired));
+
+                        if(force) {
+                            defaultsSave.add(url);
+                            if(!defaults.contains(url)) {
+                                for(ImageryInfo i : layers) {
+                                    if(url.equals(i.url))
+                                        force = false;
+                                }
+                                if(force)
+                                    layers.add(new ImageryInfo(name, url));
+                            }
+                        }
+                    }
+                }
+            }
+            catch (IOException e)
+            {
+            }
+        }
+
+        Main.pref.putCollection("imagery.layers.default", defaultsSave.size() > 0
+            ? defaultsSave : defaults);
+        Collections.sort(layers);
+        save();
+    }
+
+    public void add(ImageryInfo info) {
+        layers.add(info);
+    }
+
+    public void remove(ImageryInfo info) {
+        layers.remove(info);
+    }
+
+    public void save() {
+        LinkedList<Collection<String>> coll = new LinkedList<Collection<String>>();
+        for (ImageryInfo info : layers) {
+            coll.add(info.getInfoArray());
+        }
+        Main.pref.putArray("imagery.layers", coll);
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPlugin.java	(revision 24501)
@@ -0,0 +1,274 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.IconToggleButton;
+import org.openstreetmap.josm.gui.MainMenu;
+import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.plugins.Plugin;
+import org.openstreetmap.josm.plugins.PluginHandler;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.PluginProxy;
+import org.openstreetmap.josm.plugins.imagery.wms.Map_Rectifier_WMSmenuAction;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSAdapter;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSLayer;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSRemoteHandler;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerExporter;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerImporter;
+
+public class ImageryPlugin extends Plugin {
+
+    JMenu imageryJMenu;
+
+    public static ImageryPlugin instance;
+    public static WMSAdapter wmsAdapter = new WMSAdapter();
+
+    public ImageryLayerInfo info = new ImageryLayerInfo();
+
+    // remember state of menu item to restore on changed preferences
+    private boolean menuEnabled = false;
+
+    /***************************************************************
+     * Remote control initialization:
+     * If you need remote control in some other plug-in
+     * copy this stuff and the call to initRemoteControl below
+     * and replace the RequestHandler subclass in initRemoteControl
+     ***************************************************************/
+
+    /** name of remote control plugin */
+    private final String REMOTECONTROL_NAME = "remotecontrol";
+
+    /* if necessary change these version numbers to ensure compatibility */
+
+    /** RemoteControlPlugin older than this SVN revision is not compatible */
+    final int REMOTECONTROL_MIN_REVISION = 22734;
+    /** WMSPlugin needs this specific API major version of RemoteControlPlugin */
+    final int REMOTECONTROL_NEED_API_MAJOR = 1;
+    /** All API minor versions starting from this should be compatible */
+    final int REMOTECONTROL_MIN_API_MINOR = 0;
+
+    /* these fields will contain state and version of remote control plug-in */
+    boolean remoteControlAvailable = false;
+    boolean remoteControlCompatible = true;
+    boolean remoteControlInitialized = false;
+    int remoteControlRevision = 0;
+    int remoteControlApiMajor = 0;
+    int remoteControlApiMinor = 0;
+    int remoteControlProtocolMajor = 0;
+    int remoteControlProtocolMinor = 0;
+
+    /**
+     * Check if remote control plug-in is available and if its version is
+     * high enough and register remote control command for this plug-in.
+     */
+    private void initRemoteControl() {
+        for(PluginProxy pp: PluginHandler.pluginList)
+        {
+            PluginInformation info = pp.getPluginInformation();
+            if(REMOTECONTROL_NAME.equals(info.name))
+            {
+                remoteControlAvailable = true;
+                remoteControlRevision = Integer.parseInt(info.version);
+                if(REMOTECONTROL_MIN_REVISION > remoteControlRevision)
+                {
+                    remoteControlCompatible = false;
+                }
+            }
+        }
+
+        if(remoteControlAvailable && remoteControlCompatible)
+        {
+            Plugin plugin =
+                (Plugin) PluginHandler.getPlugin(REMOTECONTROL_NAME);
+            try {
+                Method method;
+                method = plugin.getClass().getMethod("getVersion");
+                Object obj = method.invoke(plugin);
+                if((obj != null ) && (obj instanceof int[]))
+                {
+                    int[] versions = (int[]) obj;
+                    if(versions.length >= 4)
+                    {
+                        remoteControlApiMajor = versions[0];
+                        remoteControlApiMinor = versions[1];
+                        remoteControlProtocolMajor = versions[2];
+                        remoteControlProtocolMinor = versions[3];
+                    }
+                }
+
+                if((remoteControlApiMajor != REMOTECONTROL_NEED_API_MAJOR) ||
+                        (remoteControlApiMinor < REMOTECONTROL_MIN_API_MINOR))
+                {
+                    remoteControlCompatible = false;
+                }
+                if(remoteControlCompatible)
+                {
+                    System.out.println(this.getClass().getSimpleName() + ": initializing remote control");
+                    method = plugin.getClass().getMethod("addRequestHandler", String.class, Class.class);
+                    // replace command and class when you copy this to some other plug-in
+                    // for compatibility with old remotecontrol add leading "/"
+                    method.invoke(plugin, "/" + WMSRemoteHandler.command, WMSRemoteHandler.class);
+                    remoteControlInitialized = true;
+                }
+            } catch (SecurityException e) {
+                e.printStackTrace();
+            } catch (NoSuchMethodException e) {
+                e.printStackTrace();
+            } catch (IllegalArgumentException e) {
+                e.printStackTrace();
+            } catch (IllegalAccessException e) {
+                e.printStackTrace();
+            } catch (InvocationTargetException e) {
+                e.printStackTrace();
+            }
+        }
+        if(remoteControlAvailable)
+        {
+            String msg = null;
+
+            if(remoteControlCompatible)
+            {
+                if(!remoteControlInitialized)
+                {
+                    msg  = tr("Could not initialize remote control.");
+                }
+            }
+            else
+            {
+                msg  = tr("Remote control plugin is not compatible with {0}.",
+                        this.getClass().getSimpleName());
+            }
+
+            if(msg != null)
+            {
+                String additionalMessage = tr("{0} will work but remote control for this plugin is disabled.\n"
+                        + "You should update the plugins.",
+                        this.getClass().getSimpleName());
+                String versionMessage = tr("Current version of \"{1}\": {2}, internal version {3}. "
+                        + "Need version {4}, internal version {5}.\n"
+                        + "If updating the plugins does not help report a bug for \"{0}\".",
+                        this.getClass().getSimpleName(),
+                        REMOTECONTROL_NAME,
+                        ""+remoteControlRevision,
+                        (remoteControlApiMajor != 0) ?
+                                ""+remoteControlApiMajor+"."+remoteControlApiMinor :
+                                    tr("unknown"),
+                                    ""+REMOTECONTROL_MIN_REVISION,
+                                    ""+REMOTECONTROL_NEED_API_MAJOR+"."+REMOTECONTROL_MIN_API_MINOR );
+
+                String title = tr("{0}: Problem with remote control",
+                        this.getClass().getSimpleName());
+
+                System.out.println(this.getClass().getSimpleName() + ": " +
+                        msg + "\n" + versionMessage);
+
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        msg + "\n" + additionalMessage,
+                        title,
+                        JOptionPane.WARNING_MESSAGE
+                );
+            }
+        }
+
+        if(!remoteControlAvailable) {
+            System.out.println(this.getClass().getSimpleName() + ": remote control not available");
+        }
+    }
+
+    /***************************************
+     * end of remote control initialization
+     ***************************************/
+
+    protected void initExporterAndImporter() {
+        ExtensionFileFilter.exporters.add(new WMSLayerExporter());
+        ExtensionFileFilter.importers.add(new WMSLayerImporter());
+    }
+
+    public ImageryPlugin(PluginInformation info) {
+        super(info);
+        instance = this;
+        this.info.load();
+        refreshMenu();
+        initRemoteControl();
+    }
+
+    public void addLayer(ImageryInfo info) {
+        this.info.add(info);
+        this.info.save();
+        refreshMenu();
+    }
+
+    public void refreshMenu() {
+        MainMenu menu = Main.main.menu;
+
+        if (imageryJMenu == null)
+            imageryJMenu = menu.addMenu(marktr("Imagery"), KeyEvent.VK_W, menu.defaultMenuPos, ht("/Plugin/Imagery"));
+        else
+            imageryJMenu.removeAll();
+
+        // for each configured WMSInfo, add a menu entry.
+        for (final ImageryInfo u : info.layers) {
+            imageryJMenu.add(new JMenuItem(new AddImageryLayerAction(u)));
+        }
+        imageryJMenu.addSeparator();
+        imageryJMenu.add(new JMenuItem(new Map_Rectifier_WMSmenuAction()));
+
+        imageryJMenu.addSeparator();
+        imageryJMenu.add(new JMenuItem(new
+                JosmAction(tr("Blank Layer"), "blankmenu", tr("Open a blank WMS layer to load data from a file"), null, false) {
+            @Override
+            public void actionPerformed(ActionEvent ev) {
+                Main.main.addLayer(new WMSLayer());
+            }
+        }));
+        setEnabledAll(menuEnabled);
+    }
+
+    private void setEnabledAll(boolean isEnabled) {
+        for(int i=0; i < imageryJMenu.getItemCount(); i++) {
+            JMenuItem item = imageryJMenu.getItem(i);
+
+            if(item != null) item.setEnabled(isEnabled);
+        }
+        menuEnabled = isEnabled;
+    }
+
+    @Override
+    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {
+        if (oldFrame==null && newFrame!=null) {
+            setEnabledAll(true);
+            Main.map.addMapMode(new IconToggleButton
+                    (new ImageryAdjustAction(Main.map)));
+        } else if (oldFrame!=null && newFrame==null ) {
+            setEnabledAll(false);
+        }
+    }
+
+    @Override
+    public PreferenceSetting getPreferenceSetting() {
+        return new ImageryPreferenceEditor();
+    }
+
+    @Override
+    public String getPluginDir()
+    {
+        return new File(Main.pref.getPluginsDirectory(), "imagery").getPath();
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/ImageryPreferenceEditor.java	(revision 24501)
@@ -0,0 +1,395 @@
+package org.openstreetmap.josm.plugins.imagery;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trc;
+
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridBagConstraints;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+import javax.swing.Box;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JEditorPane;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableColumnModel;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.plugins.imagery.wms.AddWMSLayerPanel;
+import org.openstreetmap.josm.plugins.imagery.wms.WMSAdapter;
+import org.openstreetmap.josm.tools.GBC;
+
+public class ImageryPreferenceEditor implements PreferenceSetting {
+    private ImageryLayerTableModel model;
+    private JComboBox browser;
+
+    JCheckBox overlapCheckBox;
+    JSpinner spinEast;
+    JSpinner spinNorth;
+    JSpinner spinSimConn;
+    JCheckBox remoteCheckBox;
+    boolean allowRemoteControl = true;
+    WMSAdapter wmsAdapter = ImageryPlugin.wmsAdapter;
+    ImageryPlugin plugin = ImageryPlugin.instance;
+
+    @Override
+    public void addGui(final PreferenceTabbedPane gui) {
+        JPanel p = gui.createPreferenceTab("wms", tr("Imagery Preferences"), tr("Modify list of imagery layers displayed in the Imagery menu"));
+
+        model = new ImageryLayerTableModel();
+        final JTable list = new JTable(model) {
+            @Override
+            public String getToolTipText(MouseEvent e) {
+                java.awt.Point p = e.getPoint();
+                return (String) model.getValueAt(rowAtPoint(p), columnAtPoint(p));
+            }
+        };
+        JScrollPane scroll = new JScrollPane(list);
+        p.add(scroll, GBC.eol().fill(GridBagConstraints.BOTH));
+        scroll.setPreferredSize(new Dimension(200, 200));
+
+        final ImageryDefaultLayerTableModel modeldef = new ImageryDefaultLayerTableModel();
+        final JTable listdef = new JTable(modeldef) {
+            @Override
+            public String getToolTipText(MouseEvent e) {
+                java.awt.Point p = e.getPoint();
+                return (String) modeldef.getValueAt(rowAtPoint(p), columnAtPoint(p));
+            }
+        };
+        JScrollPane scrolldef = new JScrollPane(listdef);
+        // scrolldef is added after the buttons so it's clearer the buttons
+        // control the top list and not the default one
+        scrolldef.setPreferredSize(new Dimension(200, 200));
+
+        TableColumnModel mod = listdef.getColumnModel();
+        mod.getColumn(1).setPreferredWidth(800);
+        mod.getColumn(0).setPreferredWidth(200);
+        mod = list.getColumnModel();
+        mod.getColumn(2).setPreferredWidth(50);
+        mod.getColumn(1).setPreferredWidth(800);
+        mod.getColumn(0).setPreferredWidth(200);
+
+        JPanel buttonPanel = new JPanel(new FlowLayout());
+
+        JButton add = new JButton(tr("Add"));
+        buttonPanel.add(add, GBC.std().insets(0, 5, 0, 0));
+        add.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                AddWMSLayerPanel p = new AddWMSLayerPanel();
+                int answer = JOptionPane.showConfirmDialog(
+                        gui, p,
+                        tr("Add Imagery URL"),
+                        JOptionPane.OK_CANCEL_OPTION);
+                if (answer == JOptionPane.OK_OPTION) {
+                    model.addRow(new ImageryInfo(p.getUrlName(), p.getUrl()));
+                }
+            }
+        });
+
+        JButton delete = new JButton(tr("Delete"));
+        buttonPanel.add(delete, GBC.std().insets(0, 5, 0, 0));
+        delete.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if (list.getSelectedRow() == -1)
+                    JOptionPane.showMessageDialog(gui, tr("Please select the row to delete."));
+                else {
+                    Integer i;
+                    while ((i = list.getSelectedRow()) != -1)
+                        model.removeRow(i);
+                }
+            }
+        });
+
+        JButton copy = new JButton(tr("Copy Selected Default(s)"));
+        buttonPanel.add(copy, GBC.std().insets(0, 5, 0, 0));
+        copy.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                int[] lines = listdef.getSelectedRows();
+                if (lines.length == 0) {
+                    JOptionPane.showMessageDialog(
+                            gui,
+                            tr("Please select at least one row to copy."),
+                            tr("Information"),
+                            JOptionPane.INFORMATION_MESSAGE);
+                    return;
+                }
+
+                outer: for (int i = 0; i < lines.length; i++) {
+                    ImageryInfo info = modeldef.getRow(lines[i]);
+
+                    // Check if an entry with exactly the same values already
+                    // exists
+                    for (int j = 0; j < model.getRowCount(); j++) {
+                        if (info.equalsBaseValues(model.getRow(j))) {
+                            // Select the already existing row so the user has
+                            // some feedback in case an entry exists
+                            list.getSelectionModel().setSelectionInterval(j, j);
+                            list.scrollRectToVisible(list.getCellRect(j, 0, true));
+                            continue outer;
+                        }
+                    }
+
+                    if (info.eulaAcceptanceRequired != null) {
+                        if (!confirmeEulaAcceptance(gui, info.eulaAcceptanceRequired))
+                            continue outer;
+                    }
+
+                    model.addRow(new ImageryInfo(info));
+                    int lastLine = model.getRowCount() - 1;
+                    list.getSelectionModel().setSelectionInterval(lastLine, lastLine);
+                    list.scrollRectToVisible(list.getCellRect(lastLine, 0, true));
+                }
+            }
+        });
+
+        p.add(buttonPanel);
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        // Add default item list
+        p.add(scrolldef, GBC.eol().insets(0, 5, 0, 0).fill(GridBagConstraints.BOTH));
+
+        browser = new JComboBox(new String[] {
+                "webkit-image {0}",
+                "gnome-web-photo --mode=photo --format=png {0} /dev/stdout",
+                "gnome-web-photo-fixed {0}",
+                "webkit-image-gtk {0}"});
+        browser.setEditable(true);
+        browser.setSelectedItem(Main.pref.get("wmsplugin.browser", "webkit-image {0}"));
+        p.add(new JLabel(tr("Downloader:")), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        p.add(browser);
+
+        // Overlap
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+
+        overlapCheckBox = new JCheckBox(tr("Overlap tiles"), wmsAdapter.PROP_OVERLAP.get());
+        JLabel labelEast = new JLabel(tr("% of east:"));
+        JLabel labelNorth = new JLabel(tr("% of north:"));
+        spinEast = new JSpinner(new SpinnerNumberModel(wmsAdapter.PROP_OVERLAP_EAST.get(), 1, 50, 1));
+        spinNorth = new JSpinner(new SpinnerNumberModel(wmsAdapter.PROP_OVERLAP_NORTH.get(), 1, 50, 1));
+
+        JPanel overlapPanel = new JPanel(new FlowLayout());
+        overlapPanel.add(overlapCheckBox);
+        overlapPanel.add(labelEast);
+        overlapPanel.add(spinEast);
+        overlapPanel.add(labelNorth);
+        overlapPanel.add(spinNorth);
+
+        p.add(overlapPanel);
+
+        // Simultaneous connections
+        p.add(Box.createHorizontalGlue(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+        JLabel labelSimConn = new JLabel(tr("Simultaneous connections"));
+        spinSimConn = new JSpinner(new SpinnerNumberModel(wmsAdapter.PROP_SIMULTANEOUS_CONNECTIONS.get(), 1, 30, 1));
+        JPanel overlapPanelSimConn = new JPanel(new FlowLayout());
+        overlapPanelSimConn.add(labelSimConn);
+        overlapPanelSimConn.add(spinSimConn);
+        p.add(overlapPanelSimConn, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+
+        allowRemoteControl = Main.pref.getBoolean("wmsplugin.remotecontrol", true);
+        remoteCheckBox = new JCheckBox(tr("Allow remote control (reqires remotecontrol plugin)"), allowRemoteControl);
+        JPanel remotePanel = new JPanel(new FlowLayout());
+        remotePanel.add(remoteCheckBox);
+
+        p.add(remotePanel);
+    }
+
+    @Override
+    public boolean ok() {
+        plugin.info.save();
+        plugin.refreshMenu();
+
+        wmsAdapter.PROP_OVERLAP.put(overlapCheckBox.getModel().isSelected());
+        wmsAdapter.PROP_OVERLAP_EAST.put((Integer) spinEast.getModel().getValue());
+        wmsAdapter.PROP_OVERLAP_NORTH.put((Integer) spinNorth.getModel().getValue());
+        wmsAdapter.PROP_SIMULTANEOUS_CONNECTIONS.put((Integer) spinSimConn.getModel().getValue());
+        allowRemoteControl = remoteCheckBox.getModel().isSelected();
+
+        Main.pref.put("wmsplugin.browser", browser.getEditor().getItem().toString());
+
+        Main.pref.put("wmsplugin.remotecontrol", String.valueOf(allowRemoteControl));
+        return false;
+    }
+
+    /**
+     * Updates a server URL in the preferences dialog. Used by other plugins.
+     *
+     * @param server
+     *            The server name
+     * @param url
+     *            The server URL
+     */
+    public void setServerUrl(String server, String url) {
+        for (int i = 0; i < model.getRowCount(); i++) {
+            if (server.equals(model.getValueAt(i, 0).toString())) {
+                model.setValueAt(url, i, 1);
+                return;
+            }
+        }
+        model.addRow(new String[] { server, url });
+    }
+
+    /**
+     * Gets a server URL in the preferences dialog. Used by other plugins.
+     *
+     * @param server
+     *            The server name
+     * @return The server URL
+     */
+    public String getServerUrl(String server) {
+        for (int i = 0; i < model.getRowCount(); i++) {
+            if (server.equals(model.getValueAt(i, 0).toString())) {
+                return model.getValueAt(i, 1).toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The table model for the WMS layer
+     *
+     */
+    class ImageryLayerTableModel extends DefaultTableModel {
+        public ImageryLayerTableModel() {
+            setColumnIdentifiers(new String[] { tr("Menu Name"), tr("Imagery URL"), trc("layer", "Zoom") });
+        }
+
+        public ImageryInfo getRow(int row) {
+            return plugin.info.layers.get(row);
+        }
+
+        public void addRow(ImageryInfo i) {
+            plugin.info.add(i);
+            int p = getRowCount() - 1;
+            fireTableRowsInserted(p, p);
+        }
+
+        @Override
+        public void removeRow(int i) {
+            plugin.info.remove(getRow(i));
+            fireTableRowsDeleted(i, i);
+        }
+
+        @Override
+        public int getRowCount() {
+            return plugin.info.layers.size();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            ImageryInfo info = plugin.info.layers.get(row);
+            switch (column) {
+            case 0:
+                return info.name;
+            case 1:
+                return info.getFullURL();
+            case 2:
+                return info.pixelPerDegree == 0.0 ? "" : info.pixelPerDegree;
+            }
+            return null;
+        }
+
+        @Override
+        public void setValueAt(Object o, int row, int column) {
+            ImageryInfo info = plugin.info.layers.get(row);
+            switch (column) {
+            case 0:
+                info.name = (String) o;
+            case 1:
+                info.setURL((String)o);
+            }
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return (column != 2);
+        }
+    }
+
+    /**
+     * The table model for the WMS layer
+     *
+     */
+    class ImageryDefaultLayerTableModel extends DefaultTableModel {
+        public ImageryDefaultLayerTableModel() {
+            setColumnIdentifiers(new String[] { tr("Menu Name (Default)"), tr("Imagery URL (Default)") });
+        }
+
+        public ImageryInfo getRow(int row) {
+            return plugin.info.defaultLayers.get(row);
+        }
+
+        @Override
+        public int getRowCount() {
+            return plugin.info.defaultLayers.size();
+        }
+
+        @Override
+        public Object getValueAt(int row, int column) {
+            ImageryInfo info = plugin.info.defaultLayers.get(row);
+            switch (column) {
+            case 0:
+                return info.name;
+            case 1:
+                return info.getFullURL();
+            }
+            return null;
+        }
+
+        @Override
+        public boolean isCellEditable(int row, int column) {
+            return false;
+        }
+    }
+
+    private boolean confirmeEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
+        URL url = null;
+        try {
+            url = new URL(eulaUrl.replaceAll("\\{lang\\}", Locale.getDefault().toString()));
+            JEditorPane htmlPane = null;
+            try {
+                htmlPane = new JEditorPane(url);
+            } catch (IOException e1) {
+                // give a second chance with a default Locale 'en'
+                try {
+                    url = new URL(eulaUrl.replaceAll("\\{lang\\}", "en"));
+                    htmlPane = new JEditorPane(url);
+                } catch (IOException e2) {
+                    JOptionPane.showMessageDialog(gui ,tr("EULA license URL not available: {0}", eulaUrl));
+                    return false;
+                }
+            }
+            Box box = Box.createVerticalBox();
+            htmlPane.setEditable(false);
+            JScrollPane scrollPane = new JScrollPane(htmlPane);
+            scrollPane.setPreferredSize(new Dimension(400, 400));
+            box.add(scrollPane);
+            int option = JOptionPane.showConfirmDialog(Main.parent, box, tr("Please abort if you are not sure"), JOptionPane.YES_NO_OPTION,
+                    JOptionPane.WARNING_MESSAGE);
+            if (option == JOptionPane.YES_OPTION) {
+                return true;
+            }
+        } catch (MalformedURLException e2) {
+            JOptionPane.showMessageDialog(gui ,tr("Malformed URL for the EULA licence: {0}", eulaUrl));
+        }
+        return false;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/BingAerialTileSource.java	(revision 24501)
@@ -0,0 +1,191 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import java.awt.Image;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.gui.jmapviewer.OsmTileSource;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+public class BingAerialTileSource extends OsmTileSource.AbstractOsmTileSource {
+    private static String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
+    private static List<Attribution> attributions;
+
+    public BingAerialTileSource() {
+        super("Bing Aerial Maps", "http://ecn.t2.tiles.virtualearth.net/tiles/");
+
+        attributions = loadAttributionText();
+        System.err.println("Added " + attributions.size() + " attributions.");
+    }
+
+    class Attribution {
+        String attribution;
+        int minZoom;
+        int maxZoom;
+        Bounds bounds;
+    }
+
+    class AttrHandler extends DefaultHandler {
+
+        private String string;
+        private Attribution curr;
+        private List<Attribution> attributions = new ArrayList<Attribution>();
+        private double southLat;
+        private double northLat;
+        private double eastLon;
+        private double westLon;
+        private boolean inCoverage = false;
+
+        @Override
+        public void startElement(String uri, String stripped, String tagName,
+                Attributes attrs) throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                curr = new Attribution();
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = true;
+            }
+        }
+
+        @Override
+        public void characters(char[] ch, int start, int length)
+                throws SAXException {
+            string = new String(ch, start, length);
+        }
+
+        @Override
+        public void endElement(String uri, String stripped, String tagName)
+                throws SAXException {
+            if("ImageryProvider".equals(tagName)) {
+                attributions.add(curr);
+            } else if("Attribution".equals(tagName)) {
+                curr.attribution = string;
+            } else if(inCoverage && "ZoomMin".equals(tagName)) {
+                curr.minZoom = Integer.parseInt(string);
+            } else if(inCoverage && "ZoomMax".equals(tagName)) {
+                curr.maxZoom = Integer.parseInt(string);
+            } else if(inCoverage && "SouthLatitude".equals(tagName)) {
+                southLat = Double.parseDouble(string);
+            } else if(inCoverage && "NorthLatitude".equals(tagName)) {
+                northLat = Double.parseDouble(string);
+            } else if(inCoverage && "EastLongitude".equals(tagName)) {
+                eastLon = Double.parseDouble(string);
+            } else if(inCoverage && "WestLongitude".equals(tagName)) {
+                westLon = Double.parseDouble(string);
+            } else if("BoundingBox".equals(tagName)) {
+                curr.bounds = new Bounds(northLat, westLon, southLat, eastLon);
+            } else if("CoverageArea".equals(tagName)) {
+                inCoverage = false;
+            }
+            string = "";
+        }
+    }
+
+    private List<Attribution> loadAttributionText() {
+        try {
+            URL u = new URL("http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key="+API_KEY+"&include=ImageryProviders&output=xml");
+            InputStream stream = u.openStream();
+            XMLReader parser = XMLReaderFactory.createXMLReader();
+            AttrHandler handler = new AttrHandler();
+            parser.setContentHandler(handler);
+            parser.parse(new InputSource(stream));
+            return handler.attributions;
+        } catch (IOException e) {
+            System.err.println("Could not open Bing aerials attribution metadata.");
+        } catch (SAXException e) {
+            System.err.println("Could not parse Bing aerials attribution metadata.");
+            e.printStackTrace();
+        }
+        return Collections.emptyList();
+    }
+
+    @Override
+    public int getMaxZoom() {
+        return 22;
+    }
+
+    @Override
+    public String getExtension() {
+        return("jpeg");
+    }
+
+    @Override
+    public String getTilePath(int zoom, int tilex, int tiley) {
+        String quadtree = computeQuadTree(zoom, tilex, tiley);
+        return "/tiles/a" + quadtree + "." + getExtension() + "?g=587";
+    }
+
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+
+    @Override
+    public boolean requiresAttribution() {
+        return true;
+    }
+
+    @Override
+    public Image getAttributionImage() {
+        return ImageProvider.get("bing_maps").getImage();
+    }
+
+    @Override
+    public String getAttributionLinkURL() {
+        //return "http://bing.com/maps"
+        // FIXME: I've set attributionLinkURL temporarily to ToU URL to comply with bing ToU
+        // (the requirement is that we have such a link at the bottom of the window)
+        return "http://go.microsoft.com/?linkid=9710837";
+    }
+
+    @Override
+    public String getTermsOfUseURL() {
+        return "http://opengeodata.org/microsoft-imagery-details";
+    }
+
+    @Override
+    public String getAttributionText(int zoom, LatLon topLeft, LatLon botRight) {
+        Bounds windowBounds = new Bounds(topLeft, botRight);
+        StringBuilder a = new StringBuilder();
+        for (Attribution attr : attributions) {
+            Bounds attrBounds = attr.bounds;
+            if(zoom <= attr.maxZoom && zoom >= attr.minZoom) {
+                if(windowBounds.getMin().lon() < attrBounds.getMax().lon()
+                        && windowBounds.getMax().lon() > attrBounds.getMin().lon()
+                        && windowBounds.getMax().lat() < attrBounds.getMin().lat()
+                        && windowBounds.getMin().lat() > attrBounds.getMax().lat()) {
+                    a.append(attr.attribution);
+                    a.append(" ");
+                }
+            }
+        }
+        return a.toString();
+    }
+
+    static String computeQuadTree(int zoom, int tilex, int tiley) {
+        StringBuilder k = new StringBuilder();
+        for(int i = zoom; i > 0; i--) {
+            char digit = 48;
+            int mask = 1 << (i - 1);
+            if ((tilex & mask) != 0) {
+                digit += 1;
+            }
+            if ((tiley & mask) != 0) {
+                digit += 2;
+            }
+            k.append(digit);
+        }
+        return k.toString();
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSKey.java	(revision 24501)
@@ -0,0 +1,78 @@
+/**
+ *
+ */
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+/**
+ * <p>
+ * Key for map tile. Key have just X and Y value. It have overriden {@link #hashCode()},
+ * {@link #equals(Object)} and also {@link #toString()}.
+ * </p>
+ *
+ * @author LuVar <lubomir.varga@freemap.sk>
+ * @author Dave Hansen <dave@sr71.net>
+ *
+ */
+public class TMSKey {
+    private final int x;
+    private final int y;
+    private final int level;
+
+    /**
+     * <p>
+     * Constructs key for hashmaps for some tile describedy by X and Y position. X and Y are tiles
+     * positions on discrete map.
+     * </p>
+     *
+     * @param x x position in tiles table
+     * @param y y position in tiles table
+     */
+    public final boolean valid;
+    public TMSKey(int x, int y, int level) {
+        this.x = x;
+        this.y = y;
+        this.level = level;
+        if (level <= 0 || x < 0 || y < 0) {
+            this.valid = false;
+            System.err.println("invalid TMSKey("+level+", "+x+", "+y+")");
+        } else {
+            this.valid = true;
+        }
+    }
+
+    /**
+     * <p>
+     * Returns true ONLY if x and y are equals.
+     * </p>
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof TMSKey) {
+            TMSKey smk = (TMSKey) obj;
+            if((smk.x == this.x) && (smk.y == this.y) && (smk.level == this.level)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return  return new Integer(this.x + this.y * 10000).hashCode();
+     * @see java.lang.Object#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return new Integer(this.x + this.y * 10000 + this.level * 100000).hashCode();
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "TMSKey(x=" + this.x + ",y=" + this.y + ",level=" + level + ")";
+    }
+
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSLayer.java	(revision 24501)
@@ -0,0 +1,1073 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.ImageObserver;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.SwingUtilities;
+
+import org.openstreetmap.gui.jmapviewer.JobDispatcher;
+import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
+import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
+import org.openstreetmap.gui.jmapviewer.Tile;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.RenameLayerAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+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.gui.layer.Layer;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.ImageryLayer;
+
+/**
+ * Class that displays a slippy map layer.
+ *
+ * @author Frederik Ramm <frederik@remote.org>
+ * @author LuVar <lubomir.varga@freemap.sk>
+ * @author Dave Hansen <dave@sr71.net>
+ *
+ */
+public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
+    boolean debug = false;
+    void out(String s)
+    {
+        Main.debug(s);
+    }
+
+    protected MemoryTileCache tileCache;
+    protected TileSource tileSource;
+    protected TileLoader tileLoader;
+    JobDispatcher jobDispatcher = JobDispatcher.getInstance();
+
+    HashSet<Tile> tileRequestsOutstanding = new HashSet<Tile>();
+    @Override
+    public synchronized void tileLoadingFinished(Tile tile, boolean success)
+    {
+        tile.setLoaded(true);
+        needRedraw = true;
+        Main.map.repaint(100);
+        tileRequestsOutstanding.remove(tile);
+        if (debug)
+            out("tileLoadingFinished() tile: " + tile + " success: " + success);
+    }
+    @Override
+    public TileCache getTileCache()
+    {
+        return tileCache;
+    }
+    void clearTileCache()
+    {
+        if (debug)
+            out("clearing tile storage");
+        tileCache = new MemoryTileCache();
+        tileCache.setCacheSize(200);
+    }
+
+    /**
+     * Actual zoom lvl. Initial zoom lvl is set to
+     */
+    public int currentZoomLevel;
+
+    LatLon lastTopLeft;
+    LatLon lastBotRight;
+    private Image bufferImage;
+    private Tile clickedTile;
+    private boolean needRedraw;
+    private JPopupMenu tileOptionMenu;
+    JCheckBoxMenuItem autoZoomPopup;
+    Tile showMetadataTile;
+    private Image attrImage;
+    private String attrTermsUrl;
+    private Rectangle attrImageBounds, attrToUBounds;
+    private static Font ATTR_FONT = Font.decode("Arial 10");
+    private static Font ATTR_LINK_FONT = Font.decode("Arial Underline 10");
+
+    protected boolean autoZoom = true;
+    protected boolean autoLoad = true;
+
+    void redraw()
+    {
+        needRedraw = true;
+        Main.map.repaint();
+    }
+
+    private void setTileStorage(TileSource tileSource)
+    {
+        int origZoom = currentZoomLevel;
+        this.tileSource = tileSource;
+        boolean requireAttr = tileSource.requiresAttribution();
+        if(requireAttr) {
+            attrImage = tileSource.getAttributionImage();
+            if(attrImage == null) {
+                System.out.println("Attribution image was null.");
+            } else {
+                System.out.println("Got an attribution image " + attrImage.getHeight(this) + "x" + attrImage.getWidth(this));
+            }
+
+            attrTermsUrl = tileSource.getTermsOfUseURL();
+        }
+
+        // The minimum should also take care of integer parsing
+        // errors which would leave us with a zoom of -1 otherwise
+        if (tileSource.getMaxZoom() < currentZoomLevel)
+            currentZoomLevel = tileSource.getMaxZoom();
+        if (tileSource.getMinZoom() > currentZoomLevel)
+            currentZoomLevel = tileSource.getMinZoom();
+        if (currentZoomLevel != origZoom) {
+            out("changed currentZoomLevel loading new tile store from " + origZoom + " to " + currentZoomLevel);
+            out("tileSource.getMinZoom(): " + tileSource.getMinZoom());
+            out("tileSource.getMaxZoom(): " + tileSource.getMaxZoom());
+        }
+        clearTileCache();
+        //tileloader = new OsmTileLoader(this);
+        tileLoader = new OsmFileCacheTileLoader(this);
+    }
+
+    @Override
+    public void displace(double dx, double dy) {
+        super.displace(dx, dy);
+        needRedraw = true;
+    }
+
+    @SuppressWarnings("serial")
+    public TMSLayer(ImageryInfo info) {
+        super(info.getName());
+
+        setBackgroundLayer(true);
+        this.setVisible(true);
+
+        currentZoomLevel = 0; //FIXME: detect current zoom level
+        if (info.getImageryType() == ImageryType.TMS) {
+            setTileStorage(new TMSTileSource(info.getName(),info.getURL()));
+        } else if (info.getImageryType() == ImageryType.BING) {
+            setTileStorage(new BingAerialTileSource());
+        } else throw new AssertionError();
+
+        tileOptionMenu = new JPopupMenu();
+
+        autoZoomPopup = new JCheckBoxMenuItem();
+        autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                autoZoom = !autoZoom;
+            }
+        });
+        autoZoomPopup.setSelected(autoZoom);
+        tileOptionMenu.add(autoZoomPopup);
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                if (clickedTile != null) {
+                    loadTile(clickedTile);
+                    redraw();
+                }
+            }
+        }));
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Show Tile Info")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                out("info tile: " + clickedTile);
+                if (clickedTile != null) {
+                    showMetadataTile = clickedTile;
+                    redraw();
+                }
+            }
+        }));
+
+        /* FIXME
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Request Update")) {
+            public void actionPerformed(ActionEvent ae) {
+                if (clickedTile != null) {
+                    clickedTile.requestUpdate();
+                    redraw();
+                }
+            }
+        }));*/
+
+        tileOptionMenu.add(new JMenuItem(new AbstractAction(
+                tr("Load All Tiles")) {
+            @Override
+            public void actionPerformed(ActionEvent ae) {
+                loadAllTiles(true);
+                redraw();
+            }
+        }));
+
+        // increase and decrease commands
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Increase zoom")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        increaseZoomLevel();
+                        redraw();
+                    }
+                }));
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Decrease zoom")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        decreaseZoomLevel();
+                        redraw();
+                    }
+                }));
+
+        // FIXME: currently ran in errors
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Snap to tile size")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        if (lastImageScale == null) {
+                            out("please wait for a tile to be loaded before snapping");
+                            return;
+                        }
+                        double new_factor = Math.sqrt(lastImageScale);
+                        if (debug)
+                            out("tile snap: scale was: " + lastImageScale + ", new factor: " + new_factor);
+                        Main.map.mapView.zoomToFactor(new_factor);
+                        redraw();
+                    }
+                }));
+        // end of adding menu commands
+
+        tileOptionMenu.add(new JMenuItem(
+                new AbstractAction(tr("Flush Tile Cache")) {
+                    @Override
+                    public void actionPerformed(ActionEvent ae) {
+                        System.out.print("flushing all tiles...");
+                        clearTileCache();
+                        System.out.println("done");
+                    }
+                }));
+        // end of adding menu commands
+
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                Main.map.mapView.addMouseListener(new MouseAdapter() {
+                    @Override
+                    public void mouseClicked(MouseEvent e) {
+                        if (e.getButton() == MouseEvent.BUTTON3) {
+                            clickedTile = getTileForPixelpos(e.getX(), e.getY());
+                            tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
+                        } else if (e.getButton() == MouseEvent.BUTTON1) {
+                            if(!tileSource.requiresAttribution()) {
+                                return;
+                            }
+
+                            if(attrImageBounds.contains(e.getPoint())) {
+                                try {
+                                    java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
+                                    desktop.browse(new URI(tileSource.getAttributionLinkURL()));
+                                } catch (IOException e1) {
+                                    e1.printStackTrace();
+                                } catch (URISyntaxException e1) {
+                                    e1.printStackTrace();
+                                }
+                            } else if(attrToUBounds.contains(e.getPoint())) {
+                                try {
+                                    java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
+                                    desktop.browse(new URI(tileSource.getTermsOfUseURL()));
+                                } catch (IOException e1) {
+                                    e1.printStackTrace();
+                                } catch (URISyntaxException e1) {
+                                    e1.printStackTrace();
+                                }
+                            }
+                        }
+                    }
+                });
+
+                MapView.addLayerChangeListener(new LayerChangeListener() {
+                    @Override
+                    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
+                        //
+                    }
+
+                    @Override
+                    public void layerAdded(Layer newLayer) {
+                        //
+                    }
+
+                    @Override
+                    public void layerRemoved(Layer oldLayer) {
+                        MapView.removeLayerChangeListener(this);
+                    }
+                });
+            }
+        });
+    }
+
+    void zoomChanged()
+    {
+        if (debug)
+            out("zoomChanged(): " + currentZoomLevel);
+        needRedraw = true;
+        jobDispatcher.cancelOutstandingJobs();
+        tileRequestsOutstanding.clear();
+    }
+
+    int getMaxZoomLvl()
+    {
+        return tileSource.getMaxZoom();
+    }
+
+    int getMinZoomLvl()
+    {
+        return tileSource.getMinZoom();
+    }
+
+    /**
+     * Zoom in, go closer to map.
+     *
+     * @return    true, if zoom increasing was successfull, false othervise
+     */
+    public boolean zoomIncreaseAllowed()
+    {
+        boolean zia = currentZoomLevel < this.getMaxZoomLvl();
+        if (debug)
+            out("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
+        return zia;
+    }
+    public boolean increaseZoomLevel()
+    {
+        lastImageScale = null;
+        if (zoomIncreaseAllowed()) {
+            currentZoomLevel++;
+            if (debug)
+                out("increasing zoom level to: " + currentZoomLevel);
+            zoomChanged();
+        } else {
+            System.err.println("current zoom lvl ("+currentZoomLevel+") couldnt be increased. "+
+                             "MaxZoomLvl ("+this.getMaxZoomLvl()+") reached.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Zoom out from map.
+     *
+     * @return    true, if zoom increasing was successfull, false othervise
+     */
+    public boolean zoomDecreaseAllowed()
+    {
+        return currentZoomLevel > this.getMinZoomLvl();
+    }
+    public boolean decreaseZoomLevel() {
+        int minZoom = this.getMinZoomLvl();
+        lastImageScale = null;
+        if (zoomDecreaseAllowed()) {
+            if (debug)
+                out("decreasing zoom level to: " + currentZoomLevel);
+            currentZoomLevel--;
+            zoomChanged();
+        } else {
+            System.err.println("current zoom lvl couldnt be decreased. MinZoomLvl("+minZoom+") reached.");
+            return false;
+        }
+        return true;
+    }
+
+    /*
+     * We use these for quick, hackish calculations.  They
+     * are temporary only and intentionally not inserted
+     * into the tileCache.
+     */
+    synchronized Tile tempCornerTile(Tile t) {
+        int x = t.getXtile() + 1;
+        int y = t.getYtile() + 1;
+        int zoom = t.getZoom();
+        Tile tile = getTile(x, y, zoom);
+        if (tile != null)
+            return tile;
+        return new Tile(tileSource, x, y, zoom);
+    }
+    synchronized Tile getOrCreateTile(int x, int y, int zoom) {
+        Tile tile = getTile(x, y, zoom);
+        if (tile == null) {
+            tile = new Tile(tileSource, x, y, zoom);
+            tileCache.addTile(tile);
+            tile.loadPlaceholderFromCache(tileCache);
+        }
+        return tile;
+    }
+
+    /*
+     * This can and will return null for tiles that are not
+     * already in the cache.
+     */
+    synchronized Tile getTile(int x, int y, int zoom) {
+        int max = (1 << zoom);
+        if (x < 0 || x >= max || y < 0 || y >= max)
+                return null;
+        Tile tile = tileCache.getTile(tileSource, x, y, zoom);
+        return tile;
+    }
+
+    synchronized boolean loadTile(Tile tile)
+    {
+        if (tile == null)
+            return false;
+        if (tile.hasError())
+            return false;
+        if (tile.isLoaded())
+            return false;
+        if (tile.isLoading())
+            return false;
+        if (tileRequestsOutstanding.contains(tile))
+            return false;
+        tileRequestsOutstanding.add(tile);
+        jobDispatcher.addJob(tileLoader.createTileLoaderJob(tileSource,
+                             tile.getXtile(), tile.getYtile(), tile.getZoom()));
+        return true;
+    }
+
+    void loadAllTiles(boolean force) {
+        MapView mv = Main.map.mapView;
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+
+        TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
+
+        // if there is more than 18 tiles on screen in any direction, do not
+        // load all tiles!
+        if (ts.tooLarge()) {
+            System.out.println("Not downloading all tiles because there is more than 18 tiles on an axis!");
+            return;
+        }
+        ts.loadAllTiles(force);
+    }
+
+    /*
+     * Attempt to approximate how much the image is being scaled. For instance,
+     * a 100x100 image being scaled to 50x50 would return 0.25.
+     */
+    Image lastScaledImage = null;
+    @Override
+    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
+        boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
+        needRedraw = true;
+        if (debug)
+            out("imageUpdate() done: " + done + " calling repaint");
+        Main.map.repaint(done ? 0 : 100);
+        return !done;
+    }
+    boolean imageLoaded(Image i) {
+        if (i == null)
+            return false;
+        int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
+        if ((status & ALLBITS) != 0)
+            return true;
+        return false;
+    }
+    Image getLoadedTileImage(Tile tile)
+    {
+        if (!tile.isLoaded())
+            return null;
+        Image img = tile.getImage();
+        if (!imageLoaded(img))
+            return null;
+        return img;
+    }
+
+    double getImageScaling(Image img, Rectangle r) {
+        int realWidth = -1;
+        int realHeight = -1;
+        if (img != null) {
+            realWidth = img.getHeight(this);
+            realWidth = img.getWidth(this);
+        }
+        if (realWidth == -1 || realHeight == -1) {
+            /*
+             * We need a good image against which to work. If
+             * the current one isn't loaded, then try the last one.
+             * Should be good enough. If we've never seen one, then
+             * guess.
+             */
+            if (lastScaledImage != null) {
+                return getImageScaling(lastScaledImage, r);
+            }
+            realWidth = 256;
+            realHeight = 256;
+        } else {
+            lastScaledImage = img;
+        }
+        /*
+         * If the zoom scale gets really, really off, these can get into
+         * the millions, so make this a double to prevent integer
+         * overflows.
+         */
+        double drawWidth = r.width;
+        double drawHeight = r.height;
+        // stem.out.println("drawWidth: " + drawWidth + " drawHeight: " +
+        // drawHeight);
+
+        double drawArea = drawWidth * drawHeight;
+        double realArea = realWidth * realHeight;
+
+        return drawArea / realArea;
+    }
+
+    LatLon tileLatLon(Tile t)
+    {
+        int zoom = t.getZoom();
+        return new LatLon(tileYToLat(t.getYtile(), zoom),
+                          tileXToLon(t.getXtile(), zoom));
+    }
+
+    int paintFromOtherZooms(Graphics g, Tile topLeftTile, Tile botRightTile)
+    {
+        LatLon topLeft  = tileLatLon(topLeftTile);
+        LatLon botRight = tileLatLon(botRightTile);
+
+
+        /*
+         * Go looking for tiles in zoom levels *other* than the current
+         * one. Even if they might look bad, they look better than a
+         * blank tile.
+         *
+         * Make darn sure that the tilesCache can either hold all of
+         * these "fake" tiles or that they don't get inserted in it to
+         * begin with.
+         */
+        //int otherZooms[] = {-5, -4, -3, 2, -2, 1, -1};
+        int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
+        int painted = 0;
+        debug = true;
+        for (int zoomOff : otherZooms) {
+            int zoom = currentZoomLevel + zoomOff;
+            if ((zoom < this.getMinZoomLvl()) ||
+                (zoom > this.getMaxZoomLvl())) {
+                continue;
+            }
+            TileSet ts = new TileSet(topLeft, botRight, zoom);
+            int zoom_painted = 0;
+            this.paintTileImages(g, ts, zoom, null);
+            if (debug && zoom_painted > 0)
+                out("painted " + zoom_painted + "/"+ ts.size() +
+                    " tiles from zoom("+zoomOff+"): " + zoom);
+            painted += zoom_painted;
+            if (zoom_painted >= ts.size()) {
+                if (debug)
+                    out("broke after drawing " + zoom_painted + "/"+ ts.size() + " at zoomOff: " + zoomOff);
+                break;
+            }
+        }
+        debug = false;
+        return painted;
+    }
+    Rectangle tileToRect(Tile t1)
+    {
+        /*
+         * We need to get a box in which to draw, so advance by one tile in
+         * each direction to find the other corner of the box.
+         * Note: this somewhat pollutes the tile cache
+         */
+        Tile t2 = tempCornerTile(t1);
+        Rectangle rect = new Rectangle(pixelPos(t1));
+        rect.add(pixelPos(t2));
+        return rect;
+    }
+
+    // 'source' is the pixel coordinates for the area that
+    // the img is capable of filling in.  However, we probably
+    // only want a portion of it.
+    //
+    // 'border' is the screen cordinates that need to be drawn.
+    //  We must not draw outside of it.
+    void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border)
+    {
+        Rectangle target = source;
+
+        // If a border is specified, only draw the intersection
+        // if what we have combined with what we are supposed
+        // to draw.
+        if (border != null) {
+            target = source.intersection(border);
+            if (debug)
+                out("source: " + source + "\nborder: " + border + "\nintersection: " + target);
+        }
+
+        // All of the rectangles are in screen coordinates.  We need
+        // to how these correlate to the sourceImg pixels.  We could
+        // avoid doing this by scaling the image up to the 'source' size,
+        // but this should be cheaper.
+        //
+        // In some projections, x any y are scaled differently enough to
+        // cause a pixel or two of fudge.  Calculate them separately.
+        double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
+        double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
+
+        // How many pixels into the 'source' rectangle are we drawing?
+        int screen_x_offset = target.x - source.x;
+        int screen_y_offset = target.y - source.y;
+        // And how many pixels into the image itself does that
+        // correlate to?
+        int img_x_offset = (int)(screen_x_offset * imageXScaling);
+        int img_y_offset = (int)(screen_y_offset * imageYScaling);
+        // Now calculate the other corner of the image that we need
+        // by scaling the 'target' rectangle's dimensions.
+        int img_x_end   = img_x_offset + (int)(target.getWidth() * imageXScaling);
+        int img_y_end   = img_y_offset + (int)(target.getHeight() * imageYScaling);
+
+        if (debug) {
+            out("drawing image into target rect: " + target);
+        }
+        g.drawImage(sourceImg,
+                        target.x, target.y,
+                        target.x + target.width, target.y + target.height,
+                        img_x_offset, img_y_offset,
+                        img_x_end, img_y_end,
+                        this);
+    }
+    Double lastImageScale = null;
+    // This function is called for several zoom levels, not just
+    // the current one.  It should not trigger any tiles to be
+    // downloaded.  It should also avoid polluting the tile cache
+    // with any tiles since these tiles are not mandatory.
+    //
+    // The "border" tile tells us the boundaries of where we may
+    // draw.  It will not be from the zoom level that is being
+    // drawn currently.  If drawing the currentZoomLevel,
+    // border is null and we draw the entire tile set.
+    List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
+        Rectangle borderRect = null;
+        if (border != null)
+            borderRect = tileToRect(border);
+        List<Tile> missedTiles = new LinkedList<Tile>();
+        boolean imageScaleRecorded = false;
+        for (Tile tile : ts.allTiles()) {
+            Image img = getLoadedTileImage(tile);
+            if (img == null) {
+                if (debug)
+                    out("missed tile: " + tile);
+                missedTiles.add(tile);
+                continue;
+            }
+            Rectangle sourceRect = tileToRect(tile);
+            if (borderRect != null && !sourceRect.intersects(borderRect))
+                continue;
+            drawImageInside(g, img, sourceRect, borderRect);
+            if (!imageScaleRecorded && zoom == currentZoomLevel) {
+                lastImageScale = new Double(getImageScaling(img, sourceRect));
+                imageScaleRecorded = true;
+            }
+        }// end of for
+        return missedTiles;
+    }
+
+    void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
+        int fontHeight = g.getFontMetrics().getHeight();
+        if (tile == null)
+            return;
+        Point p = pixelPos(t);
+        int texty = p.y + 2 + fontHeight;
+
+        if (tile == showMetadataTile) {
+            String md = tile.toString();
+            if (md != null) {
+                g.drawString(md, p.x + 2, texty);
+                texty += 1 + fontHeight;
+            }
+        }
+
+        String tileStatus = tile.getStatus();
+        if (!tile.isLoaded()) {
+            g.drawString(tr("image " + tileStatus), p.x + 2, texty);
+            texty += 1 + fontHeight;
+        }
+    }
+
+    public Point pixelPos(LatLon ll) {
+        return Main.map.mapView.getPoint(Main.proj.latlon2eastNorth(ll).add(getDx(), getDy()));
+    }
+    public Point pixelPos(Tile t) {
+        double lon = tileXToLon(t.getXtile(), t.getZoom());
+        LatLon tmpLL = new LatLon(tileYToLat(t.getYtile(), t.getZoom()), lon);
+        return pixelPos(tmpLL);
+    }
+    private class TileSet {
+        int z12x0, z12x1, z12y0, z12y1;
+        int zoom;
+        int tileMax = -1;
+
+        TileSet(LatLon topLeft, LatLon botRight, int zoom) {
+            this.zoom = zoom;
+            z12x0 = lonToTileX(topLeft.lon(),  zoom);
+            z12y0 = latToTileY(topLeft.lat(),  zoom);
+            z12x1 = lonToTileX(botRight.lon(), zoom);
+            z12y1 = latToTileY(botRight.lat(), zoom);
+            if (z12x0 > z12x1) {
+                int tmp = z12x0;
+                z12x0 = z12x1;
+                z12x1 = tmp;
+            }
+            if (z12y0 > z12y1) {
+                int tmp = z12y0;
+                z12y0 = z12y1;
+                z12y1 = tmp;
+            }
+            tileMax = (int)Math.pow(2.0, zoom);
+            if (z12x0 < 0) z12x0 = 0;
+            if (z12y0 < 0) z12y0 = 0;
+            if (z12x1 > tileMax) z12x1 = tileMax;
+            if (z12y1 > tileMax) z12y1 = tileMax;
+        }
+        boolean tooSmall() {
+            return this.tilesSpanned() < 2.1;
+        }
+        boolean tooLarge() {
+            return this.tilesSpanned() > 10;
+        }
+        boolean insane() {
+            return this.tilesSpanned() > 100;
+        }
+        double tilesSpanned() {
+            return Math.sqrt(1.0 * this.size());
+        }
+
+        double size() {
+            double x_span = z12x1 - z12x0 + 1.0;
+            double y_span = z12y1 - z12y0 + 1.0;
+            return x_span * y_span;
+        }
+
+        /*
+         * Get all tiles represented by this TileSet that are
+         * already in the tileCache.
+         */
+        List<Tile> allTiles()
+        {
+            return this.allTiles(false);
+        }
+        private List<Tile> allTiles(boolean create)
+        {
+            List<Tile> ret = new ArrayList<Tile>();
+            // Don't even try to iterate over the set.
+            // Someone created a crazy number of them
+            if (this.insane())
+                return ret;
+            for (int x = z12x0; x <= z12x1; x++) {
+                for (int y = z12y0; y <= z12y1; y++) {
+                    Tile t;
+                    if (create)
+                        t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
+                    else
+                        t = getTile(x % tileMax, y % tileMax, zoom);
+                    if (t != null)
+                        ret.add(t);
+                }
+            }
+            return ret;
+        }
+        void loadAllTiles(boolean force)
+        {
+            List<Tile> tiles = this.allTiles(true);
+            boolean autoload = TMSLayer.this.autoLoad;
+            if (!autoload && !force)
+               return;
+            int nr_queued = 0;
+            for (Tile t : tiles) {
+                if (loadTile(t))
+                    nr_queued++;
+            }
+            if (debug)
+                if (nr_queued > 0)
+                    out("queued to load: " + nr_queued + "/" + tiles.size() + " tiles at zoom: " + zoom);
+        }
+    }
+
+    boolean az_disable = false;
+    boolean autoZoomEnabled()
+    {
+        if (az_disable)
+            return false;
+        return autoZoom;
+    }
+    /**
+     */
+    @Override
+    public void paint(Graphics2D g, MapView mv, Bounds bounds) {
+        //long start = System.currentTimeMillis();
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        Graphics2D oldg = g;
+
+        if (botRight.lon() == 0.0 || botRight.lat() == 0) {
+            Main.debug("still initializing??");
+            // probably still initializing
+            return;
+        }
+
+        if (lastTopLeft != null && lastBotRight != null && topLeft.equalsEpsilon(lastTopLeft)
+                && botRight.equalsEpsilon(lastBotRight) && bufferImage != null
+                && mv.getWidth() == bufferImage.getWidth(null) && mv.getHeight() == bufferImage.getHeight(null)
+                && !needRedraw) {
+
+            if (debug)
+                out("drawing buffered image");
+            g.drawImage(bufferImage, 0, 0, null);
+            return;
+        }
+
+        needRedraw = false;
+        lastTopLeft = topLeft;
+        lastBotRight = botRight;
+        bufferImage = mv.createImage(mv.getWidth(), mv.getHeight());
+        g = (Graphics2D) bufferImage.getGraphics();
+
+        int zoom = currentZoomLevel;
+        TileSet ts = new TileSet(topLeft, botRight, zoom);
+
+        if (autoZoomEnabled()) {
+            if (zoomDecreaseAllowed() && ts.tooLarge()) {
+                if (debug)
+                    out("too many tiles, decreasing zoom from " + currentZoomLevel);
+                if (decreaseZoomLevel())
+                    this.paint(oldg, mv, bounds);
+                return;
+            }
+            if (zoomIncreaseAllowed() && ts.tooSmall()) {
+                if (debug)
+                    out("too zoomed in, (" + ts.tilesSpanned()
+                        + "), increasing zoom from " + currentZoomLevel);
+                // This is a hack.  ts.tooSmall() is proabably a bad thing, and this works
+                // around it.  If we have a very small window, the tileSet may be well
+                // less than 1 real tile wide, but that's expected.  But, this sees the
+                // tile set as too small and zooms in.  The code below that checks for
+                // pixel stretching disagrees and tries to zoom out.  Both calls recurse,
+                // hillarity ensues, and the stack overflows.
+                //
+                // This really needs to get fixed properly.  We probably shouldn't even
+                // have the tooSmall() check on tileSets.  But, this also helps the zoom
+                // converge to the correct place much faster.
+                boolean tmp = az_disable;
+                az_disable = true;
+                if (increaseZoomLevel())
+                     this.paint(oldg, mv, bounds);
+                az_disable = tmp;
+                return;
+            }
+        }
+
+        // Too many tiles... refuse to draw
+        if (!ts.tooLarge()) {
+            //out("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
+            ts.loadAllTiles(false);
+        }
+
+        g.setColor(Color.DARK_GRAY);
+
+        List<Tile> missedTiles = this.paintTileImages(g, ts, currentZoomLevel, null);
+        int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
+        for (int zoomOffset : otherZooms) {
+            if (!autoZoomEnabled())
+                break;
+            if (!autoLoad)
+                break;
+            int newzoom = currentZoomLevel + zoomOffset;
+            if (missedTiles.size() <= 0)
+                break;
+            List<Tile> newlyMissedTiles = new LinkedList<Tile>();
+            for (Tile missed : missedTiles) {
+                Tile t2 = tempCornerTile(missed);
+                LatLon topLeft2  = tileLatLon(missed);
+                LatLon botRight2 = tileLatLon(t2);
+                TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
+                if (ts2.tooLarge())
+                    continue;
+                newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
+            }
+            missedTiles = newlyMissedTiles;
+        }
+        if (debug && missedTiles.size() > 0) {
+            out("still missed "+missedTiles.size()+" in the end");
+        }
+        g.setColor(Color.red);
+
+        // The current zoom tileset is guaranteed to have all of
+        // its tiles
+        for (Tile t : ts.allTiles()) {
+            this.paintTileText(ts, t, g, mv, currentZoomLevel, t);
+        }
+
+        if (tileSource.requiresAttribution()) {
+            // Draw attribution
+            g.setColor(Color.white);
+            Font font = g.getFont();
+            g.setFont(ATTR_LINK_FONT);
+
+            // Draw terms of use text
+            Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
+            int textHeight = (int) termsStringBounds.getHeight() - 5;
+            int textWidth = (int) termsStringBounds.getWidth();
+            int termsTextY = mv.getHeight() - textHeight;
+            if(attrTermsUrl != null) {
+                int x = 2;
+                int y = mv.getHeight() - textHeight;
+                attrToUBounds = new Rectangle(x, y, textWidth, textHeight);
+                g.drawString("Background Terms of Use", x, y);
+            }
+
+            // Draw attribution logo
+            int imgWidth = attrImage.getWidth(this);
+            if(attrImage != null) {
+                int x = 2;
+                int height = attrImage.getHeight(this);
+                int y = termsTextY - height;
+                attrImageBounds = new Rectangle(x, y, imgWidth, height);
+                g.drawImage(attrImage, x, y, this);
+            }
+
+            String attributionText = tileSource.getAttributionText(currentZoomLevel, topLeft, botRight);
+            Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
+            g.drawString(attributionText, mv.getWidth() - (int) stringBounds.getWidth(), mv.getHeight() - textHeight);
+
+            g.setFont(font);
+        }
+
+        oldg.drawImage(bufferImage, 0, 0, null);
+
+        if (autoZoomEnabled() && lastImageScale != null) {
+            // If each source image pixel is being stretched into > 3
+            // drawn pixels, zoom in... getting too pixelated
+            if (lastImageScale > 3 && zoomIncreaseAllowed()) {
+                if (debug)
+                    out("autozoom increase: scale: " + lastImageScale);
+                increaseZoomLevel();
+                this.paint(oldg, mv, bounds);
+            // If each source image pixel is being squished into > 0.32
+            // of a drawn pixels, zoom out.
+            } else if ((lastImageScale < 0.45) && (lastImageScale > 0) && zoomDecreaseAllowed()) {
+                if (debug)
+                    out("autozoom decrease: scale: " + lastImageScale);
+                decreaseZoomLevel();
+                this.paint(oldg, mv, bounds);
+            }
+        }
+        //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
+        oldg.setColor(Color.black);
+        if (ts.insane()) {
+            oldg.drawString("zoom in to load any tiles", 120, 120);
+        } else if (ts.tooLarge()) {
+            oldg.drawString("zoom in to load more tiles", 120, 120);
+        } else if (ts.tooSmall()) {
+            oldg.drawString("increase zoom level to see more detail", 120, 120);
+        }
+    }// end of paint method
+
+    /**
+     * This isn't very efficient, but it is only used when the
+     * user right-clicks on the map.
+     */
+    Tile getTileForPixelpos(int px, int py) {
+        if (debug)
+            out("getTileForPixelpos("+px+", "+py+")");
+        MapView mv = Main.map.mapView;
+        Point clicked = new Point(px, py);
+        LatLon topLeft = mv.getLatLon(0, 0);
+        LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
+        int z = currentZoomLevel;
+        TileSet ts = new TileSet(topLeft, botRight, z);
+
+        if (!ts.tooLarge())
+            ts.loadAllTiles(false); // make sure there are tile objects for all tiles
+        Tile clickedTile = null;
+        for (Tile t1 : ts.allTiles()) {
+            Tile t2 = tempCornerTile(t1);
+            Rectangle r = new Rectangle(pixelPos(t1));
+            r.add(pixelPos(t2));
+            if (debug)
+                out("r: " + r + " clicked: " + clicked);
+            if (!r.contains(clicked))
+                continue;
+            clickedTile  = t1;
+            break;
+        }
+        if (clickedTile == null)
+             return null;
+        System.out.println("clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
+                           " scale: " + lastImageScale + " currentZoomLevel: " + currentZoomLevel);
+        return clickedTile;
+    }
+
+    @Override
+    public Action[] getMenuEntries() {
+        return new Action[] {
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                // color,
+                new RenameLayerAction(this.getAssociatedFile(), this),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this) };
+    }
+
+    @Override
+    public String getToolTipText() {
+        return null;
+    }
+
+    @Override
+    public void visitBoundingBox(BoundingXYVisitor v) {
+    }
+
+    private int latToTileY(double lat, int zoom) {
+        double l = lat / 180 * Math.PI;
+        double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
+        return (int) (Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI);
+    }
+
+    private int lonToTileX(double lon, int zoom) {
+        return (int) (Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0);
+    }
+
+    private double tileYToLat(int y, int zoom) {
+        return Math.atan(Math.sinh(Math.PI
+                - (Math.PI * y / Math.pow(2.0, zoom - 1))))
+                * 180 / Math.PI;
+    }
+
+    private double tileXToLon(int x, int zoom) {
+        return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/tms/TMSTileSource.java	(revision 24501)
@@ -0,0 +1,13 @@
+package org.openstreetmap.josm.plugins.imagery.tms;
+
+import org.openstreetmap.gui.jmapviewer.OsmTileSource;
+
+public class TMSTileSource extends OsmTileSource.AbstractOsmTileSource {
+    public TMSTileSource(String name, String url) {
+        super(name, url);
+    }
+    @Override
+    public TileUpdate getTileUpdate() {
+        return TileUpdate.IfNoneMatch;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/AddWMSLayerPanel.java	(revision 24501)
@@ -0,0 +1,530 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.HeadlessException;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.JTree;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.MutableTreeNode;
+import javax.swing.tree.TreePath;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.data.projection.ProjectionSubPrefs;
+import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
+import org.openstreetmap.josm.tools.GBC;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+
+public class AddWMSLayerPanel extends JPanel {
+    private List<LayerDetails> selectedLayers;
+    private URL serviceUrl;
+    private LayerDetails selectedLayer;
+
+    private JTextField menuName;
+    private JTextArea resultingLayerField;
+    private MutableTreeNode treeRootNode;
+    private DefaultTreeModel treeData;
+    private JTree layerTree;
+    private JButton showBoundsButton;
+
+    private boolean previouslyShownUnsupportedCrsError = false;
+
+    public AddWMSLayerPanel() {
+        JPanel wmsFetchPanel = new JPanel(new GridBagLayout());
+        menuName = new JTextField(40);
+        menuName.setText(tr("Unnamed WMS Layer"));
+        final JTextArea serviceUrl = new JTextArea(3, 40);
+        serviceUrl.setLineWrap(true);
+        serviceUrl.setText("http://sample.com/wms?");
+        wmsFetchPanel.add(new JLabel(tr("Menu Name")), GBC.std().insets(0,0,5,0));
+        wmsFetchPanel.add(menuName, GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+        wmsFetchPanel.add(new JLabel(tr("Service URL")), GBC.std().insets(0,0,5,0));
+        JScrollPane scrollPane = new JScrollPane(serviceUrl,
+                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
+                JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        wmsFetchPanel.add(scrollPane, GBC.eop().insets(5,0,0,0));
+        JButton getLayersButton = new JButton(tr("Get Layers"));
+        getLayersButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                Cursor beforeCursor = getCursor();
+                try {
+                    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+                    attemptGetCapabilities(serviceUrl.getText());
+                } finally {
+                    setCursor(beforeCursor);
+                }
+            }
+        });
+        wmsFetchPanel.add(getLayersButton, GBC.eop().anchor(GridBagConstraints.EAST));
+
+        treeRootNode = new DefaultMutableTreeNode();
+        treeData = new DefaultTreeModel(treeRootNode);
+        layerTree = new JTree(treeData);
+        layerTree.setCellRenderer(new LayerTreeCellRenderer());
+        layerTree.addTreeSelectionListener(new TreeSelectionListener() {
+
+            @Override
+            public void valueChanged(TreeSelectionEvent e) {
+                TreePath[] selectionRows = layerTree.getSelectionPaths();
+                if(selectionRows == null) {
+                    showBoundsButton.setEnabled(false);
+                    selectedLayer = null;
+                    return;
+                }
+
+                selectedLayers = new LinkedList<LayerDetails>();
+                for (TreePath i : selectionRows) {
+                    Object userObject = ((DefaultMutableTreeNode) i.getLastPathComponent()).getUserObject();
+                    if(userObject instanceof LayerDetails) {
+                        LayerDetails detail = (LayerDetails) userObject;
+                        if(!detail.isSupported()) {
+                            layerTree.removeSelectionPath(i);
+                            if(!previouslyShownUnsupportedCrsError) {
+                                JOptionPane.showMessageDialog(null, tr("That layer does not support any of JOSM's projections,\n" +
+                                "so you can not use it. This message will not show again."),
+                                tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+                                previouslyShownUnsupportedCrsError = true;
+                            }
+                        } else if(detail.ident != null) {
+                            selectedLayers.add(detail);
+                        }
+                    }
+                }
+
+                if (!selectedLayers.isEmpty()) {
+                    resultingLayerField.setText(buildGetMapUrl());
+
+                    if(selectedLayers.size() == 1) {
+                        showBoundsButton.setEnabled(true);
+                        selectedLayer = selectedLayers.get(0);
+                    }
+                } else {
+                    showBoundsButton.setEnabled(false);
+                    selectedLayer = null;
+                }
+            }
+        });
+        wmsFetchPanel.add(new JScrollPane(layerTree), GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+
+        JPanel layerManipulationButtons = new JPanel();
+        showBoundsButton = new JButton(tr("Show Bounds"));
+        showBoundsButton.setEnabled(false);
+        showBoundsButton.addActionListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if(selectedLayer.bounds != null) {
+                    SlippyMapBBoxChooser mapPanel = new SlippyMapBBoxChooser();
+                    mapPanel.setBoundingBox(selectedLayer.bounds);
+                    JOptionPane.showMessageDialog(null, mapPanel, tr("Show Bounds"), JOptionPane.PLAIN_MESSAGE);
+                } else {
+                    JOptionPane.showMessageDialog(null, tr("No bounding box was found for this layer."),
+                            tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+                }
+            }
+        });
+        layerManipulationButtons.add(showBoundsButton);
+
+        wmsFetchPanel.add(layerManipulationButtons, GBC.eol().insets(0,0,5,0));
+        wmsFetchPanel.add(new JLabel(tr("WMS URL")), GBC.std().insets(0,0,5,0));
+        resultingLayerField = new JTextArea(3, 40);
+        resultingLayerField.setLineWrap(true);
+        wmsFetchPanel.add(new JScrollPane(resultingLayerField, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), GBC.eop().insets(5,0,0,0).fill(GridBagConstraints.HORIZONTAL));
+
+        add(wmsFetchPanel);
+    }
+
+    private String buildRootUrl() {
+        StringBuilder a = new StringBuilder(serviceUrl.getProtocol());
+        a.append("://");
+        a.append(serviceUrl.getHost());
+        if(serviceUrl.getPort() != -1) {
+            a.append(":");
+            a.append(serviceUrl.getPort());
+        }
+        a.append(serviceUrl.getPath());
+        a.append("?");
+        if(serviceUrl.getQuery() != null) {
+            a.append(serviceUrl.getQuery());
+            if (!serviceUrl.getQuery().isEmpty() && !serviceUrl.getQuery().endsWith("&")) {
+                a.append("&");
+            }
+        }
+        return a.toString();
+    }
+
+    private String buildGetMapUrl() {
+        StringBuilder a = new StringBuilder();
+        a.append(buildRootUrl());
+        a.append("FORMAT=image/jpeg&VERSION=1.1.1&SERVICE=WMS&REQUEST=GetMap&Layers=");
+        a.append(commaSepLayerList());
+        a.append("&");
+
+        return a.toString();
+    }
+
+    private String commaSepLayerList() {
+        StringBuilder b = new StringBuilder();
+
+        Iterator<LayerDetails> iterator = selectedLayers.iterator();
+        while (iterator.hasNext()) {
+            LayerDetails layerDetails = iterator.next();
+            b.append(layerDetails.ident);
+            if(iterator.hasNext()) {
+                b.append(",");
+            }
+        }
+
+        return b.toString();
+    }
+
+    private void showError(String incomingData, Exception e) {
+        JOptionPane.showMessageDialog(this, tr("Could not parse WMS layer list."),
+                tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+        System.err.println("Could not parse WMS layer list. Incoming data:");
+        System.err.println(incomingData);
+        e.printStackTrace();
+    }
+
+    private void attemptGetCapabilities(String serviceUrlStr) {
+        URL getCapabilitiesUrl = null;
+        try {
+            if (!serviceUrlStr.trim().contains("capabilities")) {
+                // If the url doesn't already have GetCapabilities, add it in
+                getCapabilitiesUrl = new URL(serviceUrlStr + "VERSION=1.1.1&SERVICE=WMS&REQUEST=GetCapabilities");
+            } else {
+                // Otherwise assume it's a good URL and let the subsequent error
+                // handling systems deal with problems
+                getCapabilitiesUrl = new URL(serviceUrlStr);
+            }
+            serviceUrl = new URL(serviceUrlStr);
+        } catch (HeadlessException e) {
+            return;
+        } catch (MalformedURLException e) {
+            JOptionPane.showMessageDialog(this, tr("Invalid service URL."),
+                    tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        String incomingData;
+        try {
+            URLConnection openConnection = getCapabilitiesUrl.openConnection();
+            InputStream inputStream = openConnection.getInputStream();
+            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
+            String line;
+            StringBuilder ba = new StringBuilder();
+            while((line = br.readLine()) != null) {
+                ba.append(line);
+                ba.append("\n");
+            }
+            incomingData = ba.toString();
+        } catch (IOException e) {
+            JOptionPane.showMessageDialog(this, tr("Could not retrieve WMS layer list."),
+                    tr("WMS Error"), JOptionPane.ERROR_MESSAGE);
+            return;
+        }
+
+        Document document;
+        try {
+            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+            builderFactory.setValidating(false);
+            builderFactory.setNamespaceAware(true);
+            DocumentBuilder builder = builderFactory.newDocumentBuilder();
+            builder.setEntityResolver(new EntityResolver() {
+                @Override
+                public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
+                    System.out.println("Ignoring DTD " + publicId + ", " + systemId);
+                    return new InputSource(new StringReader(""));
+                }
+            });
+            document = builder.parse(new InputSource(new StringReader(incomingData)));
+        } catch (ParserConfigurationException e) {
+            showError(incomingData, e);
+            return;
+        } catch (SAXException e) {
+            showError(incomingData, e);
+            return;
+        } catch (IOException e) {
+            showError(incomingData, e);
+            return;
+        }
+
+        // Some WMS service URLs specify a different base URL for their GetMap service
+        Element child = getChild(document.getDocumentElement(), "Capability");
+        child = getChild(child, "Request");
+        child = getChild(child, "GetMap");
+        child = getChild(child, "DCPType");
+        child = getChild(child, "HTTP");
+        child = getChild(child, "Get");
+        child = getChild(child, "OnlineResource");
+        if (child != null) {
+            String baseURL = child.getAttribute("xlink:href");
+            if(baseURL != null) {
+                try {
+                    System.out.println("GetCapabilities specifies a different service URL: " + baseURL);
+                    serviceUrl = new URL(baseURL);
+                } catch (MalformedURLException e1) {
+                }
+            }
+        }
+
+        try {
+            treeRootNode.setUserObject(getCapabilitiesUrl.getHost());
+            Element capabilityElem = getChild(document.getDocumentElement(), "Capability");
+            List<Element> children = getChildren(capabilityElem, "Layer");
+            List<LayerDetails> layers = parseLayers(children, new HashSet<String>());
+            updateTreeList(layers);
+        } catch(Exception e) {
+            showError(incomingData, e);
+            return;
+        }
+    }
+
+    private void updateTreeList(List<LayerDetails> layers) {
+        addLayersToTreeData(treeRootNode, layers);
+        layerTree.expandRow(0);
+    }
+
+    private void addLayersToTreeData(MutableTreeNode parent, List<LayerDetails> layers) {
+        for (LayerDetails layerDetails : layers) {
+            DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(layerDetails);
+            addLayersToTreeData(treeNode, layerDetails.children);
+            treeData.insertNodeInto(treeNode, parent, 0);
+        }
+    }
+
+    private List<LayerDetails> parseLayers(List<Element> children, Set<String> parentCrs) {
+        List<LayerDetails> details = new LinkedList<LayerDetails>();
+        for (Element element : children) {
+            details.add(parseLayer(element, parentCrs));
+        }
+        return details;
+    }
+
+    private LayerDetails parseLayer(Element element, Set<String> parentCrs) {
+        String name = getChildContent(element, "Title", null, null);
+        String ident = getChildContent(element, "Name", null, null);
+
+        // The set of supported CRS/SRS for this layer
+        Set<String> crsList = new HashSet<String>();
+        // ...including this layer's already-parsed parent projections
+        crsList.addAll(parentCrs);
+
+        // Parse the CRS/SRS pulled out of this layer's XML element
+        // I think CRS and SRS are the same at this point
+        List<Element> crsChildren = getChildren(element, "CRS");
+        crsChildren.addAll(getChildren(element, "SRS"));
+        for (Element child : crsChildren) {
+            String crs = (String) getContent(child);
+            if(crs != null) {
+                String upperCase = crs.trim().toUpperCase();
+                crsList.add(upperCase);
+            }
+        }
+
+        // Check to see if any of the specified projections are supported by JOSM
+        boolean josmSupportsThisLayer = false;
+        for (String crs : crsList) {
+            josmSupportsThisLayer |= isProjSupported(crs);
+        }
+
+        Bounds bounds = null;
+        Element bboxElem = getChild(element, "EX_GeographicBoundingBox");
+        if(bboxElem != null) {
+            // Attempt to use EX_GeographicBoundingBox for bounding box
+            double left = Double.parseDouble(getChildContent(bboxElem, "westBoundLongitude", null, null));
+            double top = Double.parseDouble(getChildContent(bboxElem, "northBoundLatitude", null, null));
+            double right = Double.parseDouble(getChildContent(bboxElem, "eastBoundLongitude", null, null));
+            double bot = Double.parseDouble(getChildContent(bboxElem, "southBoundLatitude", null, null));
+            bounds = new Bounds(bot, left, top, right);
+        } else {
+            // If that's not available, try LatLonBoundingBox
+            bboxElem = getChild(element, "LatLonBoundingBox");
+            if(bboxElem != null) {
+                double left = Double.parseDouble(bboxElem.getAttribute("minx"));
+                double top = Double.parseDouble(bboxElem.getAttribute("maxy"));
+                double right = Double.parseDouble(bboxElem.getAttribute("maxx"));
+                double bot = Double.parseDouble(bboxElem.getAttribute("miny"));
+                bounds = new Bounds(bot, left, top, right);
+            }
+        }
+
+        List<Element> layerChildren = getChildren(element, "Layer");
+        List<LayerDetails> childLayers = parseLayers(layerChildren, crsList);
+
+        return new LayerDetails(name, ident, crsList, josmSupportsThisLayer, bounds, childLayers);
+    }
+
+    private boolean isProjSupported(String crs) {
+        for (Projection proj : Projection.allProjections) {
+            if (proj instanceof ProjectionSubPrefs) {
+                if (((ProjectionSubPrefs) proj).getPreferencesFromCode(crs) == null) {
+                    return true;
+                }
+            } else {
+                if (proj.toCode().equals(crs)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public String getUrlName() {
+        return menuName.getText();
+    }
+
+    public String getUrl() {
+        return resultingLayerField.getText();
+    }
+
+    public static void main(String[] args) {
+        JFrame f = new JFrame("Test");
+        f.setContentPane(new AddWMSLayerPanel());
+        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        f.pack();
+        f.setVisible(true);
+    }
+
+    private static String getChildContent(Element parent, String name, String missing, String empty) {
+        Element child = getChild(parent, name);
+        if (child == null) {
+            return missing;
+        } else {
+            String content = (String) getContent(child);
+            return (content != null) ? content : empty;
+        }
+    }
+
+    private static Object getContent(Element element) {
+        NodeList nl = element.getChildNodes();
+        StringBuffer content = new StringBuffer();
+        for (int i = 0; i < nl.getLength(); i++) {
+            Node node = nl.item(i);
+            switch (node.getNodeType()) {
+            case Node.ELEMENT_NODE:
+                return node;
+            case Node.CDATA_SECTION_NODE:
+            case Node.TEXT_NODE:
+                content.append(node.getNodeValue());
+                break;
+            }
+        }
+        return content.toString().trim();
+    }
+
+    private static List<Element> getChildren(Element parent, String name) {
+        List<Element> retVal = new LinkedList<Element>();
+        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
+            if (child instanceof Element && name.equals(child.getNodeName())) {
+                retVal.add((Element) child);
+            }
+        }
+        return retVal;
+    }
+
+    private static Element getChild(Element parent, String name) {
+        if (parent == null) {
+            return null;
+        }
+        for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
+            if (child instanceof Element && name.equals(child.getNodeName())) {
+                return (Element) child;
+            }
+        }
+        return null;
+    }
+
+    class LayerDetails {
+
+        private String name;
+        private String ident;
+        private List<LayerDetails> children;
+        private Bounds bounds;
+        private boolean supported;
+
+        public LayerDetails(String name, String ident, Set<String> crsList,
+                boolean supportedLayer, Bounds bounds,
+                List<LayerDetails> childLayers) {
+            this.name = name;
+            this.ident = ident;
+            this.supported = supportedLayer;
+            this.children = childLayers;
+            this.bounds = bounds;
+        }
+
+        public boolean isSupported() {
+            return this.supported;
+        }
+
+        @Override
+        public String toString() {
+            if(this.name == null || this.name.isEmpty()) {
+                return this.ident;
+            } else {
+                return this.name;
+            }
+        }
+
+    }
+
+    class LayerTreeCellRenderer extends DefaultTreeCellRenderer {
+        @Override
+        public Component getTreeCellRendererComponent(JTree tree, Object value,
+                boolean sel, boolean expanded, boolean leaf, int row,
+                boolean hasFocus) {
+            super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf,
+                    row, hasFocus);
+            DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
+            Object userObject = treeNode.getUserObject();
+            if (userObject instanceof LayerDetails) {
+                LayerDetails layer = (LayerDetails) userObject;
+                setEnabled(layer.isSupported());
+            }
+            return this;
+        }
+    }
+
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/GeorefImage.java	(revision 24501)
@@ -0,0 +1,223 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.Transparency;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.ref.SoftReference;
+
+import javax.imageio.ImageIO;
+
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.NavigatableComponent;
+
+public class GeorefImage implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    public enum State { IMAGE, NOT_IN_CACHE, FAILED};
+
+    private WMSLayer layer;
+    private State state;
+
+    private BufferedImage image;
+    private SoftReference<BufferedImage> reImg;
+    private int xIndex;
+    private int yIndex;
+
+
+    public EastNorth getMin() {
+        return layer.getEastNorth(xIndex, yIndex);
+    }
+
+    public EastNorth getMax() {
+        return layer.getEastNorth(xIndex+1, yIndex+1);
+    }
+
+
+    public GeorefImage(WMSLayer layer) {
+        this.layer = layer;
+    }
+
+    public void changePosition(int xIndex, int yIndex) {
+        if (!equalPosition(xIndex, yIndex)) {
+            this.xIndex = xIndex;
+            this.yIndex = yIndex;
+            this.image = null;
+            flushedResizedCachedInstance();
+        }
+    }
+
+    public boolean equalPosition(int xIndex, int yIndex) {
+        return this.xIndex == xIndex && this.yIndex == yIndex;
+    }
+
+    public void changeImage(State state, BufferedImage image) {
+        flushedResizedCachedInstance();
+        this.image = image;
+        this.state = state;
+
+        switch (state) {
+        case FAILED:
+        {
+            BufferedImage img = createImage();
+            Graphics g = img.getGraphics();
+            g.setColor(Color.RED);
+            g.fillRect(0, 0, img.getWidth(), img.getHeight());
+            g.setFont(g.getFont().deriveFont(Font.PLAIN).deriveFont(36.0f));
+            g.setColor(Color.BLACK);
+            g.drawString(tr("Exception occurred"), 10, img.getHeight()/2);
+            this.image = img;
+            break;
+        }
+        case NOT_IN_CACHE:
+        {
+            BufferedImage img = createImage();
+            Graphics g = img.getGraphics();
+            g.setColor(Color.GRAY);
+            g.fillRect(0, 0, img.getWidth(), img.getHeight());
+            Font font = g.getFont();
+            Font tempFont = font.deriveFont(Font.PLAIN).deriveFont(36.0f);
+            g.setFont(tempFont);
+            g.setColor(Color.BLACK);
+            g.drawString(tr("Not in cache"), 10, img.getHeight()/2);
+            g.setFont(font);
+            this.image = img;
+            break;
+        }
+        default:
+            break;
+        }
+    }
+
+    private BufferedImage createImage() {
+        return new BufferedImage(layer.getBaseImageWidth(), layer.getBaseImageHeight(), BufferedImage.TYPE_INT_RGB);
+    }
+
+    public boolean paint(Graphics g, NavigatableComponent nc, int xIndex, int yIndex, int leftEdge, int bottomEdge) {
+        if (image == null)
+            return false;
+
+        if(!(this.xIndex == xIndex && this.yIndex == yIndex)){
+            return false;
+        }
+
+        int left = layer.getImageX(xIndex);
+        int bottom = layer.getImageY(yIndex);
+        int width = layer.getImageWidth(xIndex);
+        int height = layer.getImageHeight(yIndex);
+
+        int x = left - leftEdge;
+        int y = nc.getHeight() - (bottom - bottomEdge) - height;
+
+        // This happens if you zoom outside the world
+        if(width == 0 || height == 0)
+            return false;
+
+        BufferedImage img = reImg == null?null:reImg.get();
+        if(img != null && img.getWidth() == width && img.getHeight() == height) {
+            g.drawImage(img, x, y, null);
+            return true;
+        }
+
+        boolean alphaChannel = WMSLayer.PROP_ALPHA_CHANNEL.get() && getImage().getTransparency() != Transparency.OPAQUE;
+
+        try {
+            if(img != null) img.flush();
+            long freeMem = Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory();
+            //System.out.println("Free Memory:           "+ (freeMem/1024/1024) +" MB");
+            // Notice that this value can get negative due to integer overflows
+            //System.out.println("Img Size:              "+ (width*height*3/1024/1024) +" MB");
+
+            int multipl = alphaChannel ? 4 : 3;
+            // This happens when requesting images while zoomed out and then zooming in
+            // Storing images this large in memory will certainly hang up JOSM. Luckily
+            // traditional rendering is as fast at these zoom levels, so it's no loss.
+            // Also prevent caching if we're out of memory soon
+            if(width > 2000 || height > 2000 || width*height*multipl > freeMem) {
+                fallbackDraw(g, getImage(), x, y, width, height);
+            } else {
+                // We haven't got a saved resized copy, so resize and cache it
+                img = new BufferedImage(width, height, alphaChannel?BufferedImage.TYPE_INT_ARGB:BufferedImage.TYPE_3BYTE_BGR);
+                img.getGraphics().drawImage(getImage(),
+                        0, 0, width, height, // dest
+                        0, 0, getImage().getWidth(null), getImage().getHeight(null), // src
+                        null);
+                img.getGraphics().dispose();
+                g.drawImage(img, x, y, null);
+                reImg = new SoftReference<BufferedImage>(img);
+            }
+        } catch(Exception e) {
+            fallbackDraw(g, getImage(), x, y, width, height);
+        }
+        return true;
+    }
+
+    private void fallbackDraw(Graphics g, Image img, int x, int y, int width, int height) {
+        flushedResizedCachedInstance();
+        g.drawImage(
+                img, x, y, x + width, y + height,
+                0, 0, img.getWidth(null), img.getHeight(null),
+                null);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        state = (State) in.readObject();
+        boolean hasImage = in.readBoolean();
+        if (hasImage)
+            image = (ImageIO.read(ImageIO.createImageInputStream(in)));
+        else {
+            in.readObject(); // read null from input stream
+            image = null;
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.writeObject(state);
+        if(getImage() == null) {
+            out.writeBoolean(false);
+            out.writeObject(null);
+        } else {
+            out.writeBoolean(true);
+            ImageIO.write(getImage(), "png", ImageIO.createImageOutputStream(out));
+        }
+    }
+
+    public void flushedResizedCachedInstance() {
+        if (reImg != null) {
+            BufferedImage img = reImg.get();
+            if (img != null) {
+                img.flush();
+            }
+        }
+        reImg = null;
+    }
+
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public int getXIndex() {
+        return xIndex;
+    }
+
+    public int getYIndex() {
+        return yIndex;
+    }
+
+    public void setLayer(WMSLayer layer) {
+        this.layer = layer;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Grabber.java	(revision 24501)
@@ -0,0 +1,115 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.projection.Projection;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryPlugin;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+
+abstract public class Grabber implements Runnable {
+    protected final MapView mv;
+    protected final WMSLayer layer;
+    protected final CacheFiles cache;
+
+    protected ProjectionBounds b;
+    protected Projection proj;
+    protected double pixelPerDegree;
+    protected WMSRequest request;
+    protected volatile boolean canceled;
+
+    Grabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        this.mv = mv;
+        this.layer = layer;
+        this.cache = cache;
+    }
+
+    private void updateState(WMSRequest request) {
+        b = new ProjectionBounds(
+                layer.getEastNorth(request.getXIndex(), request.getYIndex()),
+                layer.getEastNorth(request.getXIndex() + 1, request.getYIndex() + 1));
+        if (b.min != null && b.max != null && ImageryPlugin.wmsAdapter.PROP_OVERLAP.get()) {
+            double eastSize =  b.max.east() - b.min.east();
+            double northSize =  b.max.north() - b.min.north();
+
+            double eastCoef = ImageryPlugin.wmsAdapter.PROP_OVERLAP_EAST.get() / 100.0;
+            double northCoef = ImageryPlugin.wmsAdapter.PROP_OVERLAP_NORTH.get() / 100.0;
+
+            this.b = new ProjectionBounds( new EastNorth(b.min.east(),
+                    b.min.north()),
+                    new EastNorth(b.max.east() + eastCoef * eastSize,
+                            b.max.north() + northCoef * northSize));
+        }
+
+        this.proj = Main.proj;
+        this.pixelPerDegree = request.getPixelPerDegree();
+        this.request = request;
+    }
+
+    abstract void fetch(WMSRequest request) throws Exception; // the image fetch code
+
+    int width(){
+        return layer.getBaseImageWidth();
+    }
+    int height(){
+        return layer.getBaseImageHeight();
+    }
+
+    @Override
+    public void run() {
+        while (true) {
+            if (canceled) {
+                return;
+            }
+            WMSRequest request = layer.getRequest();
+            if (request == null) {
+                return;
+            }
+            updateState(request);
+            if(!loadFromCache(request)){
+                attempt(request);
+            }
+            if (request.getState() != null) {
+                layer.finishRequest(request);
+                mv.repaint();
+            }
+        }
+    }
+
+    protected void attempt(WMSRequest request){ // try to fetch the image
+        int maxTries = 5; // n tries for every image
+        for (int i = 1; i <= maxTries; i++) {
+            if (canceled) {
+                return;
+            }
+            try {
+                if (!layer.requestIsValid(request)) {
+                    return;
+                }
+                fetch(request);
+                break; // break out of the retry loop
+            } catch (Exception e) {
+                try { // sleep some time and then ask the server again
+                    Thread.sleep(random(1000, 2000));
+                } catch (InterruptedException e1) {}
+
+                if(i == maxTries) {
+                    e.printStackTrace();
+                    request.finish(State.FAILED, null);
+                }
+            }
+        }
+    }
+
+    public static int random(int min, int max) {
+        return (int)(Math.random() * ((max+1)-min) ) + min;
+    }
+
+    abstract public boolean loadFromCache(WMSRequest request);
+
+    public void cancel() {
+        canceled = true;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/HTMLGrabber.java	(revision 24501)
@@ -0,0 +1,46 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+import javax.imageio.ImageIO;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+
+public class HTMLGrabber extends WMSGrabber {
+    HTMLGrabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        super(mv, layer, cache);
+    }
+
+    @Override
+    protected BufferedImage grab(URL url) throws IOException {
+        String urlstring = url.toExternalForm();
+
+        System.out.println("Grabbing HTML " + url);
+
+        ArrayList<String> cmdParams = new ArrayList<String>();
+        StringTokenizer st = new StringTokenizer(MessageFormat.format(
+                Main.pref.get("wmsplugin.browser", "webkit-image {0}"), urlstring));
+        while( st.hasMoreTokens() )
+            cmdParams.add(st.nextToken());
+
+        ProcessBuilder builder = new ProcessBuilder( cmdParams);
+
+        Process browser;
+        try {
+            browser = builder.start();
+        } catch(IOException ioe) {
+            throw new IOException( "Could not start browser. Please check that the executable path is correct.\n" + ioe.getMessage() );
+        }
+
+        BufferedImage img = ImageIO.read(browser.getInputStream());
+        cache.saveImg(urlstring, img);
+        return img;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/Map_Rectifier_WMSmenuAction.java	(revision 24501)
@@ -0,0 +1,238 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Toolkit;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JTextField;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Shortcut;
+import org.openstreetmap.josm.tools.UrlLabel;
+
+public class Map_Rectifier_WMSmenuAction extends JosmAction {
+    /**
+     * Class that bundles all required information of a rectifier service
+     */
+    public static class RectifierService {
+        private final String name;
+        private final String url;
+        private final String wmsUrl;
+        private final Pattern urlRegEx;
+        private final Pattern idValidator;
+        public JRadioButton btn;
+        /**
+         * @param name: Name of the rectifing service
+         * @param url: URL to the service where users can register, upload, etc.
+         * @param wmsUrl: URL to the WMS server where JOSM will grab the images. Insert __s__ where the ID should be placed
+         * @param urlRegEx: a regular expression that determines if a given URL is one of the service and returns the WMS id if so
+         * @param idValidator: regular expression that checks if a given ID is syntactically valid
+         */
+        public RectifierService(String name, String url, String wmsUrl, String urlRegEx, String idValidator) {
+            this.name = name;
+            this.url = url;
+            this.wmsUrl = wmsUrl;
+            this.urlRegEx = Pattern.compile(urlRegEx);
+            this.idValidator = Pattern.compile(idValidator);
+        }
+
+        public boolean isSelected() {
+            return btn.isSelected();
+        }
+    }
+
+    /**
+     * List of available rectifier services. May be extended from the outside
+     */
+    public ArrayList<RectifierService> services = new ArrayList<RectifierService>();
+
+    public Map_Rectifier_WMSmenuAction() {
+        super(tr("Rectified Image..."),
+                "OLmarker",
+                tr("Download Rectified Images From Various Services"),
+                Shortcut.registerShortcut("wms:rectimg",
+                        tr("WMS: {0}", tr("Rectified Image...")),
+                        KeyEvent.VK_R,
+                        Shortcut.GROUP_NONE),
+                        true
+        );
+
+        // Add default services
+        services.add(
+                new RectifierService("Metacarta Map Rectifier",
+                        "http://labs.metacarta.com/rectifier/",
+                        "http://labs.metacarta.com/rectifier/wms.cgi?id=__s__&srs=EPSG:4326"
+                        + "&Service=WMS&Version=1.1.0&Request=GetMap&format=image/png&",
+                        // This matches more than the "classic" WMS link, so users can pretty much
+                        // copy any link as long as it includes the ID
+                        "labs\\.metacarta\\.com/(?:.*?)(?:/|=)([0-9]+)(?:\\?|/|\\.|$)",
+                "^[0-9]+$")
+        );
+        services.add(
+                // TODO: Change all links to mapwarper.net once the project has moved.
+                // The RegEx already matches the new URL and old URLs will be forwarded
+                // to make the transition as smooth as possible for the users
+                new RectifierService("Geothings Map Warper",
+                        "http://warper.geothings.net/",
+                        "http://warper.geothings.net/maps/wms/__s__?request=GetMap&version=1.1.1"
+                        + "&styles=&format=image/png&srs=epsg:4326&exceptions=application/vnd.ogc.se_inimage&",
+                        // This matches more than the "classic" WMS link, so users can pretty much
+                        // copy any link as long as it includes the ID
+                        "(?:mapwarper\\.net|warper\\.geothings\\.net)/(?:.*?)/([0-9]+)(?:\\?|/|\\.|$)",
+                "^[0-9]+$")
+        );
+
+        // This service serves the purpose of "just this once" without forcing the user
+        // to commit the link to the preferences
+
+        // Clipboard content gets trimmed, so matching whitespace only ensures that this
+        // service will never be selected automatically.
+        services.add(new RectifierService(tr("Custom WMS Link"), "", "", "^\\s+$", ""));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        JPanel panel = new JPanel(new GridBagLayout());
+        panel.add(new JLabel(tr("Supported Rectifier Services:")), GBC.eol());
+
+        JTextField tfWmsUrl = new JTextField(30);
+
+        String clip = getClipboardContents();
+        ButtonGroup group = new ButtonGroup();
+
+        JRadioButton firstBtn = null;
+        for(RectifierService s : services) {
+            JRadioButton serviceBtn = new JRadioButton(s.name);
+            if(firstBtn == null)
+                firstBtn = serviceBtn;
+            // Checks clipboard contents against current service if no match has been found yet.
+            // If the contents match, they will be inserted into the text field and the corresponding
+            // service will be pre-selected.
+            if(!clip.equals("") && tfWmsUrl.getText().equals("")
+                    && (s.urlRegEx.matcher(clip).find() || s.idValidator.matcher(clip).matches())) {
+                serviceBtn.setSelected(true);
+                tfWmsUrl.setText(clip);
+            }
+            s.btn = serviceBtn;
+            group.add(serviceBtn);
+            if(!s.url.equals("")) {
+                panel.add(serviceBtn, GBC.std());
+                panel.add(new UrlLabel(s.url, tr("Visit Homepage")), GBC.eol().anchor(GridBagConstraints.EAST));
+            } else
+                panel.add(serviceBtn, GBC.eol().anchor(GridBagConstraints.WEST));
+        }
+
+        // Fallback in case no match was found
+        if(tfWmsUrl.getText().equals("") && firstBtn != null)
+            firstBtn.setSelected(true);
+
+        panel.add(new JLabel(tr("WMS URL or Image ID:")), GBC.eol());
+        panel.add(tfWmsUrl, GBC.eol().fill(GridBagConstraints.HORIZONTAL));
+
+        ExtendedDialog diag = new ExtendedDialog(Main.parent,
+                tr("Add Rectified Image"),
+
+                new String[] {tr("Add Rectified Image"), tr("Cancel")});
+        diag.setContent(panel);
+        diag.setButtonIcons(new String[] {"OLmarker.png", "cancel.png"});
+
+        // This repeatedly shows the dialog in case there has been an error.
+        // The loop is break;-ed if the users cancels
+        outer: while(true) {
+            diag.showDialog();
+            int answer = diag.getValue();
+            // Break loop when the user cancels
+            if(answer != 1)
+                break;
+
+            String text = tfWmsUrl.getText().trim();
+            // Loop all services until we find the selected one
+            for(RectifierService s : services) {
+                if(!s.isSelected())
+                    continue;
+
+                // We've reached the custom WMS URL service
+                // Just set the URL and hope everything works out
+                if(s.wmsUrl.equals("")) {
+                    addWMSLayer(s.name + " (" + text + ")", text);
+                    break outer;
+                }
+
+                // First try to match if the entered string as an URL
+                Matcher m = s.urlRegEx.matcher(text);
+                if(m.find()) {
+                    String id = m.group(1);
+                    String newURL = s.wmsUrl.replaceAll("__s__", id);
+                    String title = s.name + " (" + id + ")";
+                    addWMSLayer(title, newURL);
+                    break outer;
+                }
+                // If not, look if it's a valid ID for the selected service
+                if(s.idValidator.matcher(text).matches()) {
+                    String newURL = s.wmsUrl.replaceAll("__s__", text);
+                    String title = s.name + " (" + text + ")";
+                    addWMSLayer(title, newURL);
+                    break outer;
+                }
+
+                // We've found the selected service, but the entered string isn't suitable for
+                // it. So quit checking the other radio buttons
+                break;
+            }
+
+            // and display an error message. The while(true) ensures that the dialog pops up again
+            JOptionPane.showMessageDialog(Main.parent,
+                    tr("Couldn't match the entered link or id to the selected service. Please try again."),
+                    tr("No valid WMS URL or id"),
+                    JOptionPane.ERROR_MESSAGE);
+            diag.setVisible(true);
+        }
+    }
+
+    /**
+     * Adds a WMS Layer with given title and URL
+     * @param title: Name of the layer as it will shop up in the layer manager
+     * @param url: URL to the WMS server
+     */
+    private void addWMSLayer(String title, String url) {
+        Main.main.addLayer(new WMSLayer(new ImageryInfo(title, url)));
+    }
+
+    /**
+     * Helper function that extracts a String from the Clipboard if available.
+     * Returns an empty String otherwise
+     * @return String Clipboard contents if available
+     */
+    private String getClipboardContents() {
+        String result = "";
+        Transferable contents = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
+
+        if(contents == null || !contents.isDataFlavorSupported(DataFlavor.stringFlavor))
+            return "";
+
+        try {
+            result = (String)contents.getTransferData(DataFlavor.stringFlavor);
+        } catch(Exception ex) {
+            return "";
+        }
+        return result.trim();
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSAdapter.java	(revision 24501)
@@ -0,0 +1,40 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerExporter;
+import org.openstreetmap.josm.plugins.imagery.wms.io.WMSLayerImporter;
+
+// WMSPlugin-specific functions
+public class WMSAdapter {
+    CacheFiles cache = new CacheFiles("wmsplugin");
+
+    public final IntegerProperty PROP_SIMULTANEOUS_CONNECTIONS = new IntegerProperty("wmsplugin.simultaneousConnections", 3);
+    public final BooleanProperty PROP_OVERLAP = new BooleanProperty("wmsplugin.url.overlap", false);
+    public final IntegerProperty PROP_OVERLAP_EAST = new IntegerProperty("wmsplugin.url.overlapEast", 14);
+    public final IntegerProperty PROP_OVERLAP_NORTH = new IntegerProperty("wmsplugin.url.overlapNorth", 4);
+
+    protected void initExporterAndImporter() {
+        ExtensionFileFilter.exporters.add(new WMSLayerExporter());
+        ExtensionFileFilter.importers.add(new WMSLayerImporter());
+    }
+
+    public WMSAdapter() {
+        cache.setExpire(CacheFiles.EXPIRE_MONTHLY, false);
+        cache.setMaxSize(70, false);
+        initExporterAndImporter();
+    }
+
+    public Grabber getGrabber(MapView mv, WMSLayer layer){
+        if(layer.info.getImageryType() == ImageryType.HTML)
+            return new HTMLGrabber(mv, layer, cache);
+        else if(layer.info.getImageryType() == ImageryType.WMS)
+            return new WMSGrabber(mv, layer, cache);
+        else throw new AssertionError();
+    }
+
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSGrabber.java	(revision 24501)
@@ -0,0 +1,203 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.projection.Mercator;
+import org.openstreetmap.josm.gui.MapView;
+import org.openstreetmap.josm.io.CacheFiles;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.io.ProgressInputStream;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+
+
+public class WMSGrabber extends Grabber {
+    public static boolean isUrlWithPatterns(String url) {
+        return url != null && url.contains("{") && url.contains("}");
+    }
+
+    protected String baseURL;
+    private final boolean urlWithPatterns;
+
+    WMSGrabber(MapView mv, WMSLayer layer, CacheFiles cache) {
+        super(mv, layer, cache);
+        this.baseURL = layer.info.getURL();
+        /* URL containing placeholders? */
+        urlWithPatterns = isUrlWithPatterns(baseURL);
+    }
+
+    @Override
+    void fetch(WMSRequest request) throws Exception{
+        URL url = null;
+        try {
+            url = getURL(
+                    b.min.east(), b.min.north(),
+                    b.max.east(), b.max.north(),
+                    width(), height());
+            request.finish(State.IMAGE, grab(url));
+
+        } catch(Exception e) {
+            e.printStackTrace();
+            throw new Exception(e.getMessage() + "\nImage couldn't be fetched: " + (url != null ? url.toString() : ""));
+        }
+    }
+
+    public static final NumberFormat latLonFormat = new DecimalFormat("###0.0000000",
+            new DecimalFormatSymbols(Locale.US));
+
+    protected URL getURL(double w, double s,double e,double n,
+            int wi, int ht) throws MalformedURLException {
+        String myProj = Main.proj.toCode();
+        if(Main.proj instanceof Mercator) // don't use mercator code directly
+        {
+            LatLon sw = Main.proj.eastNorth2latlon(new EastNorth(w, s));
+            LatLon ne = Main.proj.eastNorth2latlon(new EastNorth(e, n));
+            myProj = "EPSG:4326";
+            s = sw.lat();
+            w = sw.lon();
+            n = ne.lat();
+            e = ne.lon();
+        }
+
+        String str = baseURL;
+        String bbox = latLonFormat.format(w) + ","
+        + latLonFormat.format(s) + ","
+        + latLonFormat.format(e) + ","
+        + latLonFormat.format(n);
+
+        if (urlWithPatterns) {
+            str = str.replaceAll("\\{proj\\}", myProj)
+            .replaceAll("\\{bbox\\}", bbox)
+            .replaceAll("\\{w\\}", latLonFormat.format(w))
+            .replaceAll("\\{s\\}", latLonFormat.format(s))
+            .replaceAll("\\{e\\}", latLonFormat.format(e))
+            .replaceAll("\\{n\\}", latLonFormat.format(n))
+            .replaceAll("\\{width\\}", String.valueOf(wi))
+            .replaceAll("\\{height\\}", String.valueOf(ht));
+        } else {
+            str += "bbox=" + bbox
+            + getProjection(baseURL, false)
+            + "&width=" + wi + "&height=" + ht;
+            if (!(baseURL.endsWith("&") || baseURL.endsWith("?"))) {
+                System.out.println(tr("Warning: The base URL ''{0}'' for a WMS service doesn't have a trailing '&' or a trailing '?'.", baseURL));
+                System.out.println(tr("Warning: Fetching WMS tiles is likely to fail. Please check you preference settings."));
+                System.out.println(tr("Warning: The complete URL is ''{0}''.", str));
+            }
+        }
+        return new URL(str.replace(" ", "%20"));
+    }
+
+    static public String getProjection(String baseURL, Boolean warn)
+    {
+        String projname = Main.proj.toCode();
+        if(Main.proj instanceof Mercator) // don't use mercator code
+            projname = "EPSG:4326";
+        String res = "";
+        try
+        {
+            Matcher m = Pattern.compile(".*srs=([a-z0-9:]+).*").matcher(baseURL.toLowerCase());
+            if(m.matches())
+            {
+                projname = projname.toLowerCase();
+                if(!projname.equals(m.group(1)) && warn)
+                {
+                    JOptionPane.showMessageDialog(Main.parent,
+                            tr("The projection ''{0}'' in URL and current projection ''{1}'' mismatch.\n"
+                                    + "This may lead to wrong coordinates.",
+                                    m.group(1), projname),
+                                    tr("Warning"),
+                                    JOptionPane.WARNING_MESSAGE);
+                }
+            }
+            else
+                res ="&srs="+projname;
+        }
+        catch(Exception e)
+        {
+        }
+        return res;
+    }
+
+    @Override
+    public boolean loadFromCache(WMSRequest request) {
+        URL url = null;
+        try{
+            url = getURL(
+                    b.min.east(), b.min.north(),
+                    b.max.east(), b.max.north(),
+                    width(), height());
+        } catch(Exception e) {
+            return false;
+        }
+        BufferedImage cached = cache.getImg(url.toString());
+        if((!request.isReal() && !layer.hasAutoDownload()) || cached != null){
+            if(cached == null){
+                request.finish(State.NOT_IN_CACHE, null);
+                return true;
+            }
+            request.finish(State.IMAGE, cached);
+            return true;
+        }
+        return false;
+    }
+
+    protected BufferedImage grab(URL url) throws IOException, OsmTransferException {
+        System.out.println("Grabbing WMS " + url);
+
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        if(layer.info.getCookies() != null && !layer.info.getCookies().equals(""))
+            conn.setRequestProperty("Cookie", layer.info.getCookies());
+        conn.setRequestProperty("User-Agent", Main.pref.get("wmsplugin.user_agent", Version.getInstance().getAgentString()));
+        conn.setConnectTimeout(Main.pref.getInteger("wmsplugin.timeout.connect", 30) * 1000);
+        conn.setReadTimeout(Main.pref.getInteger("wmsplugin.timeout.read", 30) * 1000);
+
+        String contentType = conn.getHeaderField("Content-Type");
+        if( conn.getResponseCode() != 200
+                || contentType != null && !contentType.startsWith("image") ) {
+            throw new IOException(readException(conn));
+        }
+
+        InputStream is = new ProgressInputStream(conn, null);
+        BufferedImage img = ImageIO.read(is);
+        is.close();
+
+        cache.saveImg(url.toString(), img);
+        return img;
+    }
+
+    protected String readException(URLConnection conn) throws IOException {
+        StringBuilder exception = new StringBuilder();
+        InputStream in = conn.getInputStream();
+        BufferedReader br = new BufferedReader(new InputStreamReader(in));
+
+        String line = null;
+        while( (line = br.readLine()) != null) {
+            // filter non-ASCII characters and control characters
+            exception.append(line.replaceAll("[^\\p{Print}]", ""));
+            exception.append('\n');
+        }
+        return exception.toString();
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSLayer.java	(revision 24501)
@@ -0,0 +1,787 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.DiskAccessAction;
+import org.openstreetmap.josm.actions.SaveActionBase;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
+import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener;
+import org.openstreetmap.josm.data.ProjectionBounds;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+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.io.CacheFiles;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo.ImageryType;
+import org.openstreetmap.josm.plugins.imagery.ImageryLayer;
+import org.openstreetmap.josm.plugins.imagery.ImageryPlugin;
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+import org.openstreetmap.josm.tools.ImageProvider;
+
+/**
+ * This is a layer that grabs the current screen from an WMS server. The data
+ * fetched this way is tiled and managed to the disc to reduce server load.
+ */
+public class WMSLayer extends ImageryLayer implements PreferenceChangedListener {
+
+    public static final BooleanProperty PROP_ALPHA_CHANNEL = new BooleanProperty("wmsplugin.alpha_channel", true);
+    WMSAdapter plugin = ImageryPlugin.wmsAdapter;
+
+    public int messageNum = 5; //limit for messages per layer
+    protected String resolution;
+    protected int imageSize = 500;
+    protected int dax = 10;
+    protected int day = 10;
+    protected int daStep = 5;
+    protected int minZoom = 3;
+
+    protected GeorefImage[][] images;
+    protected final int serializeFormatVersion = 5;
+    protected boolean autoDownloadEnabled = true;
+    protected boolean settingsChanged;
+    protected ImageryInfo info;
+
+    // Image index boundary for current view
+    private volatile int bminx;
+    private volatile int bminy;
+    private volatile int bmaxx;
+    private volatile int bmaxy;
+    private volatile int leftEdge;
+    private volatile int bottomEdge;
+
+    // Request queue
+    private final List<WMSRequest> requestQueue = new ArrayList<WMSRequest>();
+    private final List<WMSRequest> finishedRequests = new ArrayList<WMSRequest>();
+    private final Lock requestQueueLock = new ReentrantLock();
+    private final Condition queueEmpty = requestQueueLock.newCondition();
+    private final List<Grabber> grabbers = new ArrayList<Grabber>();
+    private final List<Thread> grabberThreads = new ArrayList<Thread>();
+    private int threadCount;
+    private int workingThreadCount;
+    private boolean canceled;
+
+
+    /** set to true if this layer uses an invalid base url */
+    private boolean usesInvalidUrl = false;
+    /** set to true if the user confirmed to use an potentially invalid WMS base url */
+    private boolean isInvalidUrlConfirmed = false;
+
+    public WMSLayer() {
+        this(new ImageryInfo(tr("Blank Layer")));
+    }
+
+    public WMSLayer(ImageryInfo info) {
+        super(info.getName());
+        setBackgroundLayer(true); /* set global background variable */
+        initializeImages();
+        this.info = new ImageryInfo(info);
+        mv = Main.map.mapView;
+        if(this.info.getPixelPerDegree() == 0.0)
+            this.info.setPixelPerDegree(getPPD());
+        resolution = mv.getDist100PixelText();
+
+        if(info.getURL() != null) {
+            WMSGrabber.getProjection(info.getURL(), true);
+            startGrabberThreads();
+            if(info.getImageryType() == ImageryType.WMS && !WMSGrabber.isUrlWithPatterns(info.getURL())) {
+                if (!(info.getURL().endsWith("&") || info.getURL().endsWith("?"))) {
+                    if (!confirmMalformedUrl(info.getURL())) {
+                        System.out.println(tr("Warning: WMS layer deactivated because of malformed base url ''{0}''", info.getURL()));
+                        usesInvalidUrl = true;
+                        setName(getName() + tr("(deactivated)"));
+                        return;
+                    } else {
+                        isInvalidUrlConfirmed = true;
+                    }
+                }
+            }
+        }
+
+        Main.pref.addPreferenceChangeListener(this);
+    }
+
+    public void doSetName(String name) {
+        setName(name);
+        info.setName(name);
+    }
+
+    public boolean hasAutoDownload(){
+        return autoDownloadEnabled;
+    }
+
+
+    @Override
+    public void destroy() {
+        cancelGrabberThreads(false);
+        Main.pref.removePreferenceChangeListener(this);
+    }
+
+    public void initializeImages() {
+        GeorefImage[][] old = images;
+        images = new GeorefImage[dax][day];
+        if (old != null) {
+            for (int i=0; i<old.length; i++) {
+                for (int k=0; k<old[i].length; k++) {
+                    GeorefImage o = old[i][k];
+                    images[modulo(o.getXIndex(),dax)][modulo(o.getYIndex(),day)] = old[i][k];
+                }
+            }
+        }
+        for(int x = 0; x<dax; ++x) {
+            for(int y = 0; y<day; ++y) {
+                if (images[x][y] == null) {
+                    images[x][y]= new GeorefImage(this);
+                }
+            }
+        }
+    }
+
+    @Override public String getToolTipText() {
+        if(autoDownloadEnabled)
+            return tr("WMS layer ({0}), automatically downloading in zoom {1}", getName(), resolution);
+        else
+            return tr("WMS layer ({0}), downloading in zoom {1}", getName(), resolution);
+    }
+
+    @Override public boolean isMergable(Layer other) {
+        return false;
+    }
+
+    @Override public void mergeFrom(Layer from) {
+    }
+
+    private int modulo (int a, int b) {
+        return a % b >= 0 ? a%b : a%b+b;
+    }
+
+    private boolean zoomIsTooBig() {
+        //don't download when it's too outzoomed
+        return info.getPixelPerDegree() / getPPD() > minZoom;
+    }
+
+    @Override public void paint(Graphics2D g, final MapView mv, Bounds b) {
+        if(info.getURL() == null || (usesInvalidUrl && !isInvalidUrlConfirmed)) return;
+
+        settingsChanged = false;
+
+        ProjectionBounds bounds = mv.getProjectionBounds();
+        bminx= getImageXIndex(bounds.min.east());
+        bminy= getImageYIndex(bounds.min.north());
+        bmaxx= getImageXIndex(bounds.max.east());
+        bmaxy= getImageYIndex(bounds.max.north());
+
+        leftEdge = (int)(bounds.min.east() * getPPD());
+        bottomEdge = (int)(bounds.min.north() * getPPD());
+
+        if (zoomIsTooBig()) {
+            for(int x = bminx; x<=bmaxx; ++x) {
+                for(int y = bminy; y<=bmaxy; ++y) {
+                    images[modulo(x,dax)][modulo(y,day)].paint(g, mv, x, y, leftEdge, bottomEdge);
+                }
+            }
+        } else {
+            downloadAndPaintVisible(g, mv, false);
+        }
+    }
+
+    protected boolean confirmMalformedUrl(String url) {
+        if (isInvalidUrlConfirmed)
+            return true;
+        String msg  = tr("<html>The base URL<br>"
+                + "''{0}''<br>"
+                + "for this WMS layer does neither end with a ''&'' nor with a ''?''.<br>"
+                + "This is likely to lead to invalid WMS request. You should check your<br>"
+                + "preference settings.<br>"
+                + "Do you want to fetch WMS tiles anyway?",
+                url);
+        String [] options = new String[] {
+                tr("Yes, fetch images"),
+                tr("No, abort")
+        };
+        int ret = JOptionPane.showOptionDialog(
+                Main.parent,
+                msg,
+                tr("Invalid URL?"),
+                JOptionPane.YES_NO_OPTION,
+                JOptionPane.WARNING_MESSAGE,
+                null,
+                options, options[1]
+        );
+        switch(ret) {
+        case JOptionPane.YES_OPTION: return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public void displace(double dx, double dy) {
+        super.displace(dx, dy);
+        settingsChanged = true;
+    }
+
+    public int getImageXIndex(double coord) {
+        return (int)Math.floor( ((coord - dx) * info.getPixelPerDegree()) / imageSize);
+    }
+
+    public int getImageYIndex(double coord) {
+        return (int)Math.floor( ((coord - dy) * info.getPixelPerDegree()) / imageSize);
+    }
+
+    public int getImageX(int imageIndex) {
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dx * getPPD());
+    }
+
+    public int getImageY(int imageIndex) {
+        return (int)(imageIndex * imageSize * (getPPD() / info.getPixelPerDegree()) + dy * getPPD());
+    }
+
+    public int getImageWidth(int xIndex) {
+        int overlap = (int)(plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_EAST.get() * imageSize * getPPD() / info.getPixelPerDegree() / 100:0);
+        return getImageX(xIndex + 1) - getImageX(xIndex) + overlap;
+    }
+
+    public int getImageHeight(int yIndex) {
+        int overlap = (int)(plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_NORTH.get() * imageSize * getPPD() / info.getPixelPerDegree() / 100:0);
+        return getImageY(yIndex + 1) - getImageY(yIndex) + overlap;
+    }
+
+    /**
+     *
+     * @return Size of image in original zoom
+     */
+    public int getBaseImageWidth() {
+        int overlap = (plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_EAST.get() * imageSize / 100:0);
+        return imageSize + overlap;
+    }
+
+    /**
+     *
+     * @return Size of image in original zoom
+     */
+    public int getBaseImageHeight() {
+        int overlap = (plugin.PROP_OVERLAP.get()?plugin.PROP_OVERLAP_NORTH.get() * imageSize / 100:0);
+        return imageSize + overlap;
+    }
+
+
+    /**
+     *
+     * @param xIndex
+     * @param yIndex
+     * @return Real EastNorth of given tile. dx/dy is not counted in
+     */
+    public EastNorth getEastNorth(int xIndex, int yIndex) {
+        return new EastNorth((xIndex * imageSize) / info.getPixelPerDegree(), (yIndex * imageSize) / info.getPixelPerDegree());
+    }
+
+
+    protected void downloadAndPaintVisible(Graphics g, final MapView mv, boolean real){
+
+        int newDax = dax;
+        int newDay = day;
+
+        if (bmaxx - bminx >= dax || bmaxx - bminx < dax - 2 * daStep) {
+            newDax = ((bmaxx - bminx) / daStep + 1) * daStep;
+        }
+
+        if (bmaxy - bminy >= day || bmaxy - bminx < day - 2 * daStep) {
+            newDay = ((bmaxy - bminy) / daStep + 1) * daStep;
+        }
+
+        if (newDax != dax || newDay != day) {
+            dax = newDax;
+            day = newDay;
+            initializeImages();
+        }
+
+        for(int x = bminx; x<=bmaxx; ++x) {
+            for(int y = bminy; y<=bmaxy; ++y){
+                images[modulo(x,dax)][modulo(y,day)].changePosition(x, y);
+            }
+        }
+
+        gatherFinishedRequests();
+
+        for(int x = bminx; x<=bmaxx; ++x) {
+            for(int y = bminy; y<=bmaxy; ++y){
+                GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
+                if (!img.paint(g, mv, x, y, leftEdge, bottomEdge)) {
+                    WMSRequest request = new WMSRequest(x, y, info.getPixelPerDegree(), real);
+                    addRequest(request);
+                }
+            }
+        }
+    }
+
+    @Override public void visitBoundingBox(BoundingXYVisitor v) {
+        for(int x = 0; x<dax; ++x) {
+            for(int y = 0; y<day; ++y)
+                if(images[x][y].getImage() != null){
+                    v.visit(images[x][y].getMin());
+                    v.visit(images[x][y].getMax());
+                }
+        }
+    }
+
+    @Override public Action[] getMenuEntries() {
+        return new Action[]{
+                LayerListDialog.getInstance().createActivateLayerAction(this),
+                LayerListDialog.getInstance().createShowHideLayerAction(),
+                LayerListDialog.getInstance().createDeleteLayerAction(),
+                SeparatorLayerAction.INSTANCE,
+                new LoadWmsAction(),
+                new SaveWmsAction(),
+                new BookmarkWmsAction(),
+                SeparatorLayerAction.INSTANCE,
+                new StartStopAction(),
+                new ToggleAlphaAction(),
+                new ChangeResolutionAction(),
+                new ReloadErrorTilesAction(),
+                new DownloadAction(),
+                SeparatorLayerAction.INSTANCE,
+                new LayerListPopup.InfoAction(this)
+        };
+    }
+
+    public GeorefImage findImage(EastNorth eastNorth) {
+        int xIndex = getImageXIndex(eastNorth.east());
+        int yIndex = getImageYIndex(eastNorth.north());
+        GeorefImage result = images[modulo(xIndex, dax)][modulo(yIndex, day)];
+        if (result.getXIndex() == xIndex && result.getYIndex() == yIndex) {
+            return result;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     *
+     * @param request
+     * @return -1 if request is no longer needed, otherwise priority of request (lower number <=> more important request)
+     */
+    private int getRequestPriority(WMSRequest request) {
+        if (request.getPixelPerDegree() != info.getPixelPerDegree()) {
+            return -1;
+        }
+        if (bminx > request.getXIndex()
+                || bmaxx < request.getXIndex()
+                || bminy > request.getYIndex()
+                || bmaxy < request.getYIndex()) {
+            return -1;
+        }
+
+        EastNorth cursorEastNorth = mv.getEastNorth(mv.lastMEvent.getX(), mv.lastMEvent.getY());
+        int mouseX = getImageXIndex(cursorEastNorth.east());
+        int mouseY = getImageYIndex(cursorEastNorth.north());
+        int dx = request.getXIndex() - mouseX;
+        int dy = request.getYIndex() - mouseY;
+
+        return dx * dx + dy * dy;
+    }
+
+    public WMSRequest getRequest() {
+        requestQueueLock.lock();
+        try {
+            workingThreadCount--;
+            Iterator<WMSRequest> it = requestQueue.iterator();
+            while (it.hasNext()) {
+                WMSRequest item = it.next();
+                int priority = getRequestPriority(item);
+                if (priority == -1) {
+                    it.remove();
+                } else {
+                    item.setPriority(priority);
+                }
+            }
+            Collections.sort(requestQueue);
+
+            EastNorth cursorEastNorth = mv.getEastNorth(mv.lastMEvent.getX(), mv.lastMEvent.getY());
+            int mouseX = getImageXIndex(cursorEastNorth.east());
+            int mouseY = getImageYIndex(cursorEastNorth.north());
+            boolean isOnMouse = requestQueue.size() > 0 && requestQueue.get(0).getXIndex() == mouseX && requestQueue.get(0).getYIndex() == mouseY;
+
+            // If there is only one thread left then keep it in case we need to download other tile urgently
+            while (!canceled &&
+                    (requestQueue.isEmpty() || (!isOnMouse && threadCount - workingThreadCount == 0 && threadCount > 1))) {
+                try {
+                    queueEmpty.await();
+                } catch (InterruptedException e) {
+                    // Shouldn't happen
+                }
+            }
+
+            workingThreadCount++;
+            if (canceled) {
+                return null;
+            } else {
+                return requestQueue.remove(0);
+            }
+
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public void finishRequest(WMSRequest request) {
+        if (request.getState() == null) {
+            throw new IllegalArgumentException("Finished request without state");
+        }
+        requestQueueLock.lock();
+        try {
+            finishedRequests.add(request);
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public void addRequest(WMSRequest request) {
+        requestQueueLock.lock();
+        try {
+            if (!requestQueue.contains(request)) {
+                requestQueue.add(request);
+                queueEmpty.signalAll();
+            }
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    public boolean requestIsValid(WMSRequest request) {
+        return bminx <= request.getXIndex() && bmaxx >= request.getXIndex() && bminy <= request.getYIndex() && bmaxy >= request.getYIndex();
+    }
+
+    private void gatherFinishedRequests() {
+        requestQueueLock.lock();
+        try {
+            for (WMSRequest request: finishedRequests) {
+                GeorefImage img = images[modulo(request.getXIndex(),dax)][modulo(request.getYIndex(),day)];
+                if (img.equalPosition(request.getXIndex(), request.getYIndex())) {
+                    img.changeImage(request.getState(), request.getImage());
+                }
+            }
+        } finally {
+            finishedRequests.clear();
+            requestQueueLock.unlock();
+        }
+    }
+
+    public class DownloadAction extends AbstractAction {
+        private static final long serialVersionUID = -7183852461015284020L;
+        public DownloadAction() {
+            super(tr("Download visible tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            if (zoomIsTooBig()) {
+                JOptionPane.showMessageDialog(
+                        Main.parent,
+                        tr("The requested area is too big. Please zoom in a little, or change resolution"),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE
+                );
+            } else {
+                downloadAndPaintVisible(mv.getGraphics(), mv, true);
+            }
+        }
+    }
+
+    public class ChangeResolutionAction extends AbstractAction {
+        public ChangeResolutionAction() {
+            super(tr("Change resolution"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            initializeImages();
+            resolution = mv.getDist100PixelText();
+            info.setPixelPerDegree(getPPD());
+            settingsChanged = true;
+            mv.repaint();
+        }
+    }
+
+    public class ReloadErrorTilesAction extends AbstractAction {
+        public ReloadErrorTilesAction() {
+            super(tr("Reload erroneous tiles"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            // Delete small files, because they're probably blank tiles.
+            // See https://josm.openstreetmap.de/ticket/2307
+            plugin.cache.customCleanUp(CacheFiles.CLEAN_SMALL_FILES, 4096);
+
+            for (int x = 0; x < dax; ++x) {
+                for (int y = 0; y < day; ++y) {
+                    GeorefImage img = images[modulo(x,dax)][modulo(y,day)];
+                    if(img.getState() == State.FAILED){
+                        addRequest(new WMSRequest(img.getXIndex(), img.getYIndex(), info.getPixelPerDegree(), true));
+                        mv.repaint();
+                    }
+                }
+            }
+        }
+    }
+
+    public class ToggleAlphaAction extends AbstractAction implements LayerAction {
+        public ToggleAlphaAction() {
+            super(tr("Alpha channel"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            JCheckBoxMenuItem checkbox = (JCheckBoxMenuItem) ev.getSource();
+            boolean alphaChannel = checkbox.isSelected();
+            PROP_ALPHA_CHANNEL.put(alphaChannel);
+
+            // clear all resized cached instances and repaint the layer
+            for (int x = 0; x < dax; ++x) {
+                for (int y = 0; y < day; ++y) {
+                    GeorefImage img = images[modulo(x, dax)][modulo(y, day)];
+                    img.flushedResizedCachedInstance();
+                }
+            }
+            mv.repaint();
+        }
+        @Override
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(PROP_ALPHA_CHANNEL.get());
+            return item;
+        }
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
+        }
+    }
+
+    public class SaveWmsAction extends AbstractAction {
+        public SaveWmsAction() {
+            super(tr("Save WMS layer to file"), ImageProvider.get("save"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            File f = SaveActionBase.createAndOpenSaveFileChooser(
+                    tr("Save WMS layer"), ".wms");
+            try {
+                if (f != null) {
+                    ObjectOutputStream oos = new ObjectOutputStream(
+                            new FileOutputStream(f)
+                    );
+                    oos.writeInt(serializeFormatVersion);
+                    oos.writeInt(dax);
+                    oos.writeInt(day);
+                    oos.writeInt(imageSize);
+                    oos.writeDouble(info.getPixelPerDegree());
+                    oos.writeObject(info.getName());
+                    oos.writeObject(info.getFullURL());
+                    oos.writeObject(images);
+                    oos.close();
+                }
+            } catch (Exception ex) {
+                ex.printStackTrace(System.out);
+            }
+        }
+    }
+
+    public class LoadWmsAction extends AbstractAction {
+        public LoadWmsAction() {
+            super(tr("Load WMS layer from file"), ImageProvider.get("load"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            JFileChooser fc = DiskAccessAction.createAndOpenFileChooser(true,
+                    false, tr("Load WMS layer"), "wms");
+            if(fc == null) return;
+            File f = fc.getSelectedFile();
+            if (f == null) return;
+            try
+            {
+                FileInputStream fis = new FileInputStream(f);
+                ObjectInputStream ois = new ObjectInputStream(fis);
+                int sfv = ois.readInt();
+                if (sfv != serializeFormatVersion) {
+                    JOptionPane.showMessageDialog(Main.parent,
+                            tr("Unsupported WMS file version; found {0}, expected {1}", sfv, serializeFormatVersion),
+                            tr("File Format Error"),
+                            JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+                autoDownloadEnabled = false;
+                dax = ois.readInt();
+                day = ois.readInt();
+                imageSize = ois.readInt();
+                info.setPixelPerDegree(ois.readDouble());
+                doSetName((String)ois.readObject());
+                info.setURL((String) ois.readObject());
+                images = (GeorefImage[][])ois.readObject();
+                ois.close();
+                fis.close();
+                for (GeorefImage[] imgs : images) {
+                    for (GeorefImage img : imgs) {
+                        if (img != null) {
+                            img.setLayer(WMSLayer.this);
+                        }
+                    }
+                }
+                settingsChanged = true;
+                mv.repaint();
+                if(info.getURL() != null)
+                {
+                    startGrabberThreads();
+                }
+            }
+            catch (Exception ex) {
+                // FIXME be more specific
+                ex.printStackTrace(System.out);
+                JOptionPane.showMessageDialog(Main.parent,
+                        tr("Error loading file"),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE);
+                return;
+            }
+        }
+    }
+    /**
+     * This action will add a WMS layer menu entry with the current WMS layer
+     * URL and name extended by the current resolution.
+     * When using the menu entry again, the WMS cache will be used properly.
+     */
+    public class BookmarkWmsAction extends AbstractAction {
+        public BookmarkWmsAction() {
+            super(tr("Set WMS Bookmark"));
+        }
+        @Override
+        public void actionPerformed(ActionEvent ev) {
+            ImageryPlugin.instance.addLayer(new ImageryInfo(info));
+        }
+    }
+
+    private class StartStopAction extends AbstractAction implements LayerAction {
+
+        public StartStopAction() {
+            super(tr("Automatic downloading"));
+        }
+
+        @Override
+        public Component createMenuComponent() {
+            JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
+            item.setSelected(autoDownloadEnabled);
+            return item;
+        }
+
+        @Override
+        public boolean supportLayers(List<Layer> layers) {
+            return layers.size() == 1 && layers.get(0) instanceof WMSLayer;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            autoDownloadEnabled = !autoDownloadEnabled;
+            if (autoDownloadEnabled) {
+                mv.repaint();
+            }
+        }
+    }
+
+    private void cancelGrabberThreads(boolean wait) {
+        requestQueueLock.lock();
+        try {
+            canceled = true;
+            for (Grabber grabber: grabbers) {
+                grabber.cancel();
+            }
+            queueEmpty.signalAll();
+        } finally {
+            requestQueueLock.unlock();
+        }
+        if (wait) {
+            for (Thread t: grabberThreads) {
+                try {
+                    t.join();
+                } catch (InterruptedException e) {
+                    // Shouldn't happen
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    private void startGrabberThreads() {
+        int threadCount = plugin.PROP_SIMULTANEOUS_CONNECTIONS.get();
+        requestQueueLock.lock();
+        try {
+            canceled = false;
+            grabbers.clear();
+            grabberThreads.clear();
+            for (int i=0; i<threadCount; i++) {
+                Grabber grabber = plugin.getGrabber(mv, this);
+                grabbers.add(grabber);
+                Thread t = new Thread(grabber, "WMS " + getName() + " " + i);
+                t.setDaemon(true);
+                t.start();
+                grabberThreads.add(t);
+            }
+            this.workingThreadCount = grabbers.size();
+            this.threadCount = grabbers.size();
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    @Override
+    public boolean isChanged() {
+        requestQueueLock.lock();
+        try {
+            return !finishedRequests.isEmpty() || settingsChanged;
+        } finally {
+            requestQueueLock.unlock();
+        }
+    }
+
+    @Override
+    public void preferenceChanged(PreferenceChangeEvent event) {
+        if (event.getKey().equals(plugin.PROP_SIMULTANEOUS_CONNECTIONS.getKey())) {
+            cancelGrabberThreads(true);
+            startGrabberThreads();
+        } else if (
+                event.getKey().equals(plugin.PROP_OVERLAP.getKey())
+                || event.getKey().equals(plugin.PROP_OVERLAP_EAST.getKey())
+                || event.getKey().equals(plugin.PROP_OVERLAP_NORTH.getKey())) {
+            for (int i=0; i<images.length; i++) {
+                for (int k=0; k<images[i].length; k++) {
+                    images[i][k] = new GeorefImage(this);
+                }
+            }
+
+            settingsChanged = true;
+        }
+    }
+
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRemoteHandler.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRemoteHandler.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRemoteHandler.java	(revision 24501)
@@ -0,0 +1,107 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.HashMap;
+import java.util.StringTokenizer;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.plugins.imagery.ImageryInfo;
+import org.openstreetmap.josm.plugins.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.plugins.remotecontrol.RequestHandler;
+import org.openstreetmap.josm.plugins.remotecontrol.RequestHandlerErrorException;
+
+public class WMSRemoteHandler extends RequestHandler {
+
+    public static final String command = "wms";
+
+    @Override
+    public String getPermissionMessage() {
+        return tr("Remote Control has been asked to load a WMS layer from the following URL:") +
+        "<br>" + args.get("url");
+    }
+
+    @Override
+    public PermissionPrefWithDefault getPermissionPref()
+    {
+        return new PermissionPrefWithDefault(
+                "wmsplugin.remotecontrol",
+                true,
+        "RemoteControl: WMS forbidden by preferences");
+    }
+
+    @Override
+    protected String[] getMandatoryParams()
+    {
+        return new String[] { "url" };
+    }
+
+    @Override
+    protected void handleRequest() throws RequestHandlerErrorException {
+        String url = args.get("url");
+        String title = args.get("title");
+        if((title == null) || (title.length() == 0))
+        {
+            title = tr("Remote WMS");
+        }
+        String cookies = args.get("cookies");
+        WMSLayer wmsLayer = new WMSLayer(new ImageryInfo(title, url, cookies));
+        Main.main.addLayer(wmsLayer);
+
+    }
+
+    @Override
+    public void parseArgs() {
+        StringTokenizer st = new StringTokenizer(request, "&?");
+        HashMap<String, String> args = new HashMap<String, String>();
+        // skip first element which is the command
+        if(st.hasMoreTokens()) st.nextToken();
+        while (st.hasMoreTokens()) {
+            String param = st.nextToken();
+            int eq = param.indexOf("=");
+            if (eq > -1)
+            {
+                String key = param.substring(0, eq);
+                /* "url=" terminates normal parameters
+                 * and will be handled separately
+                 */
+                if("url".equals(key)) break;
+
+                String value = param.substring(eq + 1);
+                // urldecode all normal values
+                try {
+                    value = URLDecoder.decode(value, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+                args.put(key,
+                        value);
+            }
+        }
+        // url as second or later parameter
+        int urlpos = request.indexOf("&url=");
+        // url as first (and only) parameter
+        if(urlpos < 0) urlpos = request.indexOf("?url=");
+        // url found?
+        if(urlpos >= 0) {
+            // URL value
+            String value = request.substring(urlpos + 5);
+            // allow skipping URL decoding with urldecode=false
+            String urldecode = args.get("urldecode");
+            if((urldecode == null) || (Boolean.valueOf(urldecode) == true))
+            {
+                try {
+                    value = URLDecoder.decode(value, "UTF-8");
+                } catch (UnsupportedEncodingException e) {
+                    // TODO Auto-generated catch block
+                    e.printStackTrace();
+                }
+            }
+            args.put("url", value);
+        }
+        this.args = args;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/WMSRequest.java	(revision 24501)
@@ -0,0 +1,102 @@
+package org.openstreetmap.josm.plugins.imagery.wms;
+
+import java.awt.image.BufferedImage;
+
+import org.openstreetmap.josm.plugins.imagery.wms.GeorefImage.State;
+
+public class WMSRequest implements Comparable<WMSRequest> {
+    private final int xIndex;
+    private final int yIndex;
+    private final double pixelPerDegree;
+    private final boolean real; // Download even if autodownloading is disabled
+    private int priority;
+    // Result
+    private State state;
+    private BufferedImage image;
+
+    public WMSRequest(int xIndex, int yIndex, double pixelPerDegree, boolean real) {
+        this.xIndex = xIndex;
+        this.yIndex = yIndex;
+        this.pixelPerDegree = pixelPerDegree;
+        this.real = real;
+    }
+
+    public void finish(State state, BufferedImage image) {
+        this.state = state;
+        this.image = image;
+    }
+
+    public int getXIndex() {
+        return xIndex;
+    }
+
+    public int getYIndex() {
+        return yIndex;
+    }
+
+    public double getPixelPerDegree() {
+        return pixelPerDegree;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        long temp;
+        temp = Double.doubleToLongBits(pixelPerDegree);
+        result = prime * result + (int) (temp ^ (temp >>> 32));
+        result = prime * result + xIndex;
+        result = prime * result + yIndex;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        WMSRequest other = (WMSRequest) obj;
+        if (Double.doubleToLongBits(pixelPerDegree) != Double
+                .doubleToLongBits(other.pixelPerDegree))
+            return false;
+        if (xIndex != other.xIndex)
+            return false;
+        if (yIndex != other.yIndex)
+            return false;
+        return true;
+    }
+
+    public void setPriority(int priority) {
+        this.priority = priority;
+    }
+
+    public int getPriority() {
+        return priority;
+    }
+
+    @Override
+    public int compareTo(WMSRequest o) {
+        return priority - o.priority;
+    }
+
+    public State getState() {
+        return state;
+    }
+
+    public BufferedImage getImage() {
+        return image;
+    }
+
+    @Override
+    public String toString() {
+        return "WMSRequest [xIndex=" + xIndex + ", yIndex=" + yIndex
+        + ", pixelPerDegree=" + pixelPerDegree + "]";
+    }
+
+    public boolean isReal() {
+        return real;
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerExporter.java	(revision 24501)
@@ -0,0 +1,13 @@
+package org.openstreetmap.josm.plugins.imagery.wms.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.io.FileExporter;
+
+public class WMSLayerExporter extends FileExporter{
+
+    public WMSLayerExporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+}
Index: applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java
===================================================================
--- applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java	(revision 24501)
+++ applications/editors/josm/plugins/imagery/src/org/openstreetmap/josm/plugins/imagery/wms/io/WMSLayerImporter.java	(revision 24501)
@@ -0,0 +1,14 @@
+package org.openstreetmap.josm.plugins.imagery.wms.io;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.actions.ExtensionFileFilter;
+import org.openstreetmap.josm.io.FileImporter;
+
+public class WMSLayerImporter extends FileImporter{
+
+    public WMSLayerImporter() {
+        super(new ExtensionFileFilter("wms", "wms", tr("WMS Files (*.wms)")));
+    }
+
+}
