diff --git .checkstyle .checkstyle
index 66b7500..25cd9c8 100644
--- .checkstyle
+++ .checkstyle
@@ -5,7 +5,7 @@
     <additional-data name="protect-config-file" value="false"/>
   </local-check-config>
   <fileset name="all" enabled="true" check-config-name="JMapViewer" local="true">
-    <file-match-pattern match-pattern="." include-pattern="true"/>
+    <file-match-pattern match-pattern="\.java$" include-pattern="true"/>
   </fileset>
   <filter name="FilesFromPackage" enabled="true">
     <filter-data value="src/org/openstreetmap/gui/jmapviewer/images"/>
diff --git .classpath .classpath
index cf1198a..2c27265 100644
--- .classpath
+++ .classpath
@@ -6,7 +6,11 @@
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
-	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+		<attributes>
+			<attribute name="module" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git .gitignore .gitignore
index 6befe71..44747be 100644
--- .gitignore
+++ .gitignore
@@ -2,19 +2,17 @@
 JMapViewer.jar
 JMapViewer_Demo.jar
 JMapViewer_src.jar
-JMapViewer.zip
 /bin/
 /bintest/
 /javadoc/
 /report/
-/tiles/
 /checkstyle-jmapviewer.xml
-/findbugs-jmapviewer.xml
 /spotbugs-jmapviewer.xml
 *.exec
 
 # Maven output
 /target/
+/releases/*-SNAPSHOT/
 
 # SVN working copy metadata (this tree is an svn checkout)
 /.svn/
diff --git .project .project
index ad7e557..8a037ad 100644
--- .project
+++ .project
@@ -27,7 +27,6 @@
 		</buildCommand>
 	</buildSpec>
 	<natures>
-		<nature>org.sonar.ide.eclipse.core.sonarNature</nature>
 		<nature>org.eclipse.jdt.core.javanature</nature>
 		<nature>net.sf.eclipsecs.core.CheckstyleNature</nature>
 		<nature>edu.umd.cs.findbugs.plugin.eclipse.findbugsNature</nature>
diff --git .settings/edu.umd.cs.findbugs.core.prefs .settings/edu.umd.cs.findbugs.core.prefs
index e406091..99abdf4 100644
--- .settings/edu.umd.cs.findbugs.core.prefs
+++ .settings/edu.umd.cs.findbugs.core.prefs
@@ -1,6 +1,5 @@
 #FindBugs User Preferences
 #Sat Aug 01 22:46:10 CEST 2015
-cloud_id=edu.umd.cs.findbugs.cloud.doNothingCloud
 detectorAppendingToAnObjectOutputStream=AppendingToAnObjectOutputStream|true
 detectorAtomicityProblem=AtomicityProblem|true
 detectorBadAppletConstructor=BadAppletConstructor|false
diff --git .settings/org.eclipse.jdt.core.prefs .settings/org.eclipse.jdt.core.prefs
index c4e4a7f..a04d4e1 100644
--- .settings/org.eclipse.jdt.core.prefs
+++ .settings/org.eclipse.jdt.core.prefs
@@ -15,10 +15,9 @@ org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonN
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
 org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
-org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=11
 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
-org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.compliance=11
 org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
@@ -121,7 +120,8 @@ org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
 org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
 org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
 org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
-org.eclipse.jdt.core.compiler.source=1.8
+org.eclipse.jdt.core.compiler.release=enabled
+org.eclipse.jdt.core.compiler.source=11
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
diff --git .settings/org.sonar.ide.eclipse.core.prefs .settings/org.sonar.ide.eclipse.core.prefs
deleted file mode 100644
index 4ab0dd3..0000000
--- .settings/org.sonar.ide.eclipse.core.prefs
+++ /dev/null
@@ -1,6 +0,0 @@
-eclipse.preferences.version=1
-extraProperties=
-lastAnalysisDate=1438527074033
-projectKey=jmapviewer
-serverUrl=https\://josm.openstreetmap.de/sonar
-version=2
diff --git CONTRIBUTING.md CONTRIBUTING.md
index ee9f19a..416fa57 100644
--- CONTRIBUTING.md
+++ CONTRIBUTING.md
@@ -1,4 +1,6 @@
 # Contributing
+## Requirements
+* JDK 11 or later
 ## How to test
 * Unit tests: `mvn test`
 * Manual testing: `mvn compile exec:java -Dexec.mainClass=org.openstreetmap.gui.jmapviewer.Demo`
@@ -22,10 +24,9 @@ See [https://josm.openstreetmap.de/wiki/DevelopersGuide/PatchGuide](https://josm
 * `ant test` -> `mvn test`
 * `ant svn_info` -> manual pom.xml editing or `mvn release:update-versions -DdevelopmentVersion=$(svn propget ReleaseVersion .)-SNAPSHOT`
 * `ant pack` -> `mvn package` (note: slightly different source filename, doesn't merge source and class jars)
-* `ant create_run_jar` -> not migrated (use `mvn exec:java -Dexec.mainClass="org.openstreetmap.gui.jmapviewer.Demo"` instead)
+* `ant create_run_jar` -> not needed, the jar from `mvn package` is runnable
 * `ant spotbugs` -> `mvn spotbugs:check`
 * `ant checkstyle` -> `mvn checkstyle:checkstyle`
 * `ant javadoc` -> `mvn javadoc:javadoc`
-* `ant create_release_zip` -> `mvn jar:jar`
-* `ant create_source_release_zip` -> `mvn source:jar`
+* `ant create_release_zip`, `ant create_source_release_zip` -> `mvn -Prelease package`, written to `releases/${version}`
 * `ant checkdepsupdate` -> `mvn versions:display-dependency-updates` (note: please try to keep the version information in sync)
diff --git Readme.txt Readme.txt
index 9346474..3e8d6f6 100644
--- Readme.txt
+++ Readme.txt
@@ -21,7 +21,7 @@ JMapViewer
 (c) 2015-2019, Wiktor Niesiobędzki
 (c) 2017, Robert Scott
 
-This work bases partly on the JOSM plugin "Slippy Map Chooser" by Tim Haussmann
+This work is partly based on the JOSM plugin "Slippy Map Chooser" by Tim Haussmann
 
 License: GPL
 
@@ -29,17 +29,22 @@ FAQ:
 
 1. What is JMapViewer?
 
-JMapViewer is a Java Swing component for integrating OSM maps in to your Java 
-application. JMapViewer allows you to set markers on the map or zoom to a specific 
+JMapViewer is a Java Swing component for integrating OSM maps into your Java
+application. JMapViewer lets you set markers on the map or zoom to a specific
 location on the map.
 
 2. How does JMapViewer work?
 
-JMapViewer loads bitmap tiles from the OpenStreetmap tile server (Mapnik renderer).
-Therefore any application using JMapViewer requires a working Internet connection.    
+JMapViewer loads bitmap tiles from the OpenStreetMap tile server (Mapnik renderer).
+Therefore, any application using JMapViewer requires a working Internet connection.
 
 3. How do I use JMapViewer in my application?
 
-You can just create an instance of the class org.openstreetmap.gui.jmapviewer.JMapViewer
-using the default constructor and add it to your panel/frame/windows.
-For more details please see the Demo class in the same package.
+Just create an instance of the class org.openstreetmap.gui.jmapviewer.JMapViewer
+using the default constructor and add it to your panel, frame or window.
+For more details, please see the Demo class in the same package.
+
+4. What do I need to run JMapViewer?
+
+Java 11 or later. JMapViewer only uses the java.desktop and java.logging modules of the
+Java runtime, and no other libraries.
diff --git build.xml build.xml
index 3d07301..a4cd871 100644
--- build.xml
+++ build.xml
@@ -5,20 +5,23 @@
          xmlns:mvn="antlib:org.apache.maven.resolver.ant"
 >
 
-    <property name="java.lang.version" value="8" />
-    <dirname property="base.dir" file="${ant.file.jmapviewer}"/>
-    <property name="maven.artifact.resolver.version" value="1.5.1"/>
+    <!-- Keep in sync with maven.compiler.release and jmapviewer.buildJdk in pom.xml -->
+    <property name="java.lang.version" value="11" />
+    <property name="java.build.version" value="11" />
+    <fail message="Building JMapViewer needs JDK ${java.build.version} or later, the classes are compiled for Java ${java.lang.version}">
+        <condition><not><javaversion atleast="${java.build.version}"/></not></condition>
+    </fail>
+    <property name="maven.artifact.resolver.version" value="2.0.0"/>
     <property name="maven.artifact.resolver.parent.dir" value="${user.home}/.m2/repository/org/apache/maven/resolver/maven-resolver-ant-tasks/${maven.artifact.resolver.version}/"/>
     <property name="jacoco.includes" value="org.openstreetmap.gui.jmapviewer.*" />
     <property name="jacoco.inclbootstrapclasses" value="false" />
     <property name="jacoco.inclnolocationclasses" value="false" />
-    <!-- For Java specific stuff by version -->
-    <condition property="isJava9"><matches string="${ant.java.version}" pattern="(1.)?(9|[2-9][0-9])" /></condition>
-    <condition property="isJava23"><matches string="${ant.java.version}" pattern="2[3-9]|[3-9][0-9]" /></condition>
-    <!-- Disable jacoco 0.8.11 on Java 23+, see https://www.jacoco.org/jacoco/trunk/doc/changes.html for latest supported version -->
+    <!-- Disable jacoco on the Java versions it does not support yet, see https://www.jacoco.org/jacoco/trunk/doc/changes.html.
+         Update the pattern together with version.maven.jacoco in pom.xml. -->
+    <condition property="isJavaUnsupportedByJacoco"><matches string="${ant.java.version}" pattern="2[8-9]|[3-9][0-9]" /></condition>
     <condition property="coverageByDefault">
         <not>
-            <isset property="isJava23"/>
+            <isset property="isJavaUnsupportedByJacoco"/>
         </not>
     </condition>
     <path id="test.classpath">
@@ -50,21 +53,9 @@
     </target>
 
     <target name="build" depends="clean">
-        <!-- compile module info - we need to compile everything to ensure that all requires are there -->
-        <javac srcdir="src" destdir="bin" release="9" debug="true" includeantruntime="false" encoding="UTF-8" if:set="isJava9">
-            <include name="module-info.java" />
-            <include name="org/openstreetmap/gui/jmapviewer/**" />
-        </javac>
-
-        <!-- Remove the files compiled with Java 9 so that the next compile stage will compile to the appropriate version -->
-        <delete>
-            <fileset dir="bin">
-                <include name="org/openstreetmap/gui/jmapviewer/**" />
-            </fileset>
-        </delete>
-
         <javac srcdir="src" destdir="bin" release="${java.lang.version}" debug="true" includeantruntime="false" encoding="UTF-8">
-            <include name="org/openstreetmap/gui/jmapviewer/**" />
+            <compilerarg value="-Xlint:all"/>
+            <compilerarg value="-Xlint:-serial"/>
         </javac>
 
         <copy todir="bin">
@@ -81,20 +72,14 @@
             <env key="LANG" value="en_US"/>
         </exec>
     </target>
-    
+
     <target name="pack" depends="build">
         <!-- Create the JAR file containing the compiled class files -->
-        <jar destfile="JMapViewer.jar" filesetmanifest="mergewithoutmain">
-            <fileset dir="bin" includes="module-info.class,**/jmapviewer/**" />
-        </jar>
+        <jar destfile="JMapViewer.jar" basedir="bin" filesetmanifest="mergewithoutmain"/>
         <!-- Create the JAR file containing the source java files -->
-        <jar destfile="JMapViewer_src.jar" filesetmanifest="mergewithoutmain">
-            <fileset dir="src" includes="module-info.java,**/jmapviewer/**" />
-        </jar>
+        <jar destfile="JMapViewer_src.jar" basedir="src" filesetmanifest="mergewithoutmain"/>
     </target>
-    
-    <!-- if you want to build outside of svn, use "ant clean build [pack]" -->
-    
+
     <target name="create_run_jar" description="Create a JAR file that can be used to execute the JMapViewer demo app. Requires JMapViewer.jar to be present.">
         <jar destfile="JMapViewer_Demo.jar" filesetmanifest="mergewithoutmain">
             <manifest>
@@ -112,15 +97,14 @@
         </zip>
         <delete>
             <fileset dir="." includes="JMapViewer*.jar"/>
-        </delete> 
+        </delete>
     </target>
-    
+
     <target name="create_source_release_zip" description="Create a release zip file containing the source files">
+        <!-- Same content as tools/assembly/source-release.xml, so that the unpacked zip builds with Ant and Maven -->
         <zip destfile="releases/${svnReleaseVersion}/JMapViewer-${svnReleaseVersion}-Source.zip">
-            <zipfileset file="Readme.txt" prefix="jmapviewer-${svnReleaseVersion}"/>
-            <zipfileset file="build.xml" prefix="jmapviewer-${svnReleaseVersion}"/>
-            <zipfileset file="Gpl.txt" prefix="jmapviewer-${svnReleaseVersion}"/>
-            <zipfileset dir="src" includes="**/jmapviewer/**" prefix="jmapviewer-${svnReleaseVersion}/src"/>
+            <zipfileset dir="." includes="Readme.txt,Gpl.txt,build.xml,pom.xml,src/**,test/**,tools/**"
+                        prefix="jmapviewer-${svnReleaseVersion}"/>
         </zip>
     </target>
 
@@ -128,14 +112,15 @@
         <taskdef resource="com/puppycrawl/tools/checkstyle/ant/checkstyle-ant-task.properties"
             classpathref="checkstyle.classpath"/>
         <checkstyle config="${basedir}/tools/checkstyle/jmapviewer_checks.xml">
-            <!-- Exclude the module-info since checkstyle currently cannot parse it -->
-            <fileset dir="${basedir}/src" includes="**/*.java" excludes="module-info.java" />
+            <property key="config_loc" value="${basedir}/tools/checkstyle"/>
+            <fileset dir="${basedir}/src" includes="**/*.java" />
+            <fileset dir="${basedir}/test" includes="**/*.java" />
             <formatter type="xml" toFile="checkstyle-jmapviewer.xml"/>
         </checkstyle>
     </target>
 
     <target name="spotbugs" depends="pack,resolve">
-        <taskdef name="spotbugs" classname="edu.umd.cs.findbugs.anttask.FindBugsTask" 
+        <taskdef name="spotbugs" classname="edu.umd.cs.findbugs.anttask.FindBugsTask"
             classpathref="spotbugs.classpath"/>
         <property name="spotbugs-classpath" refid="spotbugs.classpath"/>
         <spotbugs output="xml"
@@ -149,19 +134,20 @@
     </target>
 
     <target name="javadoc">
-        <javadoc destdir="javadoc" 
+        <javadoc destdir="javadoc"
                 sourcepath="src"
-                encoding="UTF-8"    
+                encoding="UTF-8"
                 packagenames="org.openstreetmap.gui.jmapviewer.*"
                 windowtitle="JMapViewer"
                 use="true"
                 private="true"
                 linksource="true"
                 author="false">
-            <link href="https://docs.oracle.com/javase/8/docs/api"/>
+            <link href="https://docs.oracle.com/en/java/javase/${java.lang.version}/docs/api"/>
             <doctitle><![CDATA[<h2>JMapViewer - Javadoc</h2>]]></doctitle>
             <bottom><![CDATA[<a href="https://josm.openstreetmap.de/">JMapViewer</a>]]></bottom>
-            <arg value="-html5" if:set="isJava9" />
+            <arg value="--release"/>
+            <arg value="${java.lang.version}"/>
         </javadoc>
     </target>
 
@@ -204,14 +190,15 @@
                 <mvn:dependency groupId="com.github.spotbugs" artifactId="spotbugs" version="${pom.properties.version.maven.spotbugs}" scope="compile"/>
                 <mvn:dependency groupId="com.github.spotbugs" artifactId="spotbugs-ant" version="${pom.properties.version.maven.spotbugs}" scope="compile"/>
             </mvn:dependencies>
-            <mvn:path refid="spotbugs.classpath" classpath="compile"/>
+            <!-- SpotBugs needs its runtime dependencies (jaxen) to load its own plugin descriptor -->
+            <mvn:path refid="spotbugs.classpath" classpath="runtime"/>
         </mvn:resolve>
     </target>
 
-    <target name="test" depends="clean, build, resolve">
+    <target name="test" depends="build, resolve">
         <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml" classpathref="jacoco.classpath"/>
         <javac srcdir="test" destdir="bintest"
-            target="${java.lang.version}" source="${java.lang.version}" debug="on"
+            release="${java.lang.version}" debug="on"
             includeantruntime="false" createMissingPackageInfoClass="false" encoding="UTF-8">
             <compilerarg value="-Xlint:all"/>
             <compilerarg value="-Xlint:-serial"/>
diff --git pom.xml pom.xml
index 926c75d..5cae331 100644
--- pom.xml
+++ pom.xml
@@ -11,10 +11,17 @@
     <url>https://josm.openstreetmap.de/browser/osm/applications/viewer/jmapviewer</url>
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <maven.compiler.release>8</maven.compiler.release>
-        <version.maven.spotbugs>4.8.6</version.maven.spotbugs>
-        <version.maven.jacoco>0.8.12</version.maven.jacoco>
-        <version.maven.checkstyle>9.3</version.maven.checkstyle>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <maven.compiler.release>11</maven.compiler.release>
+        <!-- The JDK needed to run the build tools; the classes are compiled for maven.compiler.release -->
+        <jmapviewer.buildJdk>11</jmapviewer.buildJdk>
+        <jmapviewer.mainClass>org.openstreetmap.gui.jmapviewer.Demo</jmapviewer.mainClass>
+        <!-- Overwritten by the JaCoCo agent, see maven-surefire-plugin -->
+        <argLine></argLine>
+        <version.maven.spotbugs>4.10.4</version.maven.spotbugs>
+        <version.maven.jacoco>0.8.15</version.maven.jacoco>
+        <!-- 10.x is the last line that runs on Java 11, see jmapviewer.buildJdk -->
+        <version.maven.checkstyle>10.26.1</version.maven.checkstyle>
         <sonar.host.url>https://josm.openstreetmap.de/sonar</sonar.host.url>
         <sonar.projectKey>jmapviewer</sonar.projectKey>
     </properties>
@@ -97,7 +104,8 @@
             <dependency>
                 <groupId>org.junit</groupId>
                 <artifactId>junit-bom</artifactId>
-                <version>5.10.2</version>
+                <!-- JUnit 6 needs Java 17, while the tests are compiled for maven.compiler.release -->
+                <version>5.14.4</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
@@ -105,22 +113,6 @@
     </dependencyManagement>
 
     <dependencies>
-        <dependency>
-            <groupId>com.github.spotbugs</groupId>
-            <artifactId>spotbugs-annotations</artifactId>
-            <version>${version.maven.spotbugs}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.platform</groupId>
-            <artifactId>junit-platform-launcher</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.platform</groupId>
-            <artifactId>junit-platform-suite</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
@@ -132,8 +124,9 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-params</artifactId>
+            <!-- Surefire brings its own, but the Ant "junitlauncher" task runs off the test classpath -->
+            <groupId>org.junit.platform</groupId>
+            <artifactId>junit-platform-launcher</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
@@ -143,6 +136,7 @@
         <testSourceDirectory>${project.basedir}/test</testSourceDirectory>
         <finalName>${project.name}-${project.version}</finalName>
         <resources>
+            <!-- Images live next to the sources, as the source layout predates the Maven conventions -->
             <resource>
                 <directory>${project.build.sourceDirectory}</directory>
                 <includes>
@@ -155,54 +149,131 @@
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
-                <version>3.4.2</version>
-                <configuration>
-                    <outputDirectory>releases/${project.version}</outputDirectory>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>3.3.1</version>
-                <configuration>
-                    <outputDirectory>releases/${project.version}</outputDirectory>
-                </configuration>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>3.6.3</version>
                 <executions>
                     <execution>
-                        <id>attach-sources</id>
-                        <phase>package</phase>
+                        <id>enforce</id>
                         <goals>
-                            <goal>jar-no-fork</goal>
+                            <goal>enforce</goal>
                         </goals>
+                        <configuration>
+                            <rules>
+                                <requireMavenVersion>
+                                    <version>3.6.3</version>
+                                </requireMavenVersion>
+                                <requireJavaVersion>
+                                    <version>${jmapviewer.buildJdk}</version>
+                                </requireJavaVersion>
+                            </rules>
+                        </configuration>
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <version>3.5.0</version>
+            </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.13.0</version>
+                <version>3.16.0</version>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-Xlint:all</arg>
+                        <arg>-Xlint:-serial</arg>
+                    </compilerArgs>
+                </configuration>
                 <executions>
                     <execution>
-                        <id>default-compile</id>
+                        <id>default-testCompile</id>
                         <configuration>
-                            <release>11</release>
+                            <!-- The tests live in the same packages as the classes they test, so they run on the classpath -->
+                            <useModulePath>false</useModulePath>
                         </configuration>
                     </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.6.0</version>
+                <configuration>
+                    <!-- @{argLine} picks up the JaCoCo agent; the argLine property is empty when JaCoCo is skipped -->
+                    <argLine>@{argLine} -Dfile.encoding=UTF-8</argLine>
+                    <useModulePath>false</useModulePath>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.5.1</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <!-- Makes the jar runnable, replacing the Ant-only JMapViewer_Demo.jar -->
+                            <mainClass>${jmapviewer.mainClass}</mainClass>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.4.0</version>
+                <executions>
                     <execution>
-                        <id>base-compile</id>
+                        <id>attach-sources</id>
+                        <phase>package</phase>
                         <goals>
-                            <goal>compile</goal>
+                            <goal>jar-no-fork</goal>
                         </goals>
-                        <configuration>
-                            <release>8</release>
-                            <excludes>
-                                <exclude>module-info.java</exclude>
-                            </excludes>
-                        </configuration>
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.12.0</version>
+                <configuration>
+                    <show>private</show>
+                    <linksource>true</linksource>
+                    <windowtitle>${project.name}</windowtitle>
+                    <doctitle>&lt;h2&gt;${project.name} - Javadoc&lt;/h2&gt;</doctitle>
+                    <bottom>&lt;a href="https://josm.openstreetmap.de/"&gt;${project.name}&lt;/a&gt;</bottom>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-clean-plugin</artifactId>
+                <version>3.5.0</version>
+                <configuration>
+                    <!-- The reports that the quality plugins write outside of target/ -->
+                    <filesets>
+                        <fileset>
+                            <directory>${project.basedir}</directory>
+                            <includes>
+                                <include>checkstyle-jmapviewer.xml</include>
+                                <include>spotbugs-jmapviewer.xml</include>
+                            </includes>
+                        </fileset>
+                    </filesets>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <version>3.6.4</version>
+                <configuration>
+                    <mainClass>${jmapviewer.mainClass}</mainClass>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>versions-maven-plugin</artifactId>
+                <version>2.22.0</version>
+            </plugin>
             <!-- Start optional plugins (these can be removed by Linux distributions for building) -->
             <plugin>
                 <groupId>org.jacoco</groupId>
@@ -214,6 +285,12 @@
                         <goals>
                             <goal>prepare-agent</goal>
                         </goals>
+                        <configuration>
+                            <!-- Measure JMapViewer only, not JUnit and the other classes on the test classpath -->
+                            <includes>
+                                <include>org.openstreetmap.gui.jmapviewer.*</include>
+                            </includes>
+                        </configuration>
                     </execution>
                     <execution>
                         <id>default-report</id>
@@ -226,34 +303,34 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>4.8.6.2</version>
+                <!-- 4.10.4.1 and later require Maven 3.8.9, see maven-enforcer-plugin -->
+                <version>4.10.3.0</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>com.github.spotbugs</groupId>
+                        <artifactId>spotbugs</artifactId>
+                        <version>${version.maven.spotbugs}</version>
+                    </dependency>
+                </dependencies>
                 <configuration>
                     <xmlOutput>true</xmlOutput>
+                    <spotbugsXmlOutputDirectory>${project.basedir}</spotbugsXmlOutputDirectory>
                     <spotbugsXmlOutputFilename>spotbugs-jmapviewer.xml</spotbugsXmlOutputFilename>
                     <effort>max</effort>
                 </configuration>
-                <!-- This is commented out until all the current bugs are fixed. TODO uncomment!
-                <executions>
-                    <execution>
-                        <id>validate</id>
-                        <phase>validate</phase>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-                -->
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.4.0</version>
+                <version>3.6.0</version>
                 <configuration>
                     <configLocation>${project.basedir}/tools/checkstyle/jmapviewer_checks.xml</configLocation>
                     <includeTestSourceDirectory>true</includeTestSourceDirectory>
                     <outputFile>${project.basedir}/checkstyle-jmapviewer.xml</outputFile>
-                    <!-- checkstyle cannot parse module-info.java yet -->
-                    <excludes>module-info.java</excludes>
+                    <!-- The checks are declared with severity "warning", so the build has to fail on those -->
+                    <violationSeverity>warning</violationSeverity>
+                    <!-- Lets the rules reference the suppressions file the same way under Maven, Ant and Eclipse -->
+                    <propertyExpansion>config_loc=${project.basedir}/tools/checkstyle</propertyExpansion>
                 </configuration>
                 <dependencies>
                     <dependency>
@@ -272,35 +349,63 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-enforcer-plugin</artifactId>
-                <version>3.5.0</version>
-                <executions>
-                    <execution>
-                        <id>enforce-maven</id>
-                        <goals>
-                            <goal>enforce</goal>
-                        </goals>
-                        <configuration>
-                            <rules>
-                                <requireMavenVersion>
-                                    <version>3.6.3</version>
-                                </requireMavenVersion>
-                            </rules>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
+            <!-- End optional plugins -->
         </plugins>
     </build>
 
+    <profiles>
+        <!-- Builds the release zips below releases/, like the Ant "create_*_release_zip" targets -->
+        <profile>
+            <id>release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-assembly-plugin</artifactId>
+                        <version>3.8.0</version>
+                        <configuration>
+                            <outputDirectory>${project.basedir}/releases/${project.version}</outputDirectory>
+                            <attach>false</attach>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>release-zip</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <configuration>
+                                    <appendAssemblyId>false</appendAssemblyId>
+                                    <descriptors>
+                                        <descriptor>${project.basedir}/tools/assembly/release.xml</descriptor>
+                                    </descriptors>
+                                </configuration>
+                            </execution>
+                            <execution>
+                                <id>source-release-zip</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>single</goal>
+                                </goals>
+                                <configuration>
+                                    <descriptors>
+                                        <descriptor>${project.basedir}/tools/assembly/source-release.xml</descriptor>
+                                    </descriptors>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
     <reporting>
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-report-plugin</artifactId>
-                <version>3.3.1</version>
+                <version>3.6.0</version>
             </plugin>
             <plugin>
                 <groupId>org.jacoco</groupId>
diff --git src/module-info.java src/module-info.java
index f116957..2d95f0a 100644
--- src/module-info.java
+++ src/module-info.java
@@ -1,7 +1,8 @@
 // License: GPL. For details, see Readme.txt file.
 module org.openstreetmap.gui.jmapviewer {
-    requires java.desktop;
-    requires java.logging;
+    // java.desktop and java.logging types are part of the public API (JMapViewer is a JPanel, FeatureAdapter returns loggers)
+    requires transitive java.desktop;
+    requires transitive java.logging;
     exports org.openstreetmap.gui.jmapviewer;
     exports org.openstreetmap.gui.jmapviewer.checkBoxTree;
     exports org.openstreetmap.gui.jmapviewer.events;
diff --git src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
index 4e5216e..57fd3db 100644
--- src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
+++ src/org/openstreetmap/gui/jmapviewer/AbstractLayer.java
@@ -36,6 +36,7 @@ public class AbstractLayer {
         this(parent, name, null, style);
     }
 
+    @SuppressWarnings("this-escape")
     public AbstractLayer(LayerGroup parent, String name, String description, Style style) {
         setParent(parent);
         setName(name);
diff --git src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java
index 9d67ae6..4e003c7 100644
--- src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java
+++ src/org/openstreetmap/gui/jmapviewer/AttributionSupport.java
@@ -12,15 +12,17 @@ import java.awt.Rectangle;
 import java.awt.font.TextAttribute;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.ImageObserver;
-import java.util.HashMap;
 import java.util.Map;
 
 import org.openstreetmap.gui.jmapviewer.interfaces.Attributed;
 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
 
+/**
+ * Paints the attribution of a tile source onto the map and handles clicks on it.
+ */
 public class AttributionSupport {
     public static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
-    public static final Font ATTR_LINK_FONT;
+    public static final Font ATTR_LINK_FONT = ATTR_FONT.deriveFont(Map.of(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON));
     private static final int PADDING = 5;
 
     private Attributed source;
@@ -33,16 +35,16 @@ public class AttributionSupport {
     protected Rectangle attrToUBounds;
     protected Rectangle attrImageBounds;
 
-    static {
-        Map<TextAttribute, Integer> aUnderline = new HashMap<>();
-        aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
-        ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
+    /**
+     * Constructs a new {@code AttributionSupport}, to be initialized with {@link #initialize(Attributed)}.
+     */
+    public AttributionSupport() {
+        // Nothing to do
     }
 
     public void initialize(Attributed source) {
         this.source = source;
-        boolean requireAttr = source.requiresAttribution();
-        if (requireAttr) {
+        if (source.requiresAttribution()) {
             attrImage = source.getAttributionImage();
             attrTermsText = source.getTermsOfUseText();
             attrTermsUrl = source.getTermsOfUseURL();
@@ -51,6 +53,7 @@ public class AttributionSupport {
             }
         } else {
             attrImage = null;
+            attrTermsText = null;
             attrTermsUrl = null;
         }
     }
@@ -75,10 +78,9 @@ public class AttributionSupport {
 
         if (attrTermsText != null) {
             Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds(attrTermsText, g);
-            int textRealHeight = (int) termsStringBounds.getHeight();
-            termsTextHeight = textRealHeight;
+            termsTextHeight = (int) termsStringBounds.getHeight();
             int termsTextWidth = (int) termsStringBounds.getWidth();
-            attrToUBounds = new Rectangle(PADDING, textY - termsTextHeight + fontDescent, termsTextWidth, textRealHeight);
+            attrToUBounds = new Rectangle(PADDING, textY - termsTextHeight + fontDescent, termsTextWidth, termsTextHeight);
             drawShadedText(g, attrTermsText, PADDING, textY);
         } else {
             attrToUBounds = null;
@@ -90,7 +92,7 @@ public class AttributionSupport {
             int imgHeight = attrImage.getHeight(observer);
             int y = textY - imgHeight - termsTextHeight;
             attrImageBounds = new Rectangle(PADDING, y, imgWidth, imgHeight);
-            g.drawImage(attrImage, PADDING, y, null);
+            g.drawImage(attrImage, PADDING, y, observer);
         } else {
             attrImageBounds = null;
         }
@@ -139,7 +141,7 @@ public class AttributionSupport {
     }
 
     public boolean handleAttribution(Point p, boolean click) {
-        if (source == null || !source.requiresAttribution())
+        if (p == null || source == null || !source.requiresAttribution())
             return false;
 
         if (attrTextBounds != null && attrTextBounds.contains(p)) {
@@ -154,7 +156,7 @@ public class AttributionSupport {
             String attributionImageURL = source.getAttributionImageURL();
             if (attributionImageURL != null) {
                 if (click) {
-                    FeatureAdapter.openLink(source.getAttributionImageURL());
+                    FeatureAdapter.openLink(attributionImageURL);
                 }
                 return true;
             }
@@ -169,5 +171,4 @@ public class AttributionSupport {
         }
         return false;
     }
-
 }
diff --git src/org/openstreetmap/gui/jmapviewer/Coordinate.java src/org/openstreetmap/gui/jmapviewer/Coordinate.java
index 3c9c2e2..ea6c6fb 100644
--- src/org/openstreetmap/gui/jmapviewer/Coordinate.java
+++ src/org/openstreetmap/gui/jmapviewer/Coordinate.java
@@ -13,9 +13,9 @@ import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
  * A geographical coordinate consisting of latitude and longitude.
  *
  * @author Jan Peter Stotz
- *
  */
 public class Coordinate implements ICoordinate, Serializable {
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 1L;
     private double x;
     private double y;
@@ -25,6 +25,7 @@ public class Coordinate implements ICoordinate, Serializable {
      * @param lat latitude in degrees
      * @param lon longitude in degrees
      */
+    @SuppressWarnings("this-escape")
     public Coordinate(double lat, double lon) {
         setLat(lat);
         setLon(lon);
@@ -50,11 +51,13 @@ public class Coordinate implements ICoordinate, Serializable {
         x = lon;
     }
 
+    // Java 14: annotate with @Serial
     private void writeObject(ObjectOutputStream out) throws IOException {
         out.writeObject(x);
         out.writeObject(y);
     }
 
+    // Java 14: annotate with @Serial
     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
         x = (Double) in.readObject();
         y = (Double) in.readObject();
@@ -73,7 +76,7 @@ public class Coordinate implements ICoordinate, Serializable {
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
-        if (o == null || !this.getClass().equals(o.getClass())) return false;
+        if (o == null || !getClass().equals(o.getClass())) return false;
         Coordinate that = (Coordinate) o;
         return Double.compare(that.x, x) == 0 && Double.compare(that.y, y) == 0;
     }
diff --git src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
index ae79b0d..4fc9207 100644
--- src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
+++ src/org/openstreetmap/gui/jmapviewer/DefaultMapController.java
@@ -14,13 +14,12 @@ import java.util.Locale;
  * mouse button and zooming by double click or by mouse wheel.
  *
  * @author Jan Peter Stotz
- *
  */
 public class DefaultMapController extends JMapController implements MouseListener, MouseMotionListener,
-MouseWheelListener {
+        MouseWheelListener {
 
     private static final int MOUSE_BUTTONS_MASK = MouseEvent.BUTTON3_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK
-    | MouseEvent.BUTTON2_DOWN_MASK;
+            | MouseEvent.BUTTON2_DOWN_MASK;
 
     private static final int MAC_MOUSE_BUTTON3_MASK = MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON1_DOWN_MASK;
 
@@ -51,14 +50,19 @@ MouseWheelListener {
         // Is only the selected mouse button pressed?
         if ((e.getModifiersEx() & MOUSE_BUTTONS_MASK) == movementMouseButtonMask
                 || (isPlatformOsx() && e.getModifiersEx() == MAC_MOUSE_BUTTON3_MASK)) {
-            Point p = e.getPoint();
-            if (lastDragPoint != null) {
-                int diffx = lastDragPoint.x - p.x;
-                int diffy = lastDragPoint.y - p.y;
-                map.moveMap(diffx, diffy);
-            }
-            lastDragPoint = p;
+            dragTo(e.getPoint());
+        }
+    }
+
+    /**
+     * Moves the map by the distance from the previous drag point to {@code p}.
+     * @param p the current mouse position
+     */
+    private void dragTo(Point p) {
+        if (lastDragPoint != null) {
+            map.moveMap(lastDragPoint.x - p.x, lastDragPoint.y - p.y);
         }
+        lastDragPoint = p;
     }
 
     @Override
@@ -125,6 +129,7 @@ MouseWheelListener {
      */
     public void setMovementMouseButton(int movementMouseButton) {
         this.movementMouseButton = movementMouseButton;
+        // Java 14: use a switch expression
         switch (movementMouseButton) {
             case MouseEvent.BUTTON1:
                 movementMouseButtonMask = MouseEvent.BUTTON1_DOWN_MASK;
@@ -168,26 +173,19 @@ MouseWheelListener {
 
     @Override
     public void mouseMoved(MouseEvent e) {
-        // Mac OSX simulates with  ctrl + mouse 1  the second mouse button hence no dragging events get fired.
-        //
+        // Mac OSX simulates the second mouse button with ctrl + mouse 1, hence no dragging events get fired.
         if (isPlatformOsx()) {
             if (!movementEnabled || !isMoving)
                 return;
             // Is only the selected mouse button pressed?
             if (e.getModifiersEx() == MouseEvent.CTRL_DOWN_MASK) {
-                Point p = e.getPoint();
-                if (lastDragPoint != null) {
-                    int diffx = lastDragPoint.x - p.x;
-                    int diffy = lastDragPoint.y - p.y;
-                    map.moveMap(diffx, diffy);
-                }
-                lastDragPoint = p;
+                dragTo(e.getPoint());
             }
         }
     }
 
     /**
-     * Replies true if we are currently running on OSX
+     * Determines whether we are currently running on OSX.
      *
      * @return true if we are currently running on OSX
      */
diff --git src/org/openstreetmap/gui/jmapviewer/Demo.java src/org/openstreetmap/gui/jmapviewer/Demo.java
index 551005b..b3d1917 100644
--- src/org/openstreetmap/gui/jmapviewer/Demo.java
+++ src/org/openstreetmap/gui/jmapviewer/Demo.java
@@ -13,6 +13,7 @@ import javax.swing.JComboBox;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
 
 import org.openstreetmap.gui.jmapviewer.events.JMVCommandEvent;
 import org.openstreetmap.gui.jmapviewer.interfaces.JMapViewerEventListener;
@@ -26,10 +27,10 @@ import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
  * Demonstrates the usage of {@link JMapViewer}
  *
  * @author Jan Peter Stotz
- *
  */
-public class Demo extends JFrame implements JMapViewerEventListener {
+public final class Demo extends JFrame implements JMapViewerEventListener {
 
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 1L;
 
     private final JMapViewerTree treeMap;
@@ -62,17 +63,16 @@ public class Demo extends JFrame implements JMapViewerEventListener {
         JPanel helpPanel = new JPanel();
 
         mperpLabelName = new JLabel("Meters/Pixels: ");
-        mperpLabelValue = new JLabel(String.format("%s", map().getMeterPerPixel()));
+        mperpLabelValue = new JLabel(String.valueOf(map().getMeterPerPixel()));
 
         zoomLabel = new JLabel("Zoom: ");
-        zoomValue = new JLabel(String.format("%s", map().getZoom()));
+        zoomValue = new JLabel(String.valueOf(map().getZoom()));
 
         add(panel, BorderLayout.NORTH);
         add(helpPanel, BorderLayout.SOUTH);
         panel.add(panelTop, BorderLayout.NORTH);
         panel.add(panelBottom, BorderLayout.SOUTH);
-        JLabel helpLabel = new JLabel("Use right mouse button to move,\n "
-                + "left double click or mouse wheel to zoom.");
+        JLabel helpLabel = new JLabel("Use right mouse button to move, left double click or mouse wheel to zoom.");
         helpPanel.add(helpLabel);
         JButton button = new JButton("setDisplayToFitMapMarkers");
         button.addActionListener(e -> map().setDisplayToFitMapMarkers());
@@ -82,8 +82,7 @@ public class Demo extends JFrame implements JMapViewerEventListener {
                 new BingAerialTileSource(),
         });
         tileSourceSelector.addItemListener(e -> map().setTileSource((TileSource) e.getItem()));
-        JComboBox<TileLoader> tileLoaderSelector;
-        tileLoaderSelector = new JComboBox<>(new TileLoader[] {new OsmTileLoader(map())});
+        JComboBox<TileLoader> tileLoaderSelector = new JComboBox<>(new TileLoader[] {new OsmTileLoader(map())});
         tileLoaderSelector.addItemListener(e -> map().setTileLoader((TileLoader) e.getItem()));
         map().setTileLoader((TileLoader) tileLoaderSelector.getSelectedItem());
         panelTop.add(tileSourceSelector);
@@ -92,15 +91,12 @@ public class Demo extends JFrame implements JMapViewerEventListener {
         showMapMarker.setSelected(map().getMapMarkersVisible());
         showMapMarker.addActionListener(e -> map().setMapMarkerVisible(showMapMarker.isSelected()));
         panelBottom.add(showMapMarker);
-        ///
         final JCheckBox showTreeLayers = new JCheckBox("Tree Layers visible");
         showTreeLayers.addActionListener(e -> treeMap.setTreeVisible(showTreeLayers.isSelected()));
         panelBottom.add(showTreeLayers);
-        ///
         final JCheckBox showToolTip = new JCheckBox("ToolTip visible");
         showToolTip.addActionListener(e -> map().setToolTipText(null));
         panelBottom.add(showToolTip);
-        ///
         final JCheckBox showTileGrid = new JCheckBox("Tile grid visible");
         showTileGrid.setSelected(map().isTileGridVisible());
         showTileGrid.addActionListener(e -> map().setTileGridVisible(showTileGrid.isSelected()));
@@ -121,7 +117,6 @@ public class Demo extends JFrame implements JMapViewerEventListener {
 
         add(treeMap, BorderLayout.CENTER);
 
-        //
         LayerGroup germanyGroup = new LayerGroup("Germany");
         Layer germanyWestLayer = germanyGroup.addLayer("Germany West");
         Layer germanyEastLayer = germanyGroup.addLayer("Germany East");
@@ -153,9 +148,6 @@ public class Demo extends JFrame implements JMapViewerEventListener {
         Layer wales = treeMap.addLayer("UK");
         map().addMapRectangle(new MapRectangleImpl(wales, "Wales", c(53.35, -4.57), c(51.64, -2.63)));
 
-        // map.setDisplayPosition(new Coordinate(49.807, 8.6), 11);
-        // map.setTileGridVisible(true);
-
         map().addMouseListener(new MouseAdapter() {
             @Override
             public void mouseClicked(MouseEvent e) {
@@ -170,11 +162,7 @@ public class Demo extends JFrame implements JMapViewerEventListener {
             public void mouseMoved(MouseEvent e) {
                 Point p = e.getPoint();
                 boolean cursorHand = map().getAttribution().handleAttributionCursor(p);
-                if (cursorHand) {
-                    map().setCursor(new Cursor(Cursor.HAND_CURSOR));
-                } else {
-                    map().setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
-                }
+                map().setCursor(Cursor.getPredefinedCursor(cursorHand ? Cursor.HAND_CURSOR : Cursor.DEFAULT_CURSOR));
                 if (showToolTip.isSelected()) map().setToolTipText(map().getPosition(p).toString());
             }
         });
@@ -192,20 +180,20 @@ public class Demo extends JFrame implements JMapViewerEventListener {
      * @param args Main program arguments
      */
     public static void main(String[] args) {
-        new Demo().setVisible(true);
+        SwingUtilities.invokeLater(() -> new Demo().setVisible(true));
     }
 
     private void updateZoomParameters() {
+        // the map fires events while the constructor is still running, before the labels exist
         if (mperpLabelValue != null)
-            mperpLabelValue.setText(String.format("%s", map().getMeterPerPixel()));
+            mperpLabelValue.setText(String.valueOf(map().getMeterPerPixel()));
         if (zoomValue != null)
-            zoomValue.setText(String.format("%s", map().getZoom()));
+            zoomValue.setText(String.valueOf(map().getZoom()));
     }
 
     @Override
     public void processCommand(JMVCommandEvent command) {
-        if (command.getCommand().equals(JMVCommandEvent.COMMAND.ZOOM) ||
-                command.getCommand().equals(JMVCommandEvent.COMMAND.MOVE)) {
+        if (command.getCommand() == JMVCommandEvent.COMMAND.ZOOM || command.getCommand() == JMVCommandEvent.COMMAND.MOVE) {
             updateZoomParameters();
         }
     }
diff --git src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java
index 44650a3..b820917 100644
--- src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java
+++ src/org/openstreetmap/gui/jmapviewer/FeatureAdapter.java
@@ -61,6 +61,7 @@ public final class FeatureAdapter {
     /**
      * Translation support.
      */
+    @FunctionalInterface
     public interface TranslationAdapter {
         /**
          * Translates some text for the current locale.
@@ -76,7 +77,6 @@ public final class FeatureAdapter {
          * @return the translated string.
          */
         String tr(String text, Object... objects);
-        // TODO: more i18n functions
     }
 
     /**
@@ -217,13 +217,13 @@ public final class FeatureAdapter {
 
     /**
      * Reads an image using the current {@link ImageAdapter}.
-     * @param url image URI to read
+     * @param uri image URI to read
      * @return a <code>BufferedImage</code> containing the decoded contents of the input, or <code>null</code>.
      * @throws java.net.MalformedURLException if the URI could not be converted to a URL
      * @throws IOException if an error occurs during reading.
      */
-    public static BufferedImage readImage(URI url) throws IOException {
-        return imageAdapter.read(url.toURL(), false, false);
+    public static BufferedImage readImage(URI uri) throws IOException {
+        return imageAdapter.read(uri.toURL(), false, false);
     }
 
     /**
@@ -270,11 +270,12 @@ public final class FeatureAdapter {
     }
 
     /**
-     * Get settings value for a certain key and provide a default value.
+     * Get the integer settings value for a certain key and provide a default value.
      * @param key the identifier for the setting
      * @param def the default value. For each call of get() with a given key, the
-     * default value must be the same. {@code def} may be null.
+     * default value must be the same.
      * @return the corresponding value if the property has been set before, {@code def} otherwise
+     * @throws NumberFormatException if the value that has been set is not an integer
      */
     public static int getIntSetting(String key, int def) {
         return Integer.parseInt(settingsAdapter.get(key, Integer.toString(def)));
@@ -294,6 +295,13 @@ public final class FeatureAdapter {
      * Default API key support that relies on system property named {@code <imageryId>.api-key}.
      */
     public static class DefaultApiKeyAdapter implements ApiKeyAdapter {
+        /**
+         * Constructs a new {@code DefaultApiKeyAdapter}.
+         */
+        public DefaultApiKeyAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public String retrieveApiKey(String imageryId) {
             return System.getProperty(imageryId + ".api-key");
@@ -304,6 +312,13 @@ public final class FeatureAdapter {
      * Default browser support that relies on Java Desktop API.
      */
     public static class DefaultBrowserAdapter implements BrowserAdapter {
+        /**
+         * Constructs a new {@code DefaultBrowserAdapter}.
+         */
+        public DefaultBrowserAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public void openLink(String url) {
             if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
@@ -322,6 +337,13 @@ public final class FeatureAdapter {
      * Default image support that relies on Java Image IO API.
      */
     public static class DefaultImageAdapter implements ImageAdapter {
+        /**
+         * Constructs a new {@code DefaultImageAdapter}.
+         */
+        public DefaultImageAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public BufferedImage read(URL input, boolean readMetadata, boolean enforceTransparency) throws IOException {
             return ImageIO.read(input);
@@ -329,9 +351,16 @@ public final class FeatureAdapter {
     }
 
     /**
-     * Default "translation" support that do not really translates strings, but only takes care of formatting arguments.
+     * Default "translation" support that does not really translate strings, but only takes care of formatting arguments.
      */
     public static class DefaultTranslationAdapter implements TranslationAdapter {
+        /**
+         * Constructs a new {@code DefaultTranslationAdapter}.
+         */
+        public DefaultTranslationAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public String tr(String text, Object... objects) {
             return MessageFormat.format(text, objects);
@@ -342,6 +371,13 @@ public final class FeatureAdapter {
      * Default logging support that relies on Java Logging API.
      */
     public static class DefaultLoggingAdapter implements LoggingAdapter {
+        /**
+         * Constructs a new {@code DefaultLoggingAdapter}.
+         */
+        public DefaultLoggingAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public Logger getLogger(String name) {
             return Logger.getLogger(name);
@@ -354,6 +390,13 @@ public final class FeatureAdapter {
     public static class DefaultSettingsAdapter implements SettingsAdapter {
         private final Map<String, String> settings = new TreeMap<>();
 
+        /**
+         * Constructs a new {@code DefaultSettingsAdapter}.
+         */
+        public DefaultSettingsAdapter() {
+            // Nothing to do
+        }
+
         @Override
         public String get(String key, String def) {
             return settings.getOrDefault(key, def);
diff --git src/org/openstreetmap/gui/jmapviewer/JMapController.java src/org/openstreetmap/gui/jmapviewer/JMapController.java
index 8de5300..76d5c91 100644
--- src/org/openstreetmap/gui/jmapviewer/JMapController.java
+++ src/org/openstreetmap/gui/jmapviewer/JMapController.java
@@ -21,8 +21,15 @@ public abstract class JMapController {
 
     protected JMapViewer map;
 
-    public JMapController(JMapViewer map) {
+    /**
+     * Constructs a new {@code JMapController} and registers it as listener of the map,
+     * for each of the listener interfaces that it implements.
+     * @param map the map to control
+     */
+    @SuppressWarnings("this-escape")
+    protected JMapController(JMapViewer map) {
         this.map = map;
+        // Java 16: use instanceof pattern matching instead of the casts
         if (this instanceof MouseListener)
             map.addMouseListener((MouseListener) this);
         if (this instanceof MouseWheelListener)
@@ -30,5 +37,4 @@ public abstract class JMapController {
         if (this instanceof MouseMotionListener)
             map.addMouseMotionListener((MouseMotionListener) this);
     }
-
 }
diff --git src/org/openstreetmap/gui/jmapviewer/JMapViewer.java src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
index d998d03..3411279 100644
--- src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
+++ src/org/openstreetmap/gui/jmapviewer/JMapViewer.java
@@ -6,7 +6,6 @@ import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Insets;
 import java.awt.Point;
-import java.awt.event.MouseEvent;
 import java.io.IOException;
 import java.net.URL;
 import java.util.ArrayList;
@@ -42,6 +41,7 @@ import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
  */
 public class JMapViewer extends JPanel implements TileLoaderListener {
 
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 1L;
 
     /** whether debug mode is enabled or not */
@@ -53,7 +53,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     /**
      * Vectors for clock-wise tile painting
      */
-    private static final Point[] move = {new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1)};
+    private static final Point[] MOVE = {new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1)};
 
     /** Maximum zoom level */
     public static final int MAX_ZOOM = 24;
@@ -110,9 +110,9 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
      * Creates a standard {@link JMapViewer} instance that can be controlled via
      * mouse: hold right mouse button for moving, double click left mouse button
      * or use mouse wheel for zooming. Loaded tiles are stored in a
-     * {@link MemoryTileCache} and the tile loader uses 4 parallel threads for
-     * retrieving the tiles.
+     * {@link MemoryTileCache} and retrieved by an {@link OsmTileLoader}.
      */
+    @SuppressWarnings("this-escape")
     public JMapViewer() {
         this(new MemoryTileCache());
         new DefaultMapController(this);
@@ -121,19 +121,8 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     /**
      * Creates a new {@link JMapViewer} instance.
      * @param tileCache The cache where to store tiles
-     * @param downloadThreadCount not used anymore
-     * @deprecated use {@link #JMapViewer(TileCache)}
-     */
-    @Deprecated
-    public JMapViewer(TileCache tileCache, int downloadThreadCount) {
-        this(tileCache);
-    }
-
-    /**
-     * Creates a new {@link JMapViewer} instance.
-     * @param tileCache The cache where to store tiles
-     *
      */
+    @SuppressWarnings("this-escape")
     public JMapViewer(TileCache tileCache) {
         tileSource = new OsmTileSource.Mapnik();
         tileController = new TileController(tileSource, tileCache, this);
@@ -151,11 +140,6 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
         setDisplayPosition(new Coordinate(50, 9), 3);
     }
 
-    @Override
-    public String getToolTipText(MouseEvent event) {
-        return super.getToolTipText(event);
-    }
-
     protected void initializeZoomSlider() {
         zoomSlider = new JSlider(MIN_ZOOM, tileController.getTileSource().getMaxZoom());
         zoomSlider.setOrientation(JSlider.VERTICAL);
@@ -309,7 +293,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (markers && mapMarkerList != null) {
             synchronized (this) {
-                for (MapMarker marker : mapMarkerList) {
+                for (MapMarker marker : snapshot(mapMarkerList)) {
                     if (marker.isVisible()) {
                         Point p = tileSource.latLonToXY(marker.getCoordinate(), mapZoomMax);
                         xMax = Math.max(xMax, p.x);
@@ -323,7 +307,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (rectangles && mapRectangleList != null) {
             synchronized (this) {
-                for (MapRectangle rectangle : mapRectangleList) {
+                for (MapRectangle rectangle : snapshot(mapRectangleList)) {
                     if (rectangle.isVisible()) {
                         Point bottomRight = tileSource.latLonToXY(rectangle.getBottomRight(), mapZoomMax);
                         Point topLeft = tileSource.latLonToXY(rectangle.getTopLeft(), mapZoomMax);
@@ -338,7 +322,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (polygons && mapPolygonList != null) {
             synchronized (this) {
-                for (MapPolygon polygon : mapPolygonList) {
+                for (MapPolygon polygon : snapshot(mapPolygonList)) {
                     if (polygon.isVisible()) {
                         for (ICoordinate c : polygon.getPoints()) {
                             Point p = tileSource.latLonToXY(c, mapZoomMax);
@@ -473,13 +457,13 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     }
 
     /**
-     * Calculates the position on the map of a given coordinate
+     * Calculates the y position on the map of a coordinate that is shifted by an offset in latitude.
      *
      * @param lat Latitude
      * @param lon longitude
      * @param offset Offset respect Latitude
      * @param checkOutside check if the point is outside the displayed area
-     * @return Integer the radius in pixels
+     * @return the y position in pixels, or <code>null</code> if it is not visible and checkOutside is set to <code>true</code>
      */
     public Integer getLatOffset(double lat, double lon, double offset, boolean checkOutside) {
         Point p = tileSource.latLonToXY(lat + offset, lon, zoom);
@@ -491,19 +475,18 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     }
 
     /**
-     * Calculates the position on the map of a given coordinate
+     * Calculates the radius of a map marker on the map.
      *
      * @param marker MapMarker object that define the x,y coordinate
-     * @param p coordinate
-     * @return Integer the radius in pixels
+     * @param p position of the marker on the map
+     * @return the radius in pixels, or <code>null</code> if it cannot be determined
      */
     public Integer getRadius(MapMarker marker, Point p) {
         if (marker.getMarkerStyle() == MapMarker.STYLE.FIXED)
             return (int) marker.getRadius();
         else if (p != null) {
-            Integer radius = getLatOffset(marker.getLat(), marker.getLon(), marker.getRadius(), false);
-            radius = radius == null ? null : (p.y - radius);
-            return radius;
+            Integer offset = getLatOffset(marker.getLat(), marker.getLon(), marker.getRadius(), false);
+            return offset == null ? null : (p.y - offset);
         } else
             return null;
     }
@@ -593,7 +576,8 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
             } else {
                 iMove = 0;
             }
-        } // calculate the visibility borders
+        }
+        // calculate the visibility borders
         int xMin = -tilesize;
         int yMin = -tilesize;
         int xMax = getWidth();
@@ -617,7 +601,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
                         Tile tile;
                         if (scrollWrapEnabled) {
                             // in case tilex is out of bounds, grab the tile to use for wrapping
-                            int tilexWrap = ((tilex % gridLength) + gridLength) % gridLength;
+                            int tilexWrap = Math.floorMod(tilex, gridLength);
                             tile = tileController.getTile(tilexWrap, tiley, zoom);
                         } else {
                             tile = tileController.getTile(tilex, tiley, zoom);
@@ -630,13 +614,13 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
                         }
                         painted = true;
                     }
-                    Point p = move[iMove];
+                    Point p = MOVE[iMove];
                     posx += p.x * tilesize;
                     posy += p.y * tilesize;
                     tilex += p.x;
                     tiley += p.y;
                 }
-                iMove = (iMove + 1) % move.length;
+                iMove = (iMove + 1) % MOVE.length;
             }
         }
         // outer border of the map
@@ -648,8 +632,6 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
             g.drawRect(w2 - center.x, h2 - center.y, mapSize, mapSize);
         }
 
-        // g.drawString("Tiles in cache: " + tileCache.getTileCount(), 50, 20);
-
         // keep x-coordinates from growing without bound if scroll-wrap is enabled
         if (scrollWrapEnabled) {
             center.x = center.x % mapSize;
@@ -657,7 +639,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (mapPolygonsVisible && mapPolygonList != null) {
             synchronized (this) {
-                for (MapPolygon polygon : mapPolygonList) {
+                for (MapPolygon polygon : snapshot(mapPolygonList)) {
                     if (polygon.isVisible())
                         paintPolygon(g, polygon);
                 }
@@ -666,7 +648,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (mapRectanglesVisible && mapRectangleList != null) {
             synchronized (this) {
-                for (MapRectangle rectangle : mapRectangleList) {
+                for (MapRectangle rectangle : snapshot(mapRectangleList)) {
                     if (rectangle.isVisible())
                         paintRectangle(g, rectangle);
                 }
@@ -675,7 +657,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
 
         if (mapMarkersVisible && mapMarkerList != null) {
             synchronized (this) {
-                for (MapMarker marker : mapMarkerList) {
+                for (MapMarker marker : snapshot(mapMarkerList)) {
                     if (marker.isVisible())
                         paintMarker(g, marker);
                 }
@@ -685,6 +667,17 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
         attribution.paintAttribution(g, getWidth(), getHeight(), getPosition(0, 0), getPosition(getWidth(), getHeight()), zoom, this);
     }
 
+    /**
+     * Copies a list of map objects, so that it can be iterated while other threads change it.
+     * Copying a {@link Collections#synchronizedList} is atomic, unlike iterating over it.
+     * @param <T> type of the map objects
+     * @param list list to copy
+     * @return a copy of the list
+     */
+    private static <T> List<T> snapshot(List<T> list) {
+        return new ArrayList<>(list);
+    }
+
     /**
      * Paint a single marker.
      * @param g Graphics used for painting
@@ -818,7 +811,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
         center.x += x;
         center.y += y;
         repaint();
-        this.fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this));
+        fireJMVEvent(new JMVCommandEvent(COMMAND.MOVE, this));
     }
 
     /**
@@ -870,11 +863,10 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
                 || zoom == this.zoom)
             return;
         ICoordinate zoomPos = getPosition(mapPoint);
-        tileController.cancelOutstandingJobs(); // Clearing outstanding load
-        // requests
+        tileController.cancelOutstandingJobs(); // Clearing outstanding load requests
         setDisplayPosition(mapPoint, zoomPos, zoom);
 
-        this.fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this));
+        fireJMVEvent(new JMVCommandEvent(COMMAND.ZOOM, this));
     }
 
     /**
@@ -1067,16 +1059,6 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
         repaint();
     }
 
-    /**
-     * Sets whether zoom controls are displayed or not.
-     * @param visible {@code true} if zoom controls are displayed, {@code false} otherwise
-     * @deprecated use {@link #setZoomControlsVisible(boolean)}
-     */
-    @Deprecated
-    public void setZoomContolsVisible(boolean visible) {
-        setZoomControlsVisible(visible);
-    }
-
     /**
      * Sets whether zoom controls are displayed or not.
      * @param visible {@code true} if zoom controls are displayed, {@code false} otherwise
@@ -1182,7 +1164,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     }
 
     /**
-     * Returns the zoom controls apparence style (horizontal/vertical).
+     * Returns the zoom controls appearance style (horizontal/vertical).
      * @return {@link ZOOM_BUTTON_STYLE#VERTICAL} or {@link ZOOM_BUTTON_STYLE#HORIZONTAL}
      */
     public ZOOM_BUTTON_STYLE getZoomButtonStyle() {
@@ -1190,7 +1172,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
     }
 
     /**
-     * Sets the zoom controls apparence style (horizontal/vertical).
+     * Sets the zoom controls appearance style (horizontal/vertical).
      * @param style {@link ZOOM_BUTTON_STYLE#VERTICAL} or {@link ZOOM_BUTTON_STYLE#HORIZONTAL}
      */
     public void setZoomButtonStyle(ZOOM_BUTTON_STYLE style) {
@@ -1198,6 +1180,7 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
         if (zoomSlider == null || zoomInButton == null || zoomOutButton == null) {
             return;
         }
+        // Java 14: use a switch expression with arrow labels
         switch (style) {
         case VERTICAL:
             zoomSlider.setBounds(10, 27, 30, 150);
@@ -1267,11 +1250,10 @@ public class JMapViewer extends JPanel implements TileLoaderListener {
      * @param evt event to dispatch
      */
     private void fireJMVEvent(JMVCommandEvent evt) {
-        Object[] listeners = evtListenerList.getListenerList();
-        for (int i = 0; i < listeners.length; i += 2) {
-            if (listeners[i] == JMapViewerEventListener.class) {
-                ((JMapViewerEventListener) listeners[i + 1]).processCommand(evt);
-            }
+        // getListeners returns the most recently added listener first, notify in the order they were added
+        JMapViewerEventListener[] listeners = evtListenerList.getListeners(JMapViewerEventListener.class);
+        for (int i = listeners.length - 1; i >= 0; i--) {
+            listeners[i].processCommand(evt);
         }
     }
 }
diff --git src/org/openstreetmap/gui/jmapviewer/JMapViewerRuntimeException.java src/org/openstreetmap/gui/jmapviewer/JMapViewerRuntimeException.java
index fc6222a..6746a65 100644
--- src/org/openstreetmap/gui/jmapviewer/JMapViewerRuntimeException.java
+++ src/org/openstreetmap/gui/jmapviewer/JMapViewerRuntimeException.java
@@ -12,7 +12,6 @@ public class JMapViewerRuntimeException extends RuntimeException {
      * @see Exception#Exception()
      */
     public JMapViewerRuntimeException() {
-        super();
     }
 
     /**
diff --git src/org/openstreetmap/gui/jmapviewer/JMapViewerTree.java src/org/openstreetmap/gui/jmapviewer/JMapViewerTree.java
index 20e44d7..1ff002e 100644
--- src/org/openstreetmap/gui/jmapviewer/JMapViewerTree.java
+++ src/org/openstreetmap/gui/jmapviewer/JMapViewerTree.java
@@ -5,7 +5,6 @@ import java.awt.BorderLayout;
 import java.awt.Dimension;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
-import java.util.List;
 
 import javax.swing.JLabel;
 import javax.swing.JMenuItem;
@@ -24,7 +23,7 @@ import org.openstreetmap.gui.jmapviewer.interfaces.MapObject;
  * @author galo
  */
 public class JMapViewerTree extends JPanel {
-    /** Serial Version UID */
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 3050203054402323972L;
 
     private final JMapViewer map;
@@ -36,8 +35,8 @@ public class JMapViewerTree extends JPanel {
         this(name, false);
     }
 
+    @SuppressWarnings("this-escape")
     public JMapViewerTree(String name, boolean treeVisible) {
-        super();
         splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
 
         tree = new CheckBoxTree(name);
@@ -49,10 +48,7 @@ public class JMapViewerTree extends JPanel {
         splitPane.setOneTouchExpandable(true);
         splitPane.setDividerLocation(150);
 
-        //Provide minimum sizes for the two components in the split pane
-        Dimension minimumSize = new Dimension(100, 50);
-        //tree.setMinimumSize(minimumSize);
-        map.setMinimumSize(minimumSize);
+        map.setMinimumSize(new Dimension(100, 50));
         createRefresh();
         setLayout(new BorderLayout());
         setTreeVisible(treeVisible);
@@ -71,7 +67,7 @@ public class JMapViewerTree extends JPanel {
                 if (e.isPopupTrigger()) {
                     AbstractLayer layer = ((CheckBoxNodePanel) e.getComponent()).getData().getAbstractLayer();
                     if (layer != null)
-                        JMapViewerTree.this.createPopupMenu(layer).show(e.getComponent(), e.getX(), e.getY());
+                        createPopupMenu(layer).show(e.getComponent(), e.getX(), e.getY());
                 }
             }
         });
@@ -81,7 +77,6 @@ public class JMapViewerTree extends JPanel {
         JMenuItem menuItemShow = new JMenuItem("show texts");
         JMenuItem menuItemHide = new JMenuItem("hide texts");
 
-        //Create the popup menu.
         JPopupMenu popup = new JPopupMenu();
 
         // Create items
@@ -107,6 +102,7 @@ public class JMapViewerTree extends JPanel {
 
     private static void setVisibleTexts(AbstractLayer layer, boolean visible) {
         layer.setVisibleTexts(visible);
+        // Java 16: use instanceof pattern matching instead of the casts
         if (layer instanceof LayerGroup) {
             LayerGroup group = (LayerGroup) layer;
             if (group.getLayers() != null)
@@ -118,7 +114,7 @@ public class JMapViewerTree extends JPanel {
 
     public Layer addLayer(String name) {
         Layer layer = new Layer(name);
-        this.addLayer(layer);
+        addLayer(layer);
         return layer;
     }
 
@@ -128,7 +124,6 @@ public class JMapViewerTree extends JPanel {
     }
 
     public JMapViewerTree addLayer(MapObject element) {
-        //element.getLayer().add(element);
         return addLayer(element.getLayer());
     }
 
@@ -137,10 +132,6 @@ public class JMapViewerTree extends JPanel {
         return element.getLayer();
     }
 
-    public static int size(List<?> list) {
-        return list == null ? 0 : list.size();
-    }
-
     public JMapViewer getViewer() {
         return map;
     }
@@ -149,10 +140,6 @@ public class JMapViewerTree extends JPanel {
         return tree;
     }
 
-    public void addMapObject(MapObject o){
-
-    }
-
     public void setTreeVisible(boolean visible) {
         removeAll();
         revalidate();
@@ -172,18 +159,18 @@ public class JMapViewerTree extends JPanel {
             }
 
             @Override
-            public void treeNodesInserted(TreeModelEvent arg0) {
-                // TODO Auto-generated method stub
+            public void treeNodesInserted(TreeModelEvent e) {
+                // Nothing to do
             }
 
             @Override
-            public void treeNodesRemoved(TreeModelEvent arg0) {
-                // TODO Auto-generated method stub
+            public void treeNodesRemoved(TreeModelEvent e) {
+                // Nothing to do
             }
 
             @Override
-            public void treeStructureChanged(TreeModelEvent arg0) {
-                // TODO Auto-generated method stub
+            public void treeStructureChanged(TreeModelEvent e) {
+                // Nothing to do
             }
         });
     }
diff --git src/org/openstreetmap/gui/jmapviewer/LayerGroup.java src/org/openstreetmap/gui/jmapviewer/LayerGroup.java
index 47b158b..e26dc6c 100644
--- src/org/openstreetmap/gui/jmapviewer/LayerGroup.java
+++ src/org/openstreetmap/gui/jmapviewer/LayerGroup.java
@@ -27,7 +27,7 @@ public class LayerGroup extends AbstractLayer {
     }
 
     public LayerGroup(LayerGroup parent, String name, String description, Style style) {
-        super(name, description, style);
+        super(parent, name, description, style);
     }
 
     public List<AbstractLayer> getLayers() {
diff --git src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
index 0fc018d..8c2863a 100644
--- src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
+++ src/org/openstreetmap/gui/jmapviewer/MapMarkerCircle.java
@@ -16,7 +16,6 @@ import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
  * color.
  *
  * @author Jan Peter Stotz
- *
  */
 public class MapMarkerCircle extends MapObjectImpl implements MapMarker {
 
@@ -128,19 +127,19 @@ public class MapMarkerCircle extends MapObjectImpl implements MapMarker {
 
     @Override
     public void paint(Graphics g, Point position, int radius) {
-        int sizeH = radius;
-        int size = sizeH * 2;
+        int size = radius * 2;
 
+        // Java 16: use instanceof pattern matching instead of the casts
         if (g instanceof Graphics2D && getBackColor() != null) {
             Graphics2D g2 = (Graphics2D) g;
             Composite oldComposite = g2.getComposite();
             g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
             g2.setPaint(getBackColor());
-            g.fillOval(position.x - sizeH, position.y - sizeH, size, size);
+            g.fillOval(position.x - radius, position.y - radius, size, size);
             g2.setComposite(oldComposite);
         }
         g.setColor(getColor());
-        g.drawOval(position.x - sizeH, position.y - sizeH, size, size);
+        g.drawOval(position.x - radius, position.y - radius, size, size);
 
         if (getLayer() == null || getLayer().isVisibleTexts()) paintText(g, position);
     }
diff --git src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
index 2765a33..fa28c08 100644
--- src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
+++ src/org/openstreetmap/gui/jmapviewer/MapMarkerDot.java
@@ -6,12 +6,10 @@ import java.awt.Color;
 import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
 
 /**
- * A simple implementation of the {@link MapMarker} interface. Each map marker
- * is painted as a circle with a black border line and filled with a specified
- * color.
+ * A {@link MapMarker} painted as a small circle of fixed size ({@link #DOT_RADIUS}), by default with a
+ * black border line and filled yellow.
  *
  * @author Jan Peter Stotz
- *
  */
 public class MapMarkerDot extends MapMarkerCircle {
 
@@ -33,6 +31,7 @@ public class MapMarkerDot extends MapMarkerCircle {
         this(layer, name, coord, getDefaultStyle());
     }
 
+    @SuppressWarnings("this-escape")
     public MapMarkerDot(Color color, double lat, double lon) {
         this(null, null, lat, lon);
         setColor(color);
diff --git src/org/openstreetmap/gui/jmapviewer/MapObjectImpl.java src/org/openstreetmap/gui/jmapviewer/MapObjectImpl.java
index d89bb97..41cd087 100644
--- src/org/openstreetmap/gui/jmapviewer/MapObjectImpl.java
+++ src/org/openstreetmap/gui/jmapviewer/MapObjectImpl.java
@@ -24,7 +24,6 @@ public abstract class MapObjectImpl {
     }
 
     public MapObjectImpl(Layer layer, String name, Style style) {
-        super();
         this.layer = layer;
         this.name = name;
         this.style = style;
@@ -52,7 +51,7 @@ public abstract class MapObjectImpl {
 
     public Color getColor() {
         Style styleAssigned = getStyleAssigned();
-        return styleAssigned == null ? null : getStyleAssigned().getColor();
+        return styleAssigned == null ? null : styleAssigned.getColor();
     }
 
     public void setColor(Color color) {
@@ -62,7 +61,7 @@ public abstract class MapObjectImpl {
 
     public Color getBackColor() {
         Style styleAssigned = getStyleAssigned();
-        return styleAssigned == null ? null : getStyleAssigned().getBackColor();
+        return styleAssigned == null ? null : styleAssigned.getBackColor();
     }
 
     public void setBackColor(Color backColor) {
@@ -72,7 +71,7 @@ public abstract class MapObjectImpl {
 
     public Stroke getStroke() {
         Style styleAssigned = getStyleAssigned();
-        return styleAssigned == null ? null : getStyleAssigned().getStroke();
+        return styleAssigned == null ? null : styleAssigned.getStroke();
     }
 
     public void setStroke(Stroke stroke) {
@@ -82,7 +81,7 @@ public abstract class MapObjectImpl {
 
     public Font getFont() {
         Style styleAssigned = getStyleAssigned();
-        return styleAssigned == null ? null : getStyleAssigned().getFont();
+        return styleAssigned == null ? null : styleAssigned.getFont();
     }
 
     public void setFont(Font font) {
@@ -121,8 +120,7 @@ public abstract class MapObjectImpl {
     public void paintText(Graphics g, Point position) {
         if (name != null && g != null && position != null) {
             if (getFont() == null) {
-                Font f = getDefaultFont();
-                setFont(new Font(f.getName(), Font.BOLD, f.getSize()));
+                setFont(getDefaultFont());
             }
             g.setColor(Color.DARK_GRAY);
             g.setFont(getFont());
diff --git src/org/openstreetmap/gui/jmapviewer/MapPolygonImpl.java src/org/openstreetmap/gui/jmapviewer/MapPolygonImpl.java
index fe62723..4cc9887 100644
--- src/org/openstreetmap/gui/jmapviewer/MapPolygonImpl.java
+++ src/org/openstreetmap/gui/jmapviewer/MapPolygonImpl.java
@@ -74,6 +74,7 @@ public class MapPolygonImpl extends MapObjectImpl implements MapPolygon {
         Color oldColor = g.getColor();
         g.setColor(getColor());
 
+        // Java 16: use instanceof pattern matching instead of the casts
         Stroke oldStroke = null;
         if (g instanceof Graphics2D) {
             Graphics2D g2 = (Graphics2D) g;
diff --git src/org/openstreetmap/gui/jmapviewer/MapRectangleImpl.java src/org/openstreetmap/gui/jmapviewer/MapRectangleImpl.java
index 6ba140b..5d569c7 100644
--- src/org/openstreetmap/gui/jmapviewer/MapRectangleImpl.java
+++ src/org/openstreetmap/gui/jmapviewer/MapRectangleImpl.java
@@ -52,6 +52,7 @@ public class MapRectangleImpl extends MapObjectImpl implements MapRectangle {
         // Prepare graphics
         Color oldColor = g.getColor();
         g.setColor(getColor());
+        // Java 16: use instanceof pattern matching instead of the casts
         Stroke oldStroke = null;
         if (g instanceof Graphics2D) {
             Graphics2D g2 = (Graphics2D) g;
@@ -59,14 +60,14 @@ public class MapRectangleImpl extends MapObjectImpl implements MapRectangle {
             g2.setStroke(getStroke());
         }
         // Draw
-        g.drawRect(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y);
+        int width = bottomRight.x - topLeft.x;
+        int height = bottomRight.y - topLeft.y;
+        g.drawRect(topLeft.x, topLeft.y, width, height);
         // Restore graphics
         g.setColor(oldColor);
         if (g instanceof Graphics2D) {
             ((Graphics2D) g).setStroke(oldStroke);
         }
-        int width = bottomRight.x-topLeft.x;
-        int height = bottomRight.y-topLeft.y;
         Point p = new Point(topLeft.x+(width/2), topLeft.y+(height/2));
         if (getLayer() == null || getLayer().isVisibleTexts()) paintText(g, p);
     }
diff --git src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
index 97106fa..e32df67 100644
--- src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
+++ src/org/openstreetmap/gui/jmapviewer/MemoryTileCache.java
@@ -1,11 +1,9 @@
 // License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer;
 
-import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
@@ -20,15 +18,8 @@ import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
  */
 public class MemoryTileCache implements TileCache {
 
-    private static final Logger LOGGER = Logger.getLogger(MemoryTileCache.class.getName());
     /**
-     * @deprecated since 2.20 (create a class specific logger)
-     */
-    @Deprecated
-    protected static final Logger log = LOGGER;
-
-    /**
-     * Default cache size
+     * Maximum number of tiles in the cache
      */
     protected int cacheSize;
 
@@ -59,13 +50,13 @@ public class MemoryTileCache implements TileCache {
     @Override
     public synchronized void addTile(Tile tile) {
         CacheEntry entry = createCacheEntry(tile);
-        if (hash.put(tile.getKey(), entry) == null) {
-            // only if hash hadn't had the element, add it to LRU
-            lruTiles.addFirst(entry);
-            if (hash.size() > cacheSize || lruTiles.getElementCount() > cacheSize) {
-                removeOldEntries();
-            }
+        CacheEntry replaced = hash.put(tile.getKey(), entry);
+        if (replaced != null) {
+            // the replaced entry must leave the LRU list too, or the list keeps it (and its image) forever
+            lruTiles.removeEntry(replaced);
         }
+        lruTiles.addFirst(entry);
+        removeOldEntries();
     }
 
     @Override
@@ -81,12 +72,9 @@ public class MemoryTileCache implements TileCache {
      * Removes the least recently used tiles
      */
     protected synchronized void removeOldEntries() {
-        try {
-            while (lruTiles.getElementCount() > cacheSize) {
-                removeEntry(lruTiles.getLastElement());
-            }
-        } catch (NullPointerException e) {
-            LOGGER.log(Level.WARNING, e.getMessage(), e);
+        // with a negative cache size, the loop would go on with an empty list and fail on its null last element
+        while (lruTiles.getElementCount() > Math.max(cacheSize, 0)) {
+            removeEntry(lruTiles.getLastElement());
         }
     }
 
@@ -107,9 +95,9 @@ public class MemoryTileCache implements TileCache {
 
     @Override
     public synchronized void clearErrorTiles() {
-        Collection<CacheEntry> toRemove = hash.values().stream().filter(cacheEntry -> cacheEntry.tile.hasError()).collect(Collectors.toList());
-        hash.values().removeAll(toRemove);
-        toRemove.forEach(lruTiles::removeEntry);
+        // Java 16: use Stream.toList()
+        List<CacheEntry> toRemove = hash.values().stream().filter(cacheEntry -> cacheEntry.tile.hasError()).collect(Collectors.toList());
+        toRemove.forEach(this::removeEntry);
     }
 
     @Override
@@ -125,13 +113,11 @@ public class MemoryTileCache implements TileCache {
     /**
      * Changes the maximum number of {@link Tile} objects that this cache holds.
      *
-     * @param cacheSize
-     *            new maximum number of tiles
+     * @param cacheSize new maximum number of tiles
      */
     public synchronized void setCacheSize(int cacheSize) {
         this.cacheSize = cacheSize;
-        if (hash.size() > cacheSize)
-            removeOldEntries();
+        removeOldEntries();
     }
 
     /**
@@ -155,8 +141,8 @@ public class MemoryTileCache implements TileCache {
 
     /**
      * Special implementation of a double linked list for {@link CacheEntry}
-     * elements. It supports element removal in constant time - in difference to
-     * the Java implementation which needs O(n).
+     * elements. It supports element removal in constant time - unlike
+     * {@link java.util.LinkedList}, which needs O(n).
      *
      * @author Jan Peter Stotz
      */
diff --git src/org/openstreetmap/gui/jmapviewer/OsmMercator.java src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
index b5354a0..3476381 100644
--- src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
+++ src/org/openstreetmap/gui/jmapviewer/OsmMercator.java
@@ -15,11 +15,17 @@ public class OsmMercator {
     /**
      * default tile size
      */
-    public static final int DEFAUL_TILE_SIZE = 256;
+    public static final int DEFAULT_TILE_SIZE = 256;
+    /**
+     * default tile size
+     * @deprecated use {@link #DEFAULT_TILE_SIZE}, this is the misspelled name
+     */
+    @Deprecated
+    public static final int DEFAUL_TILE_SIZE = DEFAULT_TILE_SIZE;
     /** maximum latitude (north) for mercator display */
     public static final double MAX_LAT = 85.05112877980659;
     /** minimum latitude (south) for mercator display */
-    public static final double MIN_LAT = -85.05112877980659;
+    public static final double MIN_LAT = -MAX_LAT;
     /** equatorial earth radius for EPSG:3857 (Mercator) */
     public static final double EARTH_RADIUS = 6_378_137;
 
@@ -29,7 +35,7 @@ public class OsmMercator {
     public static final OsmMercator MERCATOR_256 = new OsmMercator();
 
     /** tile size of the displayed tiles. Use long so all calculations will be in 64bit to properly handle zooms above 22 for 256 tile size */
-    private long tileSize = DEFAUL_TILE_SIZE;
+    private long tileSize = DEFAULT_TILE_SIZE;
 
     /**
      * Creates instance with default tile size of 256
@@ -45,12 +51,17 @@ public class OsmMercator {
         this.tileSize = tileSize;
     }
 
+    /**
+     * Returns the radius of the earth in pixels at the given zoom level.
+     * @param aZoomlevel zoom level
+     * @return radius in pixels
+     */
     public double radius(int aZoomlevel) {
-        return (tileSize * (1 << aZoomlevel)) / (2.0 * Math.PI);
+        return getMaxPixels(aZoomlevel) / (2.0 * Math.PI);
     }
 
     /**
-     * Returns the absolut number of pixels in y or x, defined as: 2^Zoomlevel *
+     * Returns the absolute number of pixels in y or x, defined as: 2^Zoomlevel *
      * tileSize where tileSize is the width of a tile in pixels
      *
      * @param aZoomlevel zoom level to request pixel data
@@ -60,12 +71,22 @@ public class OsmMercator {
         return tileSize * (1L << aZoomlevel);
     }
 
+    /**
+     * Returns the x offset of the map space origin from longitude 0, in pixels.
+     * @param aZoomlevel zoom level
+     * @return false easting in pixels
+     */
     public long falseEasting(int aZoomlevel) {
         return getMaxPixels(aZoomlevel) / 2;
     }
 
+    /**
+     * Returns the y offset of the map space origin from the equator, in pixels.
+     * @param aZoomlevel zoom level
+     * @return false northing in pixels (negative)
+     */
     public long falseNorthing(int aZoomlevel) {
-        return -1 * getMaxPixels(aZoomlevel) / 2;
+        return -getMaxPixels(aZoomlevel) / 2;
     }
 
     /**
@@ -75,7 +96,6 @@ public class OsmMercator {
      * @param y1 the first y coordinate
      * @param x2 the second x coordinate
      * @param y2 the second y coordinate
-     *
      * @param zoomLevel the zoom level
      * @return the distance
      */
@@ -130,7 +150,7 @@ public class OsmMercator {
      */
     public double lonToX(double aLongitude, int aZoomlevel) {
         long mp = getMaxPixels(aZoomlevel);
-        double x = (mp * (aLongitude + 180L)) / 360L;
+        double x = mp * (aLongitude + 180) / 360;
         return Math.min(x, mp);
     }
 
@@ -139,12 +159,11 @@ public class OsmMercator {
      * <p>
      * Mathematical optimization<br>
      * <code>
-     * log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))<br>
-     *
-     * y = -1 * (radius(aZoomlevel) / 2 * log(u)))) - falseNorthing(aZoomlevel))<br>
-     * y = -1 * (getMaxPixel(aZoomlevel) / 2 * PI / 2 * log(u)) - -1 * getMaxPixel(aZoomLevel) / 2<br>
-     * y = getMaxPixel(aZoomlevel) / (-4 * PI) * log(u)) + getMaxPixel(aZoomLevel) / 2<br>
-     * y = getMaxPixel(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)<br>
+     * log(u) := log((1.0 + sin(toRadians(aLat))) / (1.0 - sin(toRadians(aLat))))<br>
+     * y = -1 * (radius(aZoomlevel) / 2 * log(u)) - falseNorthing(aZoomlevel)<br>
+     * y = -1 * (getMaxPixels(aZoomlevel) / (2 * PI) / 2 * log(u)) - -1 * getMaxPixels(aZoomLevel) / 2<br>
+     * y = getMaxPixels(aZoomlevel) / (-4 * PI) * log(u) + getMaxPixels(aZoomLevel) / 2<br>
+     * y = getMaxPixels(aZoomlevel) * ((log(u) / (-4 * PI)) + 1/2)<br>
      * </code>
      * </p>
      * @param aLat
@@ -195,8 +214,7 @@ public class OsmMercator {
      * @return [MIN_LAT..MAX_LAT] is about [-85..85]
      */
     public double yToLat(long aY, int aZoomlevel) {
-        double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-1.0 * (aY + falseNorthing(aZoomlevel)) / radius(aZoomlevel))));
-        return -1 * Math.toDegrees(latitude);
+        double latitude = (Math.PI / 2) - (2 * Math.atan(Math.exp(-(aY + falseNorthing(aZoomlevel)) / radius(aZoomlevel))));
+        return -Math.toDegrees(latitude);
     }
-
 }
diff --git src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
index e872e16..17fd6a2 100644
--- src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
+++ src/org/openstreetmap/gui/jmapviewer/OsmTileLoader.java
@@ -11,8 +11,9 @@ import java.net.URLConnection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
-import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -32,20 +33,33 @@ public class OsmTileLoader implements TileLoader {
     /** Setting key for number of threads */
     public static final String THREADS_SETTING = "jmapviewer.osm-tile-loader.threads";
     private static final int DEFAULT_THREADS_NUMBER = 8;
-    private static int nThreads = DEFAULT_THREADS_NUMBER;
-    static {
+
+    /** Fixed size pool, shared by all instances */
+    private static final ThreadPoolExecutor JOB_DISPATCHER = createFixedPool(threadCount());
+
+    /** Guards resizing {@link #JOB_DISPATCHER}, which takes two steps */
+    private static final Object POOL_SIZE_LOCK = new Object();
+
+    private static ThreadPoolExecutor createFixedPool(int threads) {
+        return new ThreadPoolExecutor(threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+    }
+
+    private static int threadCount() {
+        // a failure here would fail the class initialization, so that no tile could be loaded at all
         try {
-            nThreads = FeatureAdapter.getIntSetting(THREADS_SETTING, DEFAULT_THREADS_NUMBER);
-        } catch (Exception e) {
+            int threads = FeatureAdapter.getIntSetting(THREADS_SETTING, DEFAULT_THREADS_NUMBER);
+            if (threads >= 1) {
+                return threads;
+            }
+            LOG.log(Level.SEVERE, "Invalid {0}: {1}", new Object[] {THREADS_SETTING, threads});
+        } catch (RuntimeException e) {
             LOG.log(Level.SEVERE, e.getMessage(), e);
         }
+        return DEFAULT_THREADS_NUMBER;
     }
 
-    private static final ThreadPoolExecutor jobDispatcher = (ThreadPoolExecutor) Executors.newFixedThreadPool(nThreads);
-
     private final class OsmTileJob implements TileJob {
         private final Tile tile;
-        private InputStream input;
         private boolean force;
 
         private OsmTileJob(Tile tile) {
@@ -70,12 +84,8 @@ public class OsmTileLoader implements TileLoader {
                 if ("no-tile".equals(tile.getValue("tile-info"))) {
                     tile.setError(tr("No tiles at this zoom level"));
                 } else {
-                    input = conn.getInputStream();
-                    try {
+                    try (InputStream input = conn.getInputStream()) {
                         tile.loadImage(input);
-                    } finally {
-                        input.close();
-                        input = null;
                     }
                 }
                 tile.setLoaded(true);
@@ -83,13 +93,10 @@ public class OsmTileLoader implements TileLoader {
             } catch (IOException e) {
                 tile.setError(e.getMessage());
                 listener.tileLoadingFinished(tile, false);
-                if (input == null) {
-                    try {
-                        LOG.log(Level.SEVERE, "Failed loading " + tile.getUrl() +": "
-                                +e.getClass() + ": " + e.getMessage());
-                    } catch (IOException ioe) {
-                        LOG.log(Level.SEVERE, ioe.getMessage(), ioe);
-                    }
+                try {
+                    LOG.log(Level.SEVERE, "Failed loading " + tile.getUrl() + ": " + e.getClass() + ": " + e.getMessage());
+                } catch (IOException ioe) {
+                    LOG.log(Level.SEVERE, ioe.getMessage(), ioe);
                 }
             } finally {
                 tile.loading = false;
@@ -105,7 +112,7 @@ public class OsmTileLoader implements TileLoader {
         @Override
         public void submit(boolean force) {
             this.force = force;
-            jobDispatcher.execute(this);
+            JOB_DISPATCHER.execute(this);
         }
     }
 
@@ -127,9 +134,14 @@ public class OsmTileLoader implements TileLoader {
         this(listener, null);
     }
 
+    /**
+     * Constructs a new {@code OsmTileLoader}.
+     * @param listener tile loader listener
+     * @param headers HTTP headers to send in addition to (or instead of) the default ones, can be null
+     */
     public OsmTileLoader(TileLoaderListener listener, Map<String, String> headers) {
         this.headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
-        this.headers.put("User-Agent", "JMapViewer Java/"+System.getProperty("java.version"));
+        this.headers.put("User-Agent", "JMapViewer Java/" + System.getProperty("java.version"));
         if (headers != null) {
             this.headers.putAll(headers);
         }
@@ -142,9 +154,9 @@ public class OsmTileLoader implements TileLoader {
     }
 
     protected URLConnection loadTileFromOsm(Tile tile) throws IOException {
-        URL url;
-        url = new URL(tile.getUrl());
-        URLConnection urlConn = url.openConnection();
+        // Java 20: URL(String) is deprecated, use new URI(...).toURL() (it rejects some URLs that are accepted now)
+        URLConnection urlConn = new URL(tile.getUrl()).openConnection();
+        // Java 16: use instanceof pattern matching instead of the casts
         if (urlConn instanceof HttpURLConnection) {
             prepareHttpUrlConnection((HttpURLConnection) urlConn);
         }
@@ -161,27 +173,27 @@ public class OsmTileLoader implements TileLoader {
             tile.putValue("tile-info", str);
         }
 
-        Long lng = urlConn.getExpiration();
-        if (lng.equals(0L)) {
+        long expires = urlConn.getExpiration();
+        if (expires == 0) {
             try {
                 str = urlConn.getHeaderField("Cache-Control");
                 if (str != null) {
                     for (String token: str.split(",", -1)) {
+                        // the directives are usually separated by ", "
+                        token = token.strip();
                         if (token.startsWith("max-age=")) {
-                            lng = Long.parseLong(token.substring(8)) * 1000 +
+                            expires = Long.parseLong(token.substring(8)) * 1000 +
                                     System.currentTimeMillis();
                         }
                     }
                 }
             } catch (NumberFormatException e) {
                 // ignore malformed Cache-Control headers
-                if (JMapViewer.debug) {
-                    LOG.log(Level.FINE, e.getMessage(), e);
-                }
+                LOG.log(Level.FINE, e.getMessage(), e);
             }
         }
-        if (!lng.equals(0L)) {
-            tile.putValue("expires", lng.toString());
+        if (expires != 0) {
+            tile.putValue("expires", Long.toString(expires));
         }
     }
 
@@ -202,19 +214,33 @@ public class OsmTileLoader implements TileLoader {
 
     @Override
     public boolean hasOutstandingTasks() {
-        return jobDispatcher.getTaskCount() > jobDispatcher.getCompletedTaskCount();
+        return JOB_DISPATCHER.getTaskCount() > JOB_DISPATCHER.getCompletedTaskCount();
     }
 
     @Override
     public void cancelOutstandingTasks() {
-        jobDispatcher.getQueue().clear();
+        JOB_DISPATCHER.getQueue().clear();
     }
 
     /**
      * Sets the maximum number of concurrent connections the tile loader will do
      * @param num number of concurrent connections
+     * @throws IllegalArgumentException if {@code num} is less than 1
      */
     public static void setConcurrentConnections(int num) {
-        jobDispatcher.setMaximumPoolSize(num);
+        // checked first, setCorePoolSize(0) would succeed and leave the pool half resized
+        if (num < 1) {
+            throw new IllegalArgumentException("At least one connection is needed: " + num);
+        }
+        // The pool has a fixed size, so both sizes have to change. The maximum may never be below the core size.
+        synchronized (POOL_SIZE_LOCK) {
+            if (num > JOB_DISPATCHER.getMaximumPoolSize()) {
+                JOB_DISPATCHER.setMaximumPoolSize(num);
+                JOB_DISPATCHER.setCorePoolSize(num);
+            } else {
+                JOB_DISPATCHER.setCorePoolSize(num);
+                JOB_DISPATCHER.setMaximumPoolSize(num);
+            }
+        }
     }
 }
diff --git src/org/openstreetmap/gui/jmapviewer/Projected.java src/org/openstreetmap/gui/jmapviewer/Projected.java
index e861c50..bb8c8b3 100644
--- src/org/openstreetmap/gui/jmapviewer/Projected.java
+++ src/org/openstreetmap/gui/jmapviewer/Projected.java
@@ -11,9 +11,10 @@ import java.util.Objects;
 import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
 
 /**
- * Projected coordinates represented by an encapsulates a Point2D.Double value.
+ * Projected coordinates, stored as a {@link Point2D.Double}.
  */
 public class Projected implements IProjected, Serializable {
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 1L;
     private transient Point2D.Double data;
 
@@ -36,11 +37,13 @@ public class Projected implements IProjected, Serializable {
         return data.y;
     }
 
+    // Java 14: annotate with @Serial
     private void writeObject(ObjectOutputStream out) throws IOException {
         out.writeObject(data.x);
         out.writeObject(data.y);
     }
 
+    // Java 14: annotate with @Serial
     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
         data = new Point2D.Double();
         data.x = (Double) in.readObject();
@@ -61,7 +64,7 @@ public class Projected implements IProjected, Serializable {
     public boolean equals(Object obj) {
         if (this == obj)
             return true;
-        if (obj == null || !this.getClass().equals(obj.getClass()))
+        if (obj == null || !getClass().equals(obj.getClass()))
             return false;
         final Projected other = (Projected) obj;
         return Objects.equals(data, other.data);
diff --git src/org/openstreetmap/gui/jmapviewer/Style.java src/org/openstreetmap/gui/jmapviewer/Style.java
index dc7b647..6b4e9b8 100644
--- src/org/openstreetmap/gui/jmapviewer/Style.java
+++ src/org/openstreetmap/gui/jmapviewer/Style.java
@@ -1,26 +1,20 @@
 // License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer;
 
-import java.awt.AlphaComposite;
 import java.awt.Color;
 import java.awt.Font;
 import java.awt.Stroke;
 
 public class Style {
-    private static final AlphaComposite TRANSPARENCY = AlphaComposite.getInstance(AlphaComposite.SRC_OVER);
-    private static final AlphaComposite OPAQUE = AlphaComposite.getInstance(AlphaComposite.SRC);
-
     private Color color;
     private Color backColor;
     private Stroke stroke;
     private Font font;
 
     public Style() {
-        super();
     }
 
     public Style(Color color, Color backColor, Stroke stroke, Font font) {
-        super();
         this.color = color;
         this.backColor = backColor;
         this.stroke = stroke;
@@ -58,16 +52,4 @@ public class Style {
     public void setFont(Font font) {
         this.font = font;
     }
-
-    private static AlphaComposite getAlphaComposite(Color color) {
-        return color.getAlpha() == 255 ? OPAQUE : TRANSPARENCY;
-    }
-
-    public AlphaComposite getAlphaComposite() {
-        return getAlphaComposite(color);
-    }
-
-    public AlphaComposite getBackAlphaComposite() {
-        return getAlphaComposite(backColor);
-    }
 }
diff --git src/org/openstreetmap/gui/jmapviewer/Tile.java src/org/openstreetmap/gui/jmapviewer/Tile.java
index 3c2502b..11b8cfd 100644
--- src/org/openstreetmap/gui/jmapviewer/Tile.java
+++ src/org/openstreetmap/gui/jmapviewer/Tile.java
@@ -10,7 +10,7 @@ import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
-import java.util.concurrent.Callable;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 
 import javax.imageio.ImageIO;
@@ -89,29 +89,24 @@ public class Tile {
         }
     }
 
-    private static class CachedCallable<V> implements Callable<V> {
+    /**
+     * A supplier that asks the wrapped supplier only once, on first use.
+     * @param <V> the type of the supplied value
+     */
+    private static final class Lazy<V> implements Supplier<V> {
         private V result;
-        private final Callable<V> callable;
-
-        /**
-         * Wraps callable so it is evaluated only once
-         * @param callable to cache
-         */
-        CachedCallable(Callable<V> callable) {
-            this.callable = callable;
+        private final Supplier<V> supplier;
+
+        Lazy(Supplier<V> supplier) {
+            this.supplier = supplier;
         }
 
         @Override
-        public synchronized V call() {
-            try {
-                if (result == null) {
-                    result = callable.call();
-                }
-                return result;
-            } catch (Exception e) {
-                // this should not happen here
-                throw new JMapViewerRuntimeException(e);
+        public V get() {
+            if (result == null) {
+                result = supplier.get();
             }
+            return result;
         }
     }
 
@@ -121,11 +116,8 @@ public class Tile {
      * @param cache Tile cache
      */
     public void loadPlaceholderFromCache(TileCache cache) {
-        /*
-         *  use LazyTask as creation of BufferedImage is very expensive
-         *  this way we can avoid object creation until we're sure it's needed
-         */
-        final CachedCallable<BufferedImage> tmpImage = new CachedCallable<>(() ->
+        // creation of BufferedImage is very expensive, so it is avoided until we're sure it's needed
+        Lazy<BufferedImage> tmpImage = new Lazy<>(() ->
                 new BufferedImage(source.getTileSize(), source.getTileSize(), BufferedImage.TYPE_INT_ARGB));
 
         for (int zoomDiff = 1; zoomDiff < 5; zoomDiff++) {
@@ -136,14 +128,11 @@ public class Tile {
                 int factor = 1 << zoomDiff;
                 int xtileHigh = xtile << zoomDiff;
                 int ytileHigh = ytile << zoomDiff;
-                final double scale = 1.0 / factor;
-
-                /*
-                 * use LazyTask for graphics to avoid evaluation of tmpImage, until we have
-                 * something to draw
-                 */
-                CachedCallable<Graphics2D> graphics = new CachedCallable<>(() -> {
-                    Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
+                double scale = 1.0 / factor;
+
+                // lazy, to avoid the evaluation of tmpImage until we have something to draw
+                Lazy<Graphics2D> graphics = new Lazy<>(() -> {
+                    Graphics2D g = (Graphics2D) tmpImage.get().getGraphics();
                     g.setTransform(AffineTransform.getScaleInstance(scale, scale));
                     return g;
                 });
@@ -154,12 +143,12 @@ public class Tile {
                         Tile tile = cache.getTile(source, xtileHigh + x, ytileHigh + y, zoomHigh);
                         if (tile != null && tile.isLoaded()) {
                             paintedTileCount++;
-                            tile.paint(graphics.call(), x * source.getTileSize(), y * source.getTileSize());
+                            tile.paint(graphics.get(), x * source.getTileSize(), y * source.getTileSize());
                         }
                     }
                 }
                 if (paintedTileCount == factor * factor) {
-                    image = tmpImage.call();
+                    image = tmpImage.get();
                     return;
                 }
             }
@@ -168,22 +157,21 @@ public class Tile {
             if (zoomLow >= JMapViewer.MIN_ZOOM) {
                 int xtileLow = xtile >> zoomDiff;
                 int ytileLow = ytile >> zoomDiff;
-                final int factor = 1 << zoomDiff;
-                final double scale = factor;
-                CachedCallable<Graphics2D> graphics = new CachedCallable<>(() -> {
-                    Graphics2D g = (Graphics2D) tmpImage.call().getGraphics();
+                int factor = 1 << zoomDiff;
+                Lazy<Graphics2D> graphics = new Lazy<>(() -> {
+                    Graphics2D g = (Graphics2D) tmpImage.get().getGraphics();
                     AffineTransform at = new AffineTransform();
                     int translateX = (xtile % factor) * source.getTileSize();
                     int translateY = (ytile % factor) * source.getTileSize();
-                    at.setTransform(scale, 0, 0, scale, -translateX, -translateY);
+                    at.setTransform(factor, 0, 0, factor, -translateX, -translateY);
                     g.setTransform(at);
                     return g;
                 });
 
                 Tile tile = cache.getTile(source, xtileLow, ytileLow, zoomLow);
                 if (tile != null && tile.isLoaded()) {
-                    tile.paint(graphics.call(), 0, 0);
-                    image = tmpImage.call();
+                    tile.paint(graphics.get(), 0, 0);
+                    image = tmpImage.get();
                     return;
                 }
             }
@@ -322,15 +310,14 @@ public class Tile {
 
     /**
      * Compares this object with <code>obj</code> based on
-     * the fields {@link #xtile}, {@link #ytile} and
-     * {@link #zoom}.
-     * The {@link #source} field is ignored.
+     * the fields {@link #xtile}, {@link #ytile}, {@link #zoom}
+     * and {@link #source}.
      */
     @Override
     public boolean equals(Object obj) {
         if (this == obj)
             return true;
-        if (obj == null || !this.getClass().equals(obj.getClass()))
+        if (obj == null || !getClass().equals(obj.getClass()))
             return false;
         final Tile other = (Tile) obj;
         return xtile == other.xtile
@@ -344,11 +331,11 @@ public class Tile {
     }
 
     public String getStatus() {
-        if (this.error)
+        if (error)
             return "error";
-        if (this.loaded)
+        if (loaded)
             return "loaded";
-        if (this.loading)
+        if (loading)
             return "loading";
         return "new";
     }
@@ -393,7 +380,7 @@ public class Tile {
     }
 
     /**
-     * returns the metadata of the Tile
+     * Returns the metadata of the Tile
      *
      * @param key metadata key that should be returned
      * @return null if no such metadata exists, or the value of the metadata
@@ -404,7 +391,6 @@ public class Tile {
     }
 
     /**
-     *
      * @return metadata of the tile
      */
     public Map<String, String> getMetadata() {
@@ -415,7 +401,7 @@ public class Tile {
     }
 
     /**
-     * indicate that loading process for this tile has started
+     * Indicates that loading process for this tile has started
      */
     public void initLoading() {
         error = false;
@@ -423,7 +409,7 @@ public class Tile {
     }
 
     /**
-     * indicate that loading process for this tile has ended
+     * Indicates that loading process for this tile has ended
      */
     public void finishLoading() {
         loading = false;
@@ -431,7 +417,6 @@ public class Tile {
     }
 
     /**
-     *
      * @return TileSource from which this tile comes
      */
     public TileSource getTileSource() {
@@ -439,7 +424,7 @@ public class Tile {
     }
 
     /**
-     * indicate that loading process for this tile has been canceled
+     * Indicates that loading process for this tile has been canceled
      */
     public void loadingCanceled() {
         loading = false;
diff --git src/org/openstreetmap/gui/jmapviewer/TileController.java src/org/openstreetmap/gui/jmapviewer/TileController.java
index 136f11b..46f2a4d 100644
--- src/org/openstreetmap/gui/jmapviewer/TileController.java
+++ src/org/openstreetmap/gui/jmapviewer/TileController.java
@@ -6,11 +6,20 @@ import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
 
+/**
+ * Wires a {@link TileSource}, a {@link TileCache} and a {@link TileLoader} together, to get the tiles that a map shows.
+ */
 public class TileController {
     protected TileLoader tileLoader;
     protected TileCache tileCache;
     protected TileSource tileSource;
 
+    /**
+     * Constructs a new {@code TileController}, which loads the tiles with an {@link OsmTileLoader}.
+     * @param source the tile source
+     * @param tileCache the tile cache
+     * @param listener the listener notified when a tile has been loaded
+     */
     public TileController(TileSource source, TileCache tileCache, TileLoaderListener listener) {
         this.tileSource = source;
         this.tileLoader = new OsmTileLoader(listener);
@@ -18,14 +27,13 @@ public class TileController {
     }
 
     /**
-     * retrieves a tile from the cache. If the tile is not present in the cache
-     * a load job is added to the working queue of {@link TileLoader}.
+     * Retrieves a tile from the cache. If the tile is not present in the cache, a new tile is added
+     * to the cache. If the tile is not loaded yet, a load job is added to the working queue of {@link TileLoader}.
      *
      * @param tilex the X position of the tile
      * @param tiley the Y position of the tile
      * @param zoom the zoom level of the tile
-     * @return specified tile from the cache or <code>null</code> if the tile
-     *         was not found in the cache.
+     * @return the specified tile, or <code>null</code> if the position is outside of the map
      */
     public Tile getTile(int tilex, int tiley, int zoom) {
         int max = 1 << zoom;
@@ -37,7 +45,7 @@ public class TileController {
             tileCache.addTile(tile);
             tile.loadPlaceholderFromCache(tileCache);
         }
-        if (tile.error) {
+        if (tile.hasError()) {
             tile.loadPlaceholderFromCache(tileCache);
         }
         if (!tile.isLoaded()) {
@@ -76,7 +84,6 @@ public class TileController {
 
     /**
      * Removes all jobs from the queue that are currently not being processed.
-     *
      */
     public void cancelOutstandingJobs() {
         tileLoader.cancelOutstandingTasks();
diff --git src/org/openstreetmap/gui/jmapviewer/TileRange.java src/org/openstreetmap/gui/jmapviewer/TileRange.java
index 61c5f29..25b2eca 100644
--- src/org/openstreetmap/gui/jmapviewer/TileRange.java
+++ src/org/openstreetmap/gui/jmapviewer/TileRange.java
@@ -41,18 +41,17 @@ public class TileRange {
     }
 
     protected double tilesSpanned() {
-        return Math.sqrt(1.0 * this.size());
+        return Math.sqrt(size());
     }
 
     /**
-     * Returns size
-     * @return size
-     * @throws ArithmeticException – if the result overflows an int (see {@link Math#multiplyExact(int, int)})
+     * Returns the number of tiles in this range.
+     * @return the number of tiles
+     * @throws ArithmeticException if the result overflows an int (see {@link Math#multiplyExact(int, int)})
      */
-    public int size() throws ArithmeticException {
+    public int size() {
         int xSpan = maxX - minX + 1;
         int ySpan = maxY - minY + 1;
         return Math.multiplyExact(xSpan, ySpan);
     }
 }
-
diff --git src/org/openstreetmap/gui/jmapviewer/TileXY.java src/org/openstreetmap/gui/jmapviewer/TileXY.java
index ca671cc..733d125 100644
--- src/org/openstreetmap/gui/jmapviewer/TileXY.java
+++ src/org/openstreetmap/gui/jmapviewer/TileXY.java
@@ -2,8 +2,9 @@
 package org.openstreetmap.gui.jmapviewer;
 
 /**
- * @author w
+ * Tile coordinates at a zoom level. They are fractional for points inside a tile.
  *
+ * @author w
  */
 public class TileXY {
     /**
@@ -17,7 +18,7 @@ public class TileXY {
     private final double y;
 
     /**
-     * Returns an instance of coordinates.
+     * Constructs a new {@code TileXY}.
      *
      * @param x number of the tile
      * @param y number of the tile
@@ -28,17 +29,17 @@ public class TileXY {
     }
 
     /**
-     * @return x index of the tile as integer
+     * @return x index of the tile as integer, rounded towards zero
      */
     public int getXIndex() {
-        return x < 0 ? (int) Math.ceil(x) : (int) Math.floor(x);
+        return (int) x;
     }
 
     /**
-     * @return y index of the tile as integer
+     * @return y index of the tile as integer, rounded towards zero
      */
     public int getYIndex() {
-        return y < 0 ? (int) Math.ceil(y) : (int) Math.floor(y);
+        return (int) y;
     }
 
     /**
diff --git src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeEditor.java src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeEditor.java
index c12645a..f2d908f 100644
--- src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeEditor.java
+++ src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeEditor.java
@@ -19,7 +19,7 @@ import javax.swing.tree.TreePath;
  * @author galo
  */
 public class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {
-    /** SerialVersionUID */
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = -8921320784224636657L;
 
     private final CheckBoxNodeRenderer renderer = new CheckBoxNodeRenderer();
@@ -32,25 +32,8 @@ public class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEd
 
     @Override
     public Object getCellEditorValue() {
-        final CheckBoxNodePanel panel = renderer.getPanel();
-        /*final CheckBoxNodeData checkBoxNode =
-            new CheckBoxNodeData(panel.label.getText(), panel.check.isSelected());
-        return checkBoxNode;
-        CheckBoxNodeData data = search(theTree.rootNode(), panel.label.getText());
-        data.setSelected(panel.check.isSelected());*/
-        return panel.getData();
+        return renderer.getPanel().getData();
     }
-    /*public CheckBoxNodeData search(DefaultMutableTreeNode node, String name){
-        CheckBoxNodeData data = CheckBoxTree.data(node);
-        if(data.getText().equals(name)) return data;
-        else{
-            data = null;
-            for(int i=0; i<node.getChildCount() && data==null; i++){
-                data = search((DefaultMutableTreeNode)node.getChildAt(i), name);
-            }
-            return data;
-        }
-    }*/
 
     public void addNodeListener(MouseAdapter listener) {
         renderer.addNodeListener(listener);
@@ -58,6 +41,7 @@ public class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEd
 
     @Override
     public boolean isCellEditable(final EventObject event) {
+        // Java 16: use instanceof pattern matching instead of the casts
         if (!(event instanceof MouseEvent)) return false;
         final MouseEvent mouseEvent = (MouseEvent) event;
 
@@ -88,6 +72,7 @@ public class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEd
                 fireEditingStopped();
             }
         };
+        // Java 16: use instanceof pattern matching instead of the casts
         if (editor instanceof CheckBoxNodePanel) {
             final CheckBoxNodePanel panel = (CheckBoxNodePanel) editor;
             panel.check.addItemListener(itemListener);
diff --git src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodePanel.java src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodePanel.java
index b55363e..b0e2378 100644
--- src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodePanel.java
+++ src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodePanel.java
@@ -14,14 +14,15 @@ import javax.swing.JPanel;
  * @author galo
  */
 public class CheckBoxNodePanel extends JPanel {
-    /** Serial Version UID */
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = -7236481597785619029L;
     private final JLabel label = new JLabel();
     private CheckBoxNodeData data;
     public final JCheckBox check = new JCheckBox();
 
+    @SuppressWarnings("this-escape")
     public CheckBoxNodePanel() {
-        this.check.setMargin(new Insets(0, 0, 0, 0));
+        check.setMargin(new Insets(0, 0, 0, 0));
         setLayout(new BorderLayout());
         add(check, BorderLayout.WEST);
         add(label, BorderLayout.CENTER);
diff --git src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeRenderer.java src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeRenderer.java
index 54e2642..817ec9c 100644
--- src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeRenderer.java
+++ src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxNodeRenderer.java
@@ -58,6 +58,7 @@ public class CheckBoxNodeRenderer implements TreeCellRenderer {
         final Object value, final boolean selected, final boolean expanded,
         final boolean leaf, final int row, final boolean hasFocus) {
         CheckBoxNodeData data = null;
+        // Java 16: use instanceof pattern matching instead of the casts
         if (value instanceof DefaultMutableTreeNode) {
             final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
             final Object userObject = node.getUserObject();
@@ -66,10 +67,6 @@ public class CheckBoxNodeRenderer implements TreeCellRenderer {
             }
         }
 
-        //final String stringValue =
-        //    tree.convertValueToText(value, selected, expanded, leaf, row, false);
-        //panel.label.setText(stringValue);
-
         panel.setSelected(Boolean.FALSE);
 
         panel.setEnabled(tree.isEnabled());
@@ -92,40 +89,9 @@ public class CheckBoxNodeRenderer implements TreeCellRenderer {
                 selected, expanded, leaf, row, hasFocus);
         }
 
-        //panel.label.setText(data.getText());
         panel.setData(data);
         panel.setSelected(data.isSelected());
 
         return panel;
     }
-/*
-    private JPopupMenu createPopupMenu(final AbstractLayer layer) {
-        JMenuItem menuItem;
-
-        //Create the popup menu.
-        if (layer.isVisibleTexts()) menuItem = new JMenuItem("hide texts");
-        else menuItem = new JMenuItem("show texts");
-        JPopupMenu popup = new JPopupMenu();
-        popup.add(menuItem);
-        menuItem.addActionListener(new ActionListener(){
-            @Override
-            public void actionPerformed(ActionEvent arg0) {
-                setVisibleTexts(layer, !layer.isVisibleTexts());
-            }
-        });
-
-        return popup;
-    }
-
-    private void setVisibleTexts(AbstractLayer layer, boolean visible) {
-        layer.setVisibleTexts(visible);
-        if (layer instanceof LayerGroup) {
-            LayerGroup group = (LayerGroup) layer;
-            if (group.getLayers() != null)
-                for (AbstractLayer al : group.getLayers()) {
-                    setVisibleTexts(al, visible);
-                }
-        }
-    }
-*/
 }
diff --git src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxTree.java src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxTree.java
index 261a15e..21bd428 100644
--- src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxTree.java
+++ src/org/openstreetmap/gui/jmapviewer/checkBoxTree/CheckBoxTree.java
@@ -1,12 +1,9 @@
 // License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer.checkBoxTree;
 
-import java.awt.BorderLayout;
 import java.awt.event.MouseAdapter;
 import java.util.Objects;
 
-import javax.swing.JFrame;
-import javax.swing.JScrollPane;
 import javax.swing.JTree;
 import javax.swing.event.TreeModelEvent;
 import javax.swing.event.TreeModelListener;
@@ -23,7 +20,7 @@ import org.openstreetmap.gui.jmapviewer.LayerGroup;
  * @author galo
  */
 public class CheckBoxTree extends JTree {
-    /** Serial Version UID */
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 6943401106938034256L;
 
     private final CheckBoxNodeEditor editor;
@@ -40,6 +37,7 @@ public class CheckBoxTree extends JTree {
         this(new DefaultMutableTreeNode(root));
     }
 
+    @SuppressWarnings("this-escape")
     public CheckBoxTree(DefaultMutableTreeNode node) {
         super(new DefaultTreeModel(node));
 
@@ -68,17 +66,17 @@ public class CheckBoxTree extends JTree {
 
             @Override
             public void treeNodesInserted(final TreeModelEvent e) {
-                //System.out.println("nodes inserted");
+                // Nothing to do
             }
 
             @Override
             public void treeNodesRemoved(final TreeModelEvent e) {
-                //System.out.println("nodes removed");
+                // Nothing to do
             }
 
             @Override
             public void treeStructureChanged(final TreeModelEvent e) {
-                //System.out.println("structure changed");
+                // Nothing to do
             }
         });
     }
@@ -87,41 +85,10 @@ public class CheckBoxTree extends JTree {
         editor.addNodeListener(listener);
     }
 
-    public static void main(final String[] args) {
-        final DefaultMutableTreeNode root = new DefaultMutableTreeNode(new CheckBoxNodeData("Root", Boolean.TRUE));
-
-        final DefaultMutableTreeNode accessibility =
-            add(root, "Accessibility", true);
-        add(accessibility, "Move system caret with focus/selection changes", false);
-        add(accessibility, "Always expand alt text for images", true);
-        root.add(accessibility);
-
-        final DefaultMutableTreeNode browsing =
-            new DefaultMutableTreeNode(new CheckBoxNodeData("Browsing", null));
-        add(browsing, "Notify when downloads complete", true);
-        add(browsing, "Disable script debugging", true);
-        add(browsing, "Use AutoComplete", true);
-        add(browsing, "Browse in a new process", false);
-        root.add(browsing);
-
-        final CheckBoxTree tree = new CheckBoxTree(root);
-        ((DefaultMutableTreeNode) tree.getModel().getRoot()).add(new DefaultMutableTreeNode(new CheckBoxNodeData("gggg", null)));
-        ((DefaultTreeModel) tree.getModel()).reload();
-        // show the tree on screen
-        final JFrame frame = new JFrame("CheckBox Tree");
-        final JScrollPane scrollPane = new JScrollPane(tree);
-        frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
-        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
-        frame.setSize(300, 150);
-        frame.setVisible(true);
-    }
-
     private static Boolean childStatus(DefaultMutableTreeNode node) {
         Boolean status = data(node.getChildAt(0)).isSelected();
         for (int i = 1; i < node.getChildCount() && status != null; i++) {
-            if (!status.equals(
-                    data(node.getChildAt(i)).isSelected()
-                    ))
+            if (!status.equals(data(node.getChildAt(i)).isSelected()))
                 return null;
         }
         return status;
@@ -144,11 +111,11 @@ public class CheckBoxTree extends JTree {
     private static void nodeChanged(DefaultMutableTreeNode node) {
         if (node != null) {
             changeParents(node);
-            setChildrens(node, data(node).isSelected());
+            setChildrens(node);
         }
     }
 
-    private static void setChildrens(DefaultMutableTreeNode node, Boolean value) {
+    private static void setChildrens(DefaultMutableTreeNode node) {
         for (int i = 0; i < node.getChildCount(); i++) {
             DefaultMutableTreeNode childNode = node(node.getChildAt(i));
             // Fix NPE from #20557
@@ -156,8 +123,8 @@ public class CheckBoxTree extends JTree {
             final CheckBoxNodeData nodeData = data(node);
             if (childData != null && nodeData != null &&
                     !Objects.equals(childData.isSelected(), nodeData.isSelected())) {
-                data(childNode).setSelected(data(node).isSelected());
-                setChildrens(childNode, value);
+                childData.setSelected(nodeData.isSelected());
+                setChildrens(childNode);
             }
         }
     }
@@ -186,13 +153,6 @@ public class CheckBoxTree extends JTree {
         return data(node(node));
     }
 
-    private static DefaultMutableTreeNode add(final DefaultMutableTreeNode parent, final String text, final boolean checked) {
-        final CheckBoxNodeData data = new CheckBoxNodeData(text, checked);
-        final DefaultMutableTreeNode node = new DefaultMutableTreeNode(data);
-        parent.add(node);
-        return node;
-    }
-
     public static CheckBoxNodeData createNodeData(AbstractLayer layer) {
         return new CheckBoxNodeData(layer);
     }
@@ -201,19 +161,6 @@ public class CheckBoxTree extends JTree {
         return new DefaultMutableTreeNode(createNodeData(layer));
     }
 
-    /*public DefaultMutableTreeNode addLayerGroup(LayerGroup group) {
-        if (group != null){
-            if (group.getParent() == null){
-                return add(rootNode(), group);
-            } else {
-                DefaultMutableTreeNode parentGroup = searchNode(group.getParent());
-                if(parentGroup==null) parentGroup = addLayerGroup(group.getParent());
-                DefaultMutableTreeNode node = add(parentGroup, group);
-                return node;
-            }
-        }else return null;
-    }*/
-
     public Layer addLayer(String name) {
         Layer layer = new Layer(name);
         addLayer(layer);
@@ -240,7 +187,6 @@ public class CheckBoxTree extends JTree {
         DefaultMutableTreeNode node = createNode(layer);
         parent.add(node);
         ((DefaultTreeModel) getModel()).reload();
-        //System.out.println("Created node "+layer+" upper of "+data(parent));
         return node;
     }
 
@@ -249,7 +195,7 @@ public class CheckBoxTree extends JTree {
     }
 
     public DefaultMutableTreeNode searchNode(DefaultMutableTreeNode node, AbstractLayer layer) {
-        CheckBoxNodeData data = CheckBoxTree.data(node);
+        CheckBoxNodeData data = data(node);
         if (data.getAbstractLayer() == layer) return node;
         else {
             DefaultMutableTreeNode found = null;
diff --git src/org/openstreetmap/gui/jmapviewer/events/JMVCommandEvent.java src/org/openstreetmap/gui/jmapviewer/events/JMVCommandEvent.java
index 271187f..46df730 100644
--- src/org/openstreetmap/gui/jmapviewer/events/JMVCommandEvent.java
+++ src/org/openstreetmap/gui/jmapviewer/events/JMVCommandEvent.java
@@ -8,7 +8,6 @@ import java.util.EventObject;
  * objects that register as a JMapViewerEventListener
  *
  * @author Jason Huntley
- *
  */
 public class JMVCommandEvent extends EventObject {
     public enum COMMAND {
@@ -16,16 +15,19 @@ public class JMVCommandEvent extends EventObject {
         ZOOM
     }
 
-    /**
-     *
-     */
+    // Java 14: annotate with @Serial
     private static final long serialVersionUID = 8701544867914969620L;
 
     private COMMAND command;
 
+    /**
+     * Constructs a new {@code JMVCommandEvent}.
+     * @param cmd the command
+     * @param source the object on which the event initially occurred
+     */
+    @SuppressWarnings("this-escape")
     public JMVCommandEvent(COMMAND cmd, Object source) {
         super(source);
-
         setCommand(cmd);
     }
 
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/Attributed.java src/org/openstreetmap/gui/jmapviewer/interfaces/Attributed.java
index eb7e955..9163a46 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/Attributed.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/Attributed.java
@@ -3,6 +3,9 @@ package org.openstreetmap.gui.jmapviewer.interfaces;
 
 import java.awt.Image;
 
+/**
+ * Attribution information that has to be shown together with the data of a source.
+ */
 public interface Attributed {
     /**
      * @return True if the tile source requires attribution in text or image form.
@@ -23,7 +26,7 @@ public interface Attributed {
     String getAttributionLinkURL();
 
     /**
-     * @return The URL for the attribution image. Null if no image should be displayed.
+     * @return The attribution image. Null if no image should be displayed.
      */
     Image getAttributionImage();
 
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
index 45bc5a3..95b28dd 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/MapRectangle.java
@@ -31,7 +31,7 @@ public interface MapRectangle extends MapObject {
      * <code>bottomRight</code> are specifying the coordinates within <code>g</code>
      *
      * @param g graphics structure for painting
-     * @param topLeft lop left edge of painting region
+     * @param topLeft top left edge of painting region
      * @param bottomRight bottom right edge of painting region
      */
     void paint(Graphics g, Point topLeft, Point bottomRight);
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
index 1037c9f..b405972 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/TemplatedTileSource.java
@@ -4,14 +4,13 @@ package org.openstreetmap.gui.jmapviewer.interfaces;
 import java.util.Map;
 
 /**
- * Interface for template tile sources, @see TemplatedTMSTileSource
+ * Interface for template tile sources, see {@link org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource}.
  *
  * @author Wiktor Niesiobędzki
  * @since 1.10
  */
 public interface TemplatedTileSource extends TileSource {
     /**
-     *
      * @return headers to be sent with http requests
      */
     Map<String, String> getHeaders();
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java
index 7ea76ae..7827b65 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/TileCache.java
@@ -39,7 +39,7 @@ public interface TileCache {
     void addTile(Tile tile);
 
     /**
-     * @return the number of tiles hold by the cache
+     * @return the number of tiles held by the cache
      */
     int getTileCount();
 
@@ -53,7 +53,7 @@ public interface TileCache {
      * This is implementation specific; the default calls {@link #clear()}.
      */
     default void clearErrorTiles() {
-        this.clear();
+        clear();
     }
 
     /**
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
index 925c5ba..210ad57 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/TileJob.java
@@ -10,12 +10,12 @@ package org.openstreetmap.gui.jmapviewer.interfaces;
 public interface TileJob extends Runnable {
 
     /**
-     * submits download job to backend.
+     * Submits the download job to the backend.
      */
     void submit();
 
     /**
-     * submits download job to backend.
+     * Submits the download job to the backend.
      * @param force true if the load should skip all the caches (local &amp; remote)
      */
     void submit(boolean force);
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
index a4a58cb..5e9627e 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/TileLoader.java
@@ -22,7 +22,7 @@ public interface TileLoader {
     TileJob createTileLoaderJob(Tile tile);
 
     /**
-     * cancels all outstanding tasks in the queue. This should rollback the state of the tiles in the queue
+     * Cancels all outstanding tasks in the queue. This should rollback the state of the tiles in the queue
      * to loading = false / loaded = false
      */
     void cancelOutstandingTasks();
diff --git src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
index b88fd91..af53614 100644
--- src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/interfaces/TileSource.java
@@ -97,16 +97,16 @@ public interface TileSource extends Attributed {
     double getDistance(double la1, double lo1, double la2, double lo2);
 
     /**
-     * Transforms longitude and latitude to pixel space (as if all tiles at specified zoom level where joined).
-     * @param lon longitude
+     * Transforms longitude and latitude to pixel space (as if all tiles at specified zoom level were joined).
      * @param lat latitude
+     * @param lon longitude
      * @param zoom zoom level
      * @return the pixel coordinates
      */
     Point latLonToXY(double lat, double lon, int zoom);
 
     /**
-     * Transforms longitude and latitude to pixel space (as if all tiles at specified zoom level where joined).
+     * Transforms longitude and latitude to pixel space (as if all tiles at specified zoom level were joined).
      * @param point point
      * @param zoom zoom level
      * @return the pixel coordinates
@@ -136,8 +136,8 @@ public interface TileSource extends Attributed {
 
     /**
      * Transforms longitude and latitude to tile indices.
-     * @param lon longitude
      * @param lat latitude
+     * @param lon longitude
      * @param zoom zoom level
      * @return x and y tile indices
      */
@@ -164,7 +164,7 @@ public interface TileSource extends Attributed {
     }
 
     /**
-     * Determines to longitude and latitude of a tile.
+     * Determines the longitude and latitude of a tile.
      * (Refers to the tile origin - upper left tile corner)
      * @param tile Tile
      * @return WGS84 coordinates of given tile
@@ -204,7 +204,7 @@ public interface TileSource extends Attributed {
     int getTileYMax(int zoom);
 
     /**
-     * Get minimum y index of tile for specified zoom level
+     * Get minimum y index of tile for specified zoom level.
      * @param zoom zoom level
      * @return minimum y index of tile for specified zoom level
      */
@@ -212,7 +212,7 @@ public interface TileSource extends Attributed {
 
     /**
      * Determines, if the returned data from TileSource represent "no tile at this zoom level" situation. Detection
-     * algorithms differ per TileSource, so each TileSource should implement each own specific way.
+     * algorithms differ per TileSource, so each TileSource should implement its own specific way.
      *
      * @param headers HTTP headers from response from TileSource server
      * @param statusCode HTTP status code
@@ -232,7 +232,7 @@ public interface TileSource extends Attributed {
     /**
      * Convert tile indices (x/y/zoom) into projected coordinates of the tile origin.
      * @param x x tile index
-     * @param y z tile index
+     * @param y y tile index
      * @param zoom zoom level
      * @return projected coordinates of the tile origin
      */
diff --git src/org/openstreetmap/gui/jmapviewer/package.html src/org/openstreetmap/gui/jmapviewer/package.html
index 37540a6..420ec87 100644
--- src/org/openstreetmap/gui/jmapviewer/package.html
+++ src/org/openstreetmap/gui/jmapviewer/package.html
@@ -1,12 +1,12 @@
 <html>
 <head><title>org.openstreetmap.gui.jmapviewer</title></head>
 <body>
-<p>This package and all sub-packages are belonging to the Java
-component <a href="http://wiki.openstreetmap.org/wiki/JMapViewer">JMapViewer</a>
+<p>This package and all its sub-packages belong to the Java
+component <a href="https://wiki.openstreetmap.org/wiki/JMapViewer">JMapViewer</a>.
 </p>
-<p>JMapViewer is designed to run as stand-alone component without
+<p>JMapViewer is designed to run as a stand-alone component without
 any further requirements. Therefore <b>please do not add any code that
-depends on other libraries or applications</b>. Only functions and methods
-provided by the runtime library of Java 8 should be used.</p>
+depends on other libraries or applications</b>. Only the Java 11 modules
+<code>java.desktop</code> and <code>java.logging</code> should be used.</p>
 </body>
 </html>
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java
index 49976f3..9230046 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractOsmTileSource.java
@@ -23,7 +23,7 @@ public abstract class AbstractOsmTileSource extends TMSTileSource {
      * @param id unique id for the tile source; contains only characters that
      * are safe for file names; can be null
      */
-    public AbstractOsmTileSource(String name, String baseUrl, String id) {
+    protected AbstractOsmTileSource(String name, String baseUrl, String id) {
         super(new TileSourceInfo(name, baseUrl, id));
     }
 
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
index 89dd0d8..5626e68 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTMSTileSource.java
@@ -19,7 +19,6 @@ import org.openstreetmap.gui.jmapviewer.OsmMercator;
  * Class generalizing all tile based tile sources
  *
  * @author Wiktor Niesiobędzki
- *
  */
 public abstract class AbstractTMSTileSource extends AbstractTileSource {
 
@@ -56,7 +55,7 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
      */
     @Override
     public int getDefaultTileSize() {
-        return OsmMercator.DEFAUL_TILE_SIZE;
+        return OsmMercator.DEFAULT_TILE_SIZE;
     }
 
     @Override
@@ -92,9 +91,8 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
      * @param tilex tile number in x axis
      * @param tiley tile number in y axis
      * @return String containing path part of URL of the tile
-     * @throws IOException when subclass cannot return the tile URL
      */
-    public String getTilePath(int zoom, int tilex, int tiley) throws IOException {
+    public String getTilePath(int zoom, int tilex, int tiley) {
         return "/" + zoom + "/" + tilex + "/" + tiley + "." + getExtension();
     }
 
@@ -107,7 +105,7 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
 
     @Override
     public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
-        return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
+        return getBaseUrl() + getTilePath(zoom, tilex, tiley);
     }
 
     @Override
@@ -115,9 +113,6 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
         return getName();
     }
 
-    /*
-     * Most tilesources use OsmMercator projection.
-     */
     @Override
     public int getTileSize() {
         if (tileSize <= 0) {
@@ -168,22 +163,13 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
                 try {
                     md = MessageDigest.getInstance(searchEntry.getKey());
                 } catch (NoSuchAlgorithmException e) {
-                    FeatureAdapter.getLogger(this.getClass()).log(Level.FINER, searchEntry.getKey() + " algorithm was not found", e);
-                    break;
-                }
-                byte[] byteDigest = md.digest(content);
-                final int len = byteDigest.length;
-
-                char[] hexChars = new char[len * 2];
-                int j = 0;
-                for (final int v : byteDigest) {
-                    int vn = (v & 0xf0) >> 4;
-                    hexChars[j++] = (char) (vn + (vn >= 10 ? ('a' - 10) : '0'));
-                    vn = (v & 0xf);
-                    hexChars[j++] = (char) (vn + (vn >= 10 ? ('a' - 10) : '0'));
+                    FeatureAdapter.getLogger(getClass()).log(Level.FINER, searchEntry.getKey() + " algorithm was not found", e);
+                    // the checksums of the other algorithms can still match
+                    continue;
                 }
+                String digest = toHex(md.digest(content));
                 for (String val: searchEntry.getValue()) {
-                    if (new String(hexChars).equalsIgnoreCase(val)) {
+                    if (digest.equalsIgnoreCase(val)) {
                         return true;
                     }
                 }
@@ -210,7 +196,7 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
 
     @Override
     public String getTileId(int zoom, int tilex, int tiley) {
-        return this.baseUrl + "/" + zoom + "/" + tilex + "/" + tiley;
+        return baseUrl + "/" + zoom + "/" + tilex + "/" + tiley;
     }
 
     @Override
@@ -218,6 +204,22 @@ public abstract class AbstractTMSTileSource extends AbstractTileSource {
         return modTileFeatures;
     }
 
+    /**
+     * Encodes bytes as lower case hexadecimal digits.
+     * @param bytes the bytes to encode
+     * @return two hexadecimal digits per byte
+     */
+    // Java 17: replace with java.util.HexFormat.of().formatHex(bytes)
+    private static String toHex(byte[] bytes) {
+        char[] hexChars = new char[bytes.length * 2];
+        int j = 0;
+        for (byte b : bytes) {
+            hexChars[j++] = Character.forDigit((b >> 4) & 0xf, 16);
+            hexChars[j++] = Character.forDigit(b & 0xf, 16);
+        }
+        return new String(hexChars);
+    }
+
     private static int getTileMax(int zoom) {
         return (int) Math.pow(2.0, zoom) - 1;
     }
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTileSource.java
index 6a93554..d9502c2 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/AbstractTileSource.java
@@ -8,6 +8,9 @@ import java.util.Map;
 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
 
+/**
+ * Base class for tile sources, which holds the attribution.
+ */
 public abstract class AbstractTileSource implements TileSource {
 
     protected String attributionText;
@@ -17,6 +20,13 @@ public abstract class AbstractTileSource implements TileSource {
     protected String termsOfUseText;
     protected String termsOfUseURL;
 
+    /**
+     * Constructs a new {@code AbstractTileSource}.
+     */
+    protected AbstractTileSource() {
+        // Nothing to do
+    }
+
     @Override
     public boolean requiresAttribution() {
         return attributionText != null || attributionLinkURL != null || attributionImage != null
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
index 21ba10f..a89eb79 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/BingAerialTileSource.java
@@ -3,6 +3,7 @@ package org.openstreetmap.gui.jmapviewer.tilesources;
 
 import java.awt.Image;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -17,7 +18,6 @@ import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import javax.xml.XMLConstants;
@@ -65,10 +65,6 @@ public class BingAerialTileSource extends TMSTileSource {
     /** Original Bing API key created by Potlatch2 developers in 2010 */
     private static final String API_KEY = "Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU";
 
-    private static final Pattern PATTERN_SUBDOMAIN = Pattern.compile("\\{subdomain}");
-    private static final Pattern PATTERN_QUADKEY = Pattern.compile("\\{quadkey}");
-    private static final Pattern PATTERN_CULTURE = Pattern.compile("\\{culture}");
-
     private volatile Future<List<Attribution>> attributions; // volatile is required for getAttribution(), see below.
     private String imageUrlTemplate;
     private int imageryZoomMax = Integer.MIN_VALUE;
@@ -92,12 +88,22 @@ public class BingAerialTileSource extends TMSTileSource {
         super(info);
     }
 
+    /**
+     * The attribution of an imagery provider in an area, for a range of zoom levels.
+     */
     protected static class Attribution {
         private String attributionText;
         private int minZoom;
         private int maxZoom;
         private Coordinate min;
         private Coordinate max;
+
+        /**
+         * Constructs a new {@code Attribution}.
+         */
+        protected Attribution() {
+            // Filled in by the parser
+        }
     }
 
     @Override
@@ -106,14 +112,10 @@ public class BingAerialTileSource extends TMSTileSource {
         if (getAttribution() == null)
             throw new IOException("Attribution is not loaded yet");
 
-        int t = (zoom + tilex + tiley) % subdomains.length;
-        String subdomain = subdomains[t];
-
-        String url = imageUrlTemplate;
-        url = PATTERN_SUBDOMAIN.matcher(url).replaceAll(subdomain);
-        url = PATTERN_QUADKEY.matcher(url).replaceAll(computeQuadTree(zoom, tilex, tiley));
-
-        return url;
+        String subdomain = subdomains[(zoom + tilex + tiley) % subdomains.length];
+        return imageUrlTemplate
+                .replace("{subdomain}", subdomain)
+                .replace("{quadkey}", TemplatedTMSTileSource.xyzToQuadKey(tilex, tiley, zoom));
     }
 
     /**
@@ -132,7 +134,7 @@ public class BingAerialTileSource extends TMSTileSource {
         try {
             return new URI(FeatureAdapter.getSetting(METADATA_API_SETTING, METADATA_API_URL)
                     .replace(API_KEY_PLACEHOLDER, getKey())
-                    .replace(API_KEY_LAYER, this.layer)).toURL();
+                    .replace(API_KEY_LAYER, layer)).toURL();
         } catch (URISyntaxException e) {
             MalformedURLException malformedURLException = new MalformedURLException(e.getMessage());
             malformedURLException.initCause(e);
@@ -158,11 +160,11 @@ public class BingAerialTileSource extends TMSTileSource {
         String key = FeatureAdapter.getSetting(API_KEY_SETTING, API_KEY);
         if (API_KEY.equals(key)) { // If the API key has not been customized, we try to retrieve the API key
             try {
-                String rkey = FeatureAdapter.retrieveApiKey(this.getId());
+                String rkey = FeatureAdapter.retrieveApiKey(getId());
                 if (rkey != null)
                     key = rkey;
             } catch (IOException ioException) {
-                FeatureAdapter.getLogger(this.getClass()).log(Level.WARNING, "Failed to retrieve api key", ioException);
+                LOG.log(Level.WARNING, "Failed to retrieve api key", ioException);
             }
         }
         return key;
@@ -259,20 +261,16 @@ public class BingAerialTileSource extends TMSTileSource {
             if (imageResource != null) {
                 return FeatureAdapter.readImage(imageResource);
             } else {
-                // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime
-                for (int i = 0; i < 5 && (getAttribution() == null || getAttribution().isEmpty()); i++) {
-                    // Makes sure attribution is loaded
-                    if (JMapViewer.debug) {
-                        LOG.log(Level.FINE, "Bing attribution attempt {0}", (i + 1));
-                    }
-                }
+                // Some Linux distributions (like Debian) will remove Bing logo from sources, so get it at runtime.
+                // The logo URI is part of the attribution data, which this starts loading if it is not loaded yet.
+                getAttribution();
                 if (brandLogoUri != null && !brandLogoUri.isEmpty()) {
                     LOG.log(Level.FINE, "Reading Bing logo from {0}", brandLogoUri);
                     return FeatureAdapter.readImage(new URI(brandLogoUri));
                 }
             }
         } catch (URISyntaxException | IOException e) {
-            LOG.log(Level.SEVERE, String.format("Error while retrieving Bing logo: %s", e.getMessage()));
+            LOG.log(Level.SEVERE, "Error while retrieving Bing logo: {0}", e.getMessage());
         }
         return null;
     }
@@ -296,9 +294,8 @@ public class BingAerialTileSource extends TMSTileSource {
         return () -> {
             int waitTimeSec = 1;
             while (true) {
-                try {
-                    InputSource xml = new InputSource(getAttributionUrl().openStream());
-                    List<Attribution> r = parseAttributionText(xml);
+                try (InputStream in = getAttributionUrl().openStream()) {
+                    List<Attribution> r = parseAttributionText(new InputSource(in));
                     LOG.log(Level.FINE, "Successfully loaded Bing attribution data.");
                     return r;
                 } catch (IllegalArgumentException | IOException ex) {
@@ -320,9 +317,9 @@ public class BingAerialTileSource extends TMSTileSource {
             // see http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
             synchronized (BingAerialTileSource.class) {
                 if (attributions == null) {
-                  final FutureTask<List<Attribution>> loader = new FutureTask<>(getAttributionLoaderCallable());
-                  new Thread(loader, "bing-attribution-loader").start();
-                  attributions = loader;
+                    final FutureTask<List<Attribution>> loader = new FutureTask<>(getAttributionLoaderCallable());
+                    new Thread(loader, "bing-attribution-loader").start();
+                    attributions = loader;
                 }
             }
         }
@@ -361,7 +358,7 @@ public class BingAerialTileSource extends TMSTileSource {
     private void setImageUrlTemplate(String template) {
         String noHttpTemplate = template.replace("http://ecn.{subdomain}.tiles.virtualearth.net/",
                 "https://ecn.{subdomain}.tiles.virtualearth.net/");
-        this.imageUrlTemplate = PATTERN_CULTURE.matcher(noHttpTemplate).replaceAll(Locale.getDefault().toString());
+        imageUrlTemplate = noHttpTemplate.replace("{culture}", Locale.getDefault().toString());
     }
 
     private void setImageryZoomMax(int maxZoom) {
@@ -374,20 +371,4 @@ public class BingAerialTileSource extends TMSTileSource {
             subdomains[i] = subdomainTxt.item(i).getNodeValue();
         }
     }
-
-    private 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 += (char) 1;
-            }
-            if ((tiley & mask) != 0) {
-                digit += (char) 2;
-            }
-            k.append(digit);
-        }
-        return k.toString();
-    }
 }
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
index 352e1d0..ed7cfc3 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/OsmTileSource.java
@@ -8,7 +8,11 @@ import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
 /**
  * OSM Tile source.
  */
-public class OsmTileSource {
+public final class OsmTileSource {
+
+    private OsmTileSource() {
+        // Hide default constructor for the holder of the tile source classes
+    }
 
     /**
      * The default "Mapnik" OSM tile source.
@@ -31,7 +35,7 @@ public class OsmTileSource {
 
         @Override
         public String getBaseUrl() {
-            String url = String.format(this.baseUrl, SERVER[serverNum]);
+            String url = String.format(baseUrl, SERVER[serverNum]);
             serverNum = (serverNum + 1) % SERVER.length;
             return url;
         }
@@ -57,7 +61,7 @@ public class OsmTileSource {
 
         @Override
         public String getBaseUrl() {
-            String url = String.format(this.baseUrl, SERVER[serverNum]);
+            String url = String.format(baseUrl, SERVER[serverNum]);
             serverNum = (serverNum + 1) % SERVER.length;
             return url;
         }
@@ -77,7 +81,7 @@ public class OsmTileSource {
 
         @Override
         public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
-            return this.getBaseUrl() + getTilePath(zoom, tilex, tiley) + "?apikey=" + getApiKey();
+            return getBaseUrl() + getTilePath(zoom, tilex, tiley) + "?apikey=" + getApiKey();
         }
 
         @Override
@@ -115,7 +119,7 @@ public class OsmTileSource {
 
         @Override
         public String getAttributionLinkURL() {
-            return "https://öpnvkarte.de/<";
+            return "https://öpnvkarte.de/";
         }
     }
 }
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
index 49b573d..cddb8e2 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/ScanexTileSource.java
@@ -4,7 +4,7 @@
 package org.openstreetmap.gui.jmapviewer.tilesources;
 
 import java.awt.Point;
-import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
 
 import org.openstreetmap.gui.jmapviewer.Coordinate;
 import org.openstreetmap.gui.jmapviewer.OsmMercator;
@@ -18,9 +18,10 @@ import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
  * sphere in OsmMercator, so latitude calculation differs a lot.
  * <p>
  * The longitude calculation is the same as in OsmMercator,
- * we inherit it from AbstractTMSTileSource.
- *
- * TODO: correct getDistance() method.
+ * we inherit it from TMSTileSource.
+ * <p>
+ * Note that {@link #getDistance(double, double, double, double)} is inherited as well, so it still
+ * assumes a sphere.
  */
 public class ScanexTileSource extends TMSTileSource {
     private static final String DEFAULT_URL = "https://maps.kosmosnimki.ru";
@@ -28,9 +29,12 @@ public class ScanexTileSource extends TMSTileSource {
     private static final String API_KEY = "4018C5A9AECAD8868ED5DEB2E41D09F7";
 
     // Latitude to Y and back calculations.
-    private static final double RADIUS_E = 6_378_137;   /* radius of Earth at equator, m */
-    private static final double EQUATOR = 40075016.68557849; /* equator length, m */
-    private static final double E = 0.0818191908426;  /* eccentricity of Earth's ellipsoid */
+    /** radius of Earth at equator, m */
+    private static final double RADIUS_E = 6_378_137;
+    /** equator length, m */
+    private static final double EQUATOR = 40075016.68557849;
+    /** eccentricity of Earth's ellipsoid */
+    private static final double E = 0.0818191908426;
 
     private enum ScanexLayer {
         IRS("irs", "/TileSender.ashx?ModeKey=tile&MapName=F7B8CF651682420FA1749D894C8AD0F6&LayerName=BAC78D764F0443BD9AF93E7A998C9F5B");
@@ -74,21 +78,21 @@ public class ScanexTileSource extends TMSTileSource {
          * thus is extraneous.  But let it be there just in case if
          * OsmMercator changes.
          */
-        this.tileSize = 256;
+        tileSize = 256;
 
         for (ScanexLayer slayer : ScanexLayer.values()) {
             if (url.equalsIgnoreCase(slayer.getName())) {
-                this.layer = slayer;
+                layer = slayer;
                 // Override baseUrl and maxZoom in base class.
-                this.baseUrl = DEFAULT_URL;
+                baseUrl = DEFAULT_URL;
                 if (maxZoom == 0)
-                    this.maxZoom = DEFAULT_MAXZOOM;
+                    maxZoom = DEFAULT_MAXZOOM;
                 return;
             }
         }
-        /* If not "irs" or "spot" keyword, then a custom URL. */
-        TemplatedTMSTileSource.checkUrl(info.getUrl());
-        this.templateSource = new TemplatedTMSTileSource(info);
+        // If not the "irs" keyword, then a custom URL.
+        TemplatedTMSTileSource.checkUrl(url);
+        templateSource = new TemplatedTMSTileSource(info);
     }
 
     @Override
@@ -96,12 +100,12 @@ public class ScanexTileSource extends TMSTileSource {
         return "jpeg";
     }
 
-   @Override
+    @Override
     public String getTileUrl(int zoom, int tilex, int tiley) {
-        if (this.templateSource != null)
-            return this.templateSource.getTileUrl(zoom, tilex, tiley);
+        if (templateSource != null)
+            return templateSource.getTileUrl(zoom, tilex, tiley);
         else
-            return this.getBaseUrl() + getTilePath(zoom, tilex, tiley);
+            return getBaseUrl() + getTilePath(zoom, tilex, tiley);
     }
 
     @Override
@@ -111,7 +115,7 @@ public class ScanexTileSource extends TMSTileSource {
         tilex = tilex - tmp;
         tiley = tmp - tiley - 1;
 
-        return this.layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
+        return layer.getUri() + "&apikey=" + API_KEY + "&x=" + tilex + "&y=" + tiley + "&z=" + zoom;
     }
 
     @Override
@@ -153,7 +157,7 @@ public class ScanexTileSource extends TMSTileSource {
         return (EQUATOR/2 - (RADIUS_E * Math.log(tmp/pow))) * Math.pow(2.0, zoom) / EQUATOR;
     }
 
-    /*
+    /**
      * To solve inverse formula latitude = f(y) we use
      * Newton's method. We cache previous calculated latitude,
      * because new one is usually close to the old one. In case
@@ -166,9 +170,8 @@ public class ScanexTileSource extends TMSTileSource {
             lat0 = lat;
             lat = lat - Math.toDegrees(nextTerm(Math.toRadians(lat), y, zoom));
             if (lat > OsmMercator.MAX_LAT || lat < OsmMercator.MIN_LAT) {
-                Random r = new Random();
                 lat = OsmMercator.MIN_LAT +
-                  r.nextInt((int) (OsmMercator.MAX_LAT - OsmMercator.MIN_LAT));
+                  ThreadLocalRandom.current().nextInt((int) (OsmMercator.MAX_LAT - OsmMercator.MIN_LAT));
             }
         } while (Math.abs(lat0 - lat) > 0.000001);
 
@@ -177,13 +180,13 @@ public class ScanexTileSource extends TMSTileSource {
         return lat;
     }
 
-    /* Next term in Newton's polynomial */
+    /** Next term in Newton's polynomial */
     private static double nextTerm(double lat, double y, int zoom) {
         double sinl = Math.sin(lat);
         double cosl = Math.cos(lat);
 
-        zoom = (int) Math.pow(2.0, zoom - 1d);
-        double ec = Math.exp((1 - y/zoom)*Math.PI);
+        int halfTiles = (int) Math.pow(2.0, zoom - 1d);
+        double ec = Math.exp((1 - y/halfTiles)*Math.PI);
 
         double f = Math.tan(Math.PI/4+lat/2) -
             ec * Math.pow(Math.tan(Math.PI/4 + Math.asin(E * sinl)/2), E);
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
index 09645d7..eb7fa99 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/TMSTileSource.java
@@ -17,6 +17,9 @@ import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
  */
 public class TMSTileSource extends AbstractTMSTileSource {
 
+    /** Length of the equator in the projected coordinates, in meters */
+    private static final double MERCATOR_WIDTH = 2 * Math.PI * OsmMercator.EARTH_RADIUS;
+
     protected int maxZoom;
     protected int minZoom;
     protected OsmMercator osmMercator;
@@ -25,11 +28,12 @@ public class TMSTileSource extends AbstractTMSTileSource {
      * Constructs a new {@code TMSTileSource}.
      * @param info tile source information
      */
+    @SuppressWarnings("this-escape")
     public TMSTileSource(TileSourceInfo info) {
         super(info);
         minZoom = info.getMinZoom();
         maxZoom = info.getMaxZoom();
-        this.osmMercator = new OsmMercator(this.getTileSize());
+        osmMercator = new OsmMercator(getTileSize());
     }
 
     @Override
@@ -81,16 +85,14 @@ public class TMSTileSource extends AbstractTMSTileSource {
 
     @Override
     public IProjected tileXYtoProjected(int x, int y, int zoom) {
-        double mercatorWidth = 2 * Math.PI * OsmMercator.EARTH_RADIUS;
-        double f = mercatorWidth * getTileSize() / osmMercator.getMaxPixels(zoom);
-        return new Projected(f * x - mercatorWidth / 2, -(f * y - mercatorWidth / 2));
+        double f = MERCATOR_WIDTH * getTileSize() / osmMercator.getMaxPixels(zoom);
+        return new Projected(f * x - MERCATOR_WIDTH / 2, -(f * y - MERCATOR_WIDTH / 2));
     }
 
     @Override
     public TileXY projectedToTileXY(IProjected p, int zoom) {
-        double mercatorWidth = 2 * Math.PI * OsmMercator.EARTH_RADIUS;
-        double f = mercatorWidth * getTileSize() / osmMercator.getMaxPixels(zoom);
-        return new TileXY((p.getEast() + mercatorWidth / 2) / f, (-p.getNorth() + mercatorWidth / 2) / f);
+        double f = MERCATOR_WIDTH * getTileSize() / osmMercator.getMaxPixels(zoom);
+        return new TileXY((p.getEast() + MERCATOR_WIDTH / 2) / f, (-p.getNorth() + MERCATOR_WIDTH / 2) / f);
     }
 
     @Override
diff --git src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
index b64f131..b8337c4 100644
--- src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
+++ src/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSource.java
@@ -2,10 +2,10 @@
 package org.openstreetmap.gui.jmapviewer.tilesources;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.BiConsumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -49,20 +49,18 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
     private static final Pattern PATTERN_SWITCH  = Pattern.compile("\\{switch:([^}]+)}");
     private static final Pattern PATTERN_HEADER  = Pattern.compile("\\{header\\(([^,]+),([^}]+)\\)}");
     private static final Pattern PATTERN_API_KEY = Pattern.compile("\\{apikey}");
-    private static final Pattern PATTERN_PARAM  = Pattern.compile("\\{((?:\\d+-)?z(?:oom)?(:?[+-]\\d+)?|x|y|!y|-y|quad|switch:([^}]+))}");
-
+    private static final Pattern PATTERN_PARAM   = Pattern.compile("\\{((?:\\d+-)?z(?:oom)?(?:[+-]\\d+)?|x|y|!y|-y|quad|switch:([^}]+))}");
+    private static final Pattern PATTERN_ANY     = Pattern.compile("\\{[^}]*}");
     // CHECKSTYLE.ON: SingleSpaceSeparator
 
-    private static final Pattern[] ALL_PATTERNS = {
+    private static final List<Pattern> ALL_PATTERNS = List.of(
             PATTERN_HEADER, PATTERN_ZOOM, PATTERN_X, PATTERN_Y, PATTERN_Y_YAHOO, PATTERN_NEG_Y, PATTERN_SWITCH, PATTERN_API_KEY,
-            PATTERN_QUAD
-    };
+            PATTERN_QUAD);
 
-    private Random rand;
     private String[] randomParts;
     private final Map<String, String> headers = new HashMap<>();
-    private boolean inverse_zoom;
-    private int zoom_offset;
+    private boolean inverseZoom;
+    private int zoomOffset;
 
     /**
      * Creates Templated TMS Tile Source based on ImageryInfo
@@ -77,23 +75,20 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
         handleTemplate(info.getId());
     }
 
-    private void replacePattern(BiConsumer<Matcher, StringBuffer> replaceAction, Pattern... patterns) {
-        for (Pattern p : patterns) {
-            StringBuffer output = new StringBuffer();
-            Matcher m = p.matcher(baseUrl);
-            while (m.find()) {
-                replaceAction.accept(m, output);
-            }
-            m.appendTail(output);
-            baseUrl = output.toString();
+    private void replacePattern(BiConsumer<Matcher, StringBuilder> replaceAction, Pattern pattern) {
+        StringBuilder output = new StringBuilder();
+        Matcher m = pattern.matcher(baseUrl);
+        while (m.find()) {
+            replaceAction.accept(m, output);
         }
+        m.appendTail(output);
+        baseUrl = output.toString();
     }
 
     private void handleTemplate(String imageryId) {
         // Capturing group pattern on switch values
         Matcher m = PATTERN_SWITCH.matcher(baseUrl);
         if (m.find()) {
-            rand = new Random();
             randomParts = m.group(1).split(",", -1);
         }
         // Capturing group pattern on header values
@@ -105,7 +100,8 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
         if (imageryId != null) {
             replacePattern((matcher, output) -> {
                 try {
-                    matcher.appendReplacement(output, FeatureAdapter.retrieveApiKey(imageryId));
+                    // the key is literal text, '$' and '\' in it are no group references
+                    matcher.appendReplacement(output, Matcher.quoteReplacement(FeatureAdapter.retrieveApiKey(imageryId)));
                 } catch (IOException | RuntimeException e) {
                     throw new IllegalArgumentException(e);
                 }
@@ -115,14 +111,11 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
         m = PATTERN_ZOOM.matcher(baseUrl);
         if (m.find()) {
             if (m.group(1) != null) {
-                inverse_zoom = true;
-                zoom_offset = Integer.parseInt(m.group(1));
+                inverseZoom = true;
+                zoomOffset = Integer.parseInt(m.group(1));
             }
             if (m.group(2) != null) {
-                String ofs = m.group(2);
-                if (ofs.startsWith("+"))
-                    ofs = ofs.substring(1);
-                zoom_offset += Integer.parseInt(ofs);
+                zoomOffset += Integer.parseInt(m.group(2));
             }
         }
     }
@@ -134,14 +127,15 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
 
     @Override
     public String getTileUrl(int zoom, int tilex, int tiley) {
-        StringBuffer url = new StringBuffer(baseUrl.length());
+        StringBuilder url = new StringBuilder(baseUrl.length());
         Matcher matcher = PATTERN_PARAM.matcher(baseUrl);
         while (matcher.find()) {
             final String replacement;
+            // Java 14: use a switch expression, "case "z", "zoom" ->" replaces the fall through
             switch (matcher.group(1)) {
             case "z": // PATTERN_ZOOM
             case "zoom":
-                replacement = Integer.toString((inverse_zoom ? -zoom : zoom) + zoom_offset);
+                replacement = zoomParameter(zoom);
                 break;
             case "x": // PATTERN_X
                 replacement = Integer.toString(tilex);
@@ -155,28 +149,24 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
             case "-y": // PATTERN_NEG_Y
                 replacement = Integer.toString((int) Math.pow(2, zoom)-1-tiley);
                 break;
-            case "switch:":
-                replacement = getRandomPart(randomParts);
-                break;
             case "quad": // PATTERN_QUAD
                 replacement = xyzToQuadKey(tilex, tiley, zoom);
                 break;
             default:
-                // handle switch/zoom here, as group will contain parameters and switch will not work
-                if (PATTERN_ZOOM.matcher("{" + matcher.group(1) + "}").matches()) {
-                    replacement = Integer.toString((inverse_zoom ? -zoom : zoom) + zoom_offset);
-                } else if (PATTERN_SWITCH.matcher("{" + matcher.group(1) + "}").matches()) {
-                    replacement = getRandomPart(randomParts);
-                } else {
-                    replacement = '{' + matcher.group(1) + '}';
-                }
+                // the zoom with an offset, or the switch, whose values are in group 2: their parameters defeat the switch labels
+                replacement = matcher.group(2) != null ? getRandomPart(randomParts) : zoomParameter(zoom);
             }
-            matcher.appendReplacement(url, replacement);
+            // the switch values are literal text, '$' and '\' in them are no group references
+            matcher.appendReplacement(url, Matcher.quoteReplacement(replacement));
         }
         matcher.appendTail(url);
         return url.toString().replace(" ", "%20");
     }
 
+    private String zoomParameter(int zoom) {
+        return Integer.toString((inverseZoom ? -zoom : zoom) + zoomOffset);
+    }
+
     /**
      * Convert an x y z coordinate to a quadkey
      * @param x The x coordinate
@@ -184,7 +174,7 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
      * @param z The z coordinate
      * @return The quadkey
      */
-    private static String xyzToQuadKey(int x, int y, int z) {
+    static String xyzToQuadKey(int x, int y, int z) {
         final char[] string = new char[z];
         for (int i = z; i > 0; i--) {
             char digit = '0';
@@ -200,8 +190,13 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
         return String.valueOf(string);
     }
 
+    /**
+     * Picks one of the values of a {switch:...} parameter.
+     * @param parts the values to choose from
+     * @return a randomly chosen value
+     */
     protected String getRandomPart(final String[] parts) {
-        return parts[rand.nextInt(parts.length)];
+        return parts[ThreadLocalRandom.current().nextInt(parts.length)];
     }
 
     /**
@@ -209,10 +204,10 @@ public class TemplatedTMSTileSource extends TMSTileSource implements TemplatedTi
      * @param url URL to check
      */
     public static void checkUrl(String url) {
-        assert url != null && !"".equals(url) : "URL cannot be null or empty";
-        Matcher m = Pattern.compile("\\{[^}]*}").matcher(url);
+        assert url != null && !url.isEmpty() : "URL cannot be null or empty";
+        Matcher m = PATTERN_ANY.matcher(url);
         while (m.find()) {
-            boolean isSupportedPattern = Arrays.stream(ALL_PATTERNS).anyMatch(pattern -> pattern.matcher(m.group()).matches());
+            boolean isSupportedPattern = ALL_PATTERNS.stream().anyMatch(pattern -> pattern.matcher(m.group()).matches());
             if (!isSupportedPattern) {
                 throw new IllegalArgumentException(
                         m.group() + " is not a valid TMS argument. Please check this server URL:\n" + url);
diff --git test/org/openstreetmap/gui/jmapviewer/TileRangeTest.java test/org/openstreetmap/gui/jmapviewer/TileRangeTest.java
index b91faff..2426b5a 100644
--- test/org/openstreetmap/gui/jmapviewer/TileRangeTest.java
+++ test/org/openstreetmap/gui/jmapviewer/TileRangeTest.java
@@ -1,7 +1,6 @@
 // License: GPL. For details, see Readme.txt file.
 package org.openstreetmap.gui.jmapviewer;
 
-
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -18,10 +17,18 @@ class TileRangeTest {
     @Test
     void testSize() {
         assertEquals(16, new TileRange(
-                new TileXY(3, 3), 
+                new TileXY(3, 3),
                 new TileXY(6, 6), 10).size());
     }
 
+    /**
+     * Fractional tile coordinates widen the range to whole tiles, both ends included.
+     */
+    @Test
+    void testFractionalCorners() {
+        assertEquals(9, new TileRange(new TileXY(0.5, 0.5), new TileXY(1.5, 1.5), 3).size());
+    }
+
     /**
      * Ensure that something exceptional happens when an integer overflow happens
      */
diff --git test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java
index 8e625b3..77e7919 100644
--- test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java
+++ test/org/openstreetmap/gui/jmapviewer/tilesources/TemplatedTMSTileSourceTest.java
@@ -2,29 +2,19 @@
 package org.openstreetmap.gui.jmapviewer.tilesources;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.text.MessageFormat;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
 /**
  * Tests for TemplatedTMSTileSource
  */
-public class TemplatedTMSTileSourceTest {
-
-    private static final Collection<String> TMS_IMAGERIES = Collections.singleton("http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}"
-            /*
-             *  generate for example with:
-             *  $ curl https://josm.openstreetmap.de/maps | \
-             *    xmlstarlet sel -N 'josm=http://josm.openstreetmap.de/maps-1.0' -t -v "//josm:entry[josm:type='tms']/josm:url" -n  | \
-             *    sed -e 's/\&amp;/\&/g' -e 's/^/"/' -e 's/$/",/'
-             */);
+class TemplatedTMSTileSourceTest {
 
     /**
      * triple of:
@@ -32,16 +22,12 @@ public class TemplatedTMSTileSourceTest {
      *  * expected tile url for zoom=4, x=5, y=6
      *  * expected tile url for zoom=3, x=2, y=1
      */
-    private final Collection<String[]> TEST_DATA = Arrays.asList(new String[][] {
-        /*
-         * generate with main method below once TMS_IMAGERIES is filled in
-         */
+    private static final List<String[]> TEST_DATA = List.<String[]>of(
         new String[] {
-                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}", 
-                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=4&x=5&y=9", 
-                "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=3&x=2&y=6"
-                }
-    });
+            "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z={zoom}&x={x}&y={-y}",
+            "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=4&x=5&y=9",
+            "http://imagico.de/map/osmim_tiles.php?layer=S2A_R136_N41_20150831T093006&z=3&x=2&y=6"
+        });
 
     /**
      * Check standard template
@@ -104,14 +90,18 @@ public class TemplatedTMSTileSourceTest {
     }
 
     /**
-     * Test template with switch
+     * Test template with API key
      */
     @Test
     void testGetTileUrl_apiKey() {
         System.setProperty("id1.api-key", "wololo");
-        TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", "http://localhost/{zoom}/{x}/{y}?token={apikey}&foo=bar", "id1");
-        TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
-        assertEquals("http://localhost/4/5/6?token=wololo&foo=bar", ts.getTileUrl(4, 5, 6));
+        try {
+            TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", "http://localhost/{zoom}/{x}/{y}?token={apikey}&foo=bar", "id1");
+            TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
+            assertEquals("http://localhost/4/5/6?token=wololo&foo=bar", ts.getTileUrl(4, 5, 6));
+        } finally {
+            System.clearProperty("id1.api-key");
+        }
     }
 
     /**
@@ -121,41 +111,20 @@ public class TemplatedTMSTileSourceTest {
     void testGetTileUrl_switch() {
         TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", "http://{switch:a,b,c}.localhost/{10-zoom-5}/{x}/{y}", "id1");
         TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
-        assertTrue(
-                Stream.of(
-                        "http://a.localhost/4/2/3",
-                        "http://b.localhost/4/2/3",
-                        "http://c.localhost/4/2/3"
-                        )
-                .anyMatch(Predicate.isEqual(ts.getTileUrl(1, 2, 3)))
-                );
+        assertOneOfServers("/4/2/3", ts.getTileUrl(1, 2, 3));
+        assertOneOfServers("/3/3/4", ts.getTileUrl(2, 3, 4));
+        assertOneOfServers("/2/4/5", ts.getTileUrl(3, 4, 5));
+        assertOneOfServers("/1/5/6", ts.getTileUrl(4, 5, 6));
+    }
 
-        assertTrue(
-                Stream.of(
-                        "http://a.localhost/3/3/4",
-                        "http://b.localhost/3/3/4",
-                        "http://c.localhost/3/3/4"
-                        )
-                .anyMatch(Predicate.isEqual(ts.getTileUrl(2, 3, 4)))
-                );
-        assertTrue(
-                Stream.of(
-                        "http://a.localhost/2/4/5",
-                        "http://b.localhost/2/4/5",
-                        "http://c.localhost/2/4/5"
-                        )
-                .anyMatch(Predicate.isEqual(ts.getTileUrl(3, 4, 5)))
-                );
-        assertTrue(
-                Stream.of(
-                        "http://a.localhost/1/5/6",
-                        "http://b.localhost/1/5/6",
-                        "http://c.localhost/1/5/6"
-                        )
-                .anyMatch(Predicate.isEqual(ts.getTileUrl(4, 5, 6)))
-                );
+    private static void assertOneOfServers(String path, String url) {
+        Set<String> expected = Set.of("http://a.localhost" + path, "http://b.localhost" + path, "http://c.localhost" + path);
+        assertTrue(expected.contains(url), url);
     }
 
+    /**
+     * Test template with Yahoo style y index
+     */
     @Test
     void testGetTileUrl_yahoo() {
         checkGetTileUrl(
@@ -163,9 +132,11 @@ public class TemplatedTMSTileSourceTest {
                 "http://localhost/4/5/1",
                 "http://localhost/3/1/1"
                 );
-
     }
 
+    /**
+     * Test template with reversed (TMS) y index
+     */
     @Test
     void testGetTileUrl_negative_y() {
         checkGetTileUrl(
@@ -173,9 +144,11 @@ public class TemplatedTMSTileSourceTest {
                 "http://localhost/4/5/9",
                 "http://localhost/3/1/5"
                 );
-
     }
 
+    /**
+     * Test template with quadkey
+     */
     @Test
     void testGetTileUrl_quad() {
         checkGetTileUrl(
@@ -183,13 +156,45 @@ public class TemplatedTMSTileSourceTest {
                 "http://localhost/0321",
                 "http://localhost/021"
                 );
+    }
+
+    /**
+     * Test that the header placeholders become headers and leave the URL
+     */
+    @Test
+    void testHeaders() {
+        TileSourceInfo info = new TileSourceInfo("test imagery", "http://localhost/{header(X-Test,value)}{z}/{x}/{y}", "id1");
+        info.setCookies("a=b");
+        TemplatedTMSTileSource ts = new TemplatedTMSTileSource(info);
+        assertEquals(Map.of("X-Test", "value", "Cookie", "a=b"), ts.getHeaders());
+        assertEquals("http://localhost/4/5/6", ts.getTileUrl(4, 5, 6));
+    }
+
+    /**
+     * Test that spaces are encoded
+     */
+    @Test
+    void testSpacesAreEncoded() {
+        checkGetTileUrl(
+                "http://localhost/my layer/{z}/{x}/{y}",
+                "http://localhost/my%20layer/4/5/6",
+                "http://localhost/my%20layer/3/1/2"
+                );
+    }
 
+    /**
+     * Test the URL validation
+     */
+    @Test
+    void testCheckUrl() {
+        TemplatedTMSTileSource.checkUrl("http://{switch:a,b}.localhost/{zoom}/{x}/{-y}.png?{quad}");
+        assertThrows(IllegalArgumentException.class, () -> TemplatedTMSTileSource.checkUrl("http://localhost/{unknown}/{x}/{y}"));
     }
 
-    private void checkGetTileUrl(String url, String expected123, String expected312) {
+    private static void checkGetTileUrl(String url, String expected456, String expected312) {
         TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", url, "id1");
         TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
-        assertEquals(expected123, ts.getTileUrl(4, 5, 6));
+        assertEquals(expected456, ts.getTileUrl(4, 5, 6));
         assertEquals(expected312, ts.getTileUrl(3, 1, 2));
     }
 
@@ -205,13 +210,4 @@ public class TemplatedTMSTileSourceTest {
             assertEquals(test[2], ts.getTileUrl(3, 2, 1));
         }
     }
-
-    public static void main(String[] args) {
-        for (String url: TMS_IMAGERIES) {
-            TileSourceInfo testImageryTMS = new TileSourceInfo("test imagery", url, "id1");
-            TemplatedTMSTileSource ts = new TemplatedTMSTileSource(testImageryTMS);
-            System.out.println(MessageFormat.format("new String[]{\"{0}\", \"{1}\", \"{2}\"},",
-                    url, ts.getTileUrl(4, 5, 6), ts.getTileUrl(3, 2, 1)));
-        }
-    }
 }
diff --git tools/assembly/release.xml tools/assembly/release.xml
new file mode 100644
index 0000000..62fc8ab
--- /dev/null
+++ tools/assembly/release.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Binary release zip, equivalent to the Ant "create_release_zip" target.
+     The JOSM Jenkins release job expects JMapViewer.jar and JMapViewer_src.jar inside. -->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+    <id>release</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <files>
+        <file>
+            <source>${project.build.directory}/${project.build.finalName}.jar</source>
+            <destName>JMapViewer.jar</destName>
+        </file>
+        <file>
+            <source>${project.build.directory}/${project.build.finalName}-sources.jar</source>
+            <destName>JMapViewer_src.jar</destName>
+        </file>
+        <file>
+            <source>${project.basedir}/Readme.txt</source>
+        </file>
+        <file>
+            <source>${project.basedir}/Gpl.txt</source>
+        </file>
+    </files>
+</assembly>
diff --git tools/assembly/source-release.xml tools/assembly/source-release.xml
new file mode 100644
index 0000000..cc009b8
--- /dev/null
+++ tools/assembly/source-release.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Source release zip, equivalent to the Ant "create_source_release_zip" target.
+     It ships pom.xml, the tests and the tools/ directory as well, so that the unpacked zip
+     can be built with Maven as well as with Ant. -->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+    <id>Source</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <baseDirectory>jmapviewer-${project.version}</baseDirectory>
+    <files>
+        <file>
+            <source>${project.basedir}/Readme.txt</source>
+        </file>
+        <file>
+            <source>${project.basedir}/Gpl.txt</source>
+        </file>
+        <file>
+            <source>${project.basedir}/build.xml</source>
+        </file>
+        <file>
+            <source>${project.basedir}/pom.xml</source>
+        </file>
+    </files>
+    <fileSets>
+        <fileSet>
+            <directory>${project.basedir}/src</directory>
+            <outputDirectory>src</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${project.basedir}/test</directory>
+            <outputDirectory>test</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>${project.basedir}/tools</directory>
+            <outputDirectory>tools</outputDirectory>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git tools/checkstyle/jmapviewer_checks.xml tools/checkstyle/jmapviewer_checks.xml
index 10e401a..bc6bf54 100644
--- tools/checkstyle/jmapviewer_checks.xml
+++ tools/checkstyle/jmapviewer_checks.xml
@@ -1,17 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
-
-<!--
-    This configuration file was written by the eclipse-cs plugin configuration editor
--->
-<!--
-    Checkstyle-Configuration: JMapViewer
-    Description: 
-JMapViewer Checkstyle rules
--->
+<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
+<!-- JMapViewer checkstyle rules, used by Maven, Ant and eclipse-cs (.checkstyle) -->
 <module name="Checker">
   <property name="severity" value="warning"/>
   <property name="charset" value="UTF-8"/>
+  <!-- Checkstyle 10 cannot parse module declarations; drop this filter once the build needs JDK 21 (Checkstyle 14+) -->
+  <module name="BeforeExecutionExclusionFileFilter">
+    <property name="fileNamePattern" value="module\-info\.java$"/>
+  </module>
   <module name="TreeWalker">
     <module name="MissingDeprecated"/>
     <module name="MissingOverride"/>
@@ -20,10 +16,6 @@ JMapViewer Checkstyle rules
       <property name="allowSamelineMultipleAnnotations" value="true"/>
     </module>
     <module name="AnnotationUseStyle"/>
-    <module name="JavadocStyle">
-      <property name="checkFirstSentence" value="false"/>
-      <property name="checkHtml" value="false"/>
-    </module>
     <module name="NonEmptyAtclauseDescription"/>
     <module name="AtclauseOrder"/>
     <module name="AvoidStarImport"/>
@@ -104,7 +96,7 @@ JMapViewer Checkstyle rules
   </module>
   <module name="FileTabCharacter"/>
   <module name="LineLength">
-    <property name="fileExtensions" value="java" />
+    <property name="fileExtensions" value=".java"/>
     <property name="max" value="145"/>
     <property name="tabWidth" value="4"/>
   </module>
@@ -112,8 +104,7 @@ JMapViewer Checkstyle rules
     <property name="lineSeparator" value="lf"/>
   </module>
   <module name="SuppressionFilter">
-    <property name="file" value="tools/checkstyle/jmapviewer_filters.xml"/>
-    <property name="optional" value="true"/>
+    <property name="file" value="${config_loc}/jmapviewer_filters.xml"/>
   </module>
   <module name="SuppressWithPlainTextCommentFilter">
     <property name="offCommentFormat" value="CHECKSTYLE\.OFF\: ([\w\|]+)"/>
diff --git tools/checkstyle/jmapviewer_filters.xml tools/checkstyle/jmapviewer_filters.xml
index 14abc8d..be622b7 100644
--- tools/checkstyle/jmapviewer_filters.xml
+++ tools/checkstyle/jmapviewer_filters.xml
@@ -1,9 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE suppressions PUBLIC
-     "-//Puppy Crawl//DTD Suppressions 1.0//EN"
-     "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
+    "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
+    "https://checkstyle.org/dtds/suppressions_1_2.dtd">
 <suppressions>
-  <suppress checks="." files="\.classpath" />
-  <suppress checks="." files="\.project" />
-  <suppress checks="HeaderCheck" files="ScanexTileSource\.java" />
-</suppressions>
\ No newline at end of file
+  <!-- ScanexTileSource is dual licensed (BSD or GPL) -->
+  <suppress checks="HeaderCheck" files="ScanexTileSource\.java"/>
+</suppressions>
