commit 013e837825607bb8bde748d83df4bbe028ae0c64
Author: Simon Legner <Simon.Legner@gmail.com>
Date:   2021-03-20 08:44:43 +0100

    WIP #19336 - Implement WMS server

diff --git a/resources/data/GetCapabilities.xml b/resources/data/GetCapabilities.xml
new file mode 100644
index 000000000..bc7a9ccab
--- /dev/null
+++ b/resources/data/GetCapabilities.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<WMS_Capabilities
+        version="1.3.0"
+        xmlns="http://www.opengis.net/wms"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xmlns:xlink="http://www.w3.org/1999/xlink"
+        xsi:schemaLocation="http://www.opengis.net/wms http://schemas.opengis.net/wms/1.3.0/capabilities_1_3_0.xsd">
+    <Service>
+        <Name>WMS</Name>
+        <Title>JOSM</Title>
+        <Abstract>...</Abstract>
+        <OnlineResource xlink:href="http://localhost:8111/wms?"/>
+        <Fees>none</Fees>
+        <AccessConstraints>ODbL</AccessConstraints>
+    </Service>
+    <Capability>
+        <Request>
+            <GetCapabilities>
+                <Format>application/vnd.ogc.wms_xml</Format>
+                <Format>text/xml</Format>
+                <DCPType>
+                    <HTTP>
+                        <Get>
+                            <OnlineResource xlink:href="http://localhost:8111/wms?"/>
+                        </Get>
+                    </HTTP>
+                </DCPType>
+            </GetCapabilities>
+            <GetMap>
+                <Format>image/png</Format>
+                <DCPType>
+                    <HTTP>
+                        <Get>
+                            <OnlineResource xlink:href="http://localhost:8111/wms?"/>
+                        </Get>
+                    </HTTP>
+                </DCPType>
+            </GetMap>
+        </Request>
+        <Exception>
+            <Format>application/vnd.ogc.se_xml</Format>
+            <Format>application/vnd.ogc.se_inimage</Format>
+            <Format>application/vnd.ogc.se_blank</Format>
+            <Format>text/xml</Format>
+            <Format>XML</Format>
+        </Exception>
+        <Layer>
+            <Title>JOSM Root</Title>
+            <CRS>EPSG:3857</CRS>
+            <Layer>
+                <Title>JOSM Layer</Title>
+                <Style>
+                    <Name>JOSM Style</Name>
+                    <Title>JOSM Style</Title>
+                </Style>
+            </Layer>
+        </Layer>
+    </Capability>
+</WMS_Capabilities>
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java b/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
index e03bfb4ce..c45d06b4d 100644
--- a/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
+++ b/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
@@ -44,6 +44,7 @@ import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHan
 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerForbiddenException;
 import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerOsmApiException;
 import org.openstreetmap.josm.io.remotecontrol.handler.VersionHandler;
+import org.openstreetmap.josm.io.remotecontrol.handler.WmsHandler;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -171,6 +172,7 @@ public class RequestProcessor extends Thread {
             addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
             addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
             addRequestHandlerClass(OpenApiHandler.command, OpenApiHandler.class, true);
+            addRequestHandlerClass(WmsHandler.command, WmsHandler.class, true);
         }
     }
 
@@ -181,7 +183,8 @@ public class RequestProcessor extends Thread {
     public void run() {
         Writer out = null; // NOPMD
         try { // NOPMD
-            out = new OutputStreamWriter(new BufferedOutputStream(request.getOutputStream()), RESPONSE_CHARSET);
+            BufferedOutputStream raw = new BufferedOutputStream(request.getOutputStream());
+            out = new OutputStreamWriter(raw, RESPONSE_CHARSET);
             BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.US_ASCII)); // NOPMD
 
             String get = in.readLine();
@@ -262,12 +265,16 @@ public class RequestProcessor extends Thread {
                     handler.setUrl(url);
                     handler.setSender(sender);
                     handler.handle();
-                    sendHeader(out, "200 OK", handler.getContentType(), false);
-                    out.write("Content-length: " + handler.getContent().length()
-                            + "\r\n");
-                    out.write("\r\n");
-                    out.write(handler.getContent());
-                    out.flush();
+                    if (handler instanceof WmsHandler) {
+                        sendHeader(out, "200 OK", handler.getContentType(), null, -1);
+                        out.flush();
+                        ((WmsHandler) handler).handleRequest(raw);
+                        raw.flush();
+                    } else {
+                        sendHeader(out, "200 OK", handler.getContentType(), RESPONSE_CHARSET, handler.getContent().length());
+                        out.write(handler.getContent());
+                        out.flush();
+                    }
                 } catch (RequestHandlerOsmApiException ex) {
                     Logging.debug(ex);
                     sendBadGateway(out, ex.getMessage());
@@ -305,7 +312,7 @@ public class RequestProcessor extends Thread {
     }
 
     private static void sendErrorHtml(Writer out, int errorCode, String errorName, String helpHtml) throws IOException {
-        sendHeader(out, errorCode + " " + errorName, "text/html", true);
+        sendHeader(out, errorCode + " " + errorName, "text/html", RESPONSE_CHARSET, -1);
         out.write(String.format(
                 RESPONSE_TEMPLATE,
                 "<title>" + errorName + "</title>",
@@ -389,20 +396,31 @@ public class RequestProcessor extends Thread {
      *            The status string ("200 OK", "500", etc)
      * @param contentType
      *            The content type of the data sent
-     * @param endHeaders
-     *            If true, adds a new line, ending the headers.
+     * @param charset The content charset
+     * @param length The content length
+     *
      * @throws IOException
      *             When error
      */
-    private static void sendHeader(Writer out, String status, String contentType,
-            boolean endHeaders) throws IOException {
-        out.write("HTTP/1.1 " + status + "\r\n");
-        out.write("Date: " + new Date() + "\r\n");
-        out.write("Server: " + JOSM_REMOTE_CONTROL + "\r\n");
-        out.write("Content-type: " + contentType + "; charset=" + RESPONSE_CHARSET.name().toLowerCase(Locale.ENGLISH) + "\r\n");
-        out.write("Access-Control-Allow-Origin: *\r\n");
-        if (endHeaders)
-            out.write("\r\n");
+    private static void sendHeader(Appendable out, String status, String contentType, Charset charset, long length) throws IOException {
+        out.append("HTTP/1.1 ").append(status).append("\r\n");
+        out.append("Date: ").append(String.valueOf(new Date())).append("\r\n");
+        out.append("Server: " + JOSM_REMOTE_CONTROL + "\r\n");
+
+        out.append("Content-type: ").append(contentType);
+        if (charset != null) {
+            out.append("; charset=").append(charset.name().toLowerCase(Locale.ENGLISH));
+        }
+        out.append("\r\n");
+
+        if (length > 0) {
+            out.append("Content-length: ").append(String.valueOf(length)).append("\r\n");
+        }
+
+        out.append("Access-Control-Allow-Origin: *\r\n");
+
+        // end header
+        out.append("\r\n");
     }
 
     /**
diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/WmsHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/WmsHandler.java
new file mode 100644
index 000000000..a504778e0
--- /dev/null
+++ b/src/org/openstreetmap/josm/io/remotecontrol/handler/WmsHandler.java
@@ -0,0 +1,104 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.remotecontrol.handler;
+
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.mappaint.RenderingHelper;
+import org.openstreetmap.josm.gui.mappaint.RenderingHelper.StyleData;
+import org.openstreetmap.josm.io.CachedFile;
+import org.openstreetmap.josm.io.IllegalDataException;
+import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+import org.openstreetmap.josm.tools.CheckParameterUtil;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class WmsHandler extends RequestHandler {
+
+    /**
+     * The remote control command name.
+     */
+    public static final String command = "wms";
+
+    @Override
+    protected void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException {
+        if (args.get("request").equalsIgnoreCase("GetCapabilities")) {
+            contentType = "text/xml";
+        } else if (args.get("request").equalsIgnoreCase("GetMap")) {
+            contentType = args.getOrDefault("format", "image/png");
+        }
+    }
+
+    public void handleRequest(OutputStream out) throws IOException {
+        if (args.get("request").equalsIgnoreCase("GetCapabilities")) {
+            byte[] bytes = new CachedFile("resource://data/GetCapabilities.xml").getByteContent();
+            out.write(bytes);
+        } else if (args.get("request").equalsIgnoreCase("GetMap")) {
+            getMap(out);
+        }
+    }
+
+    private void getMap(OutputStream out) throws IOException {
+//        double[] bbox = Pattern.compile(",").splitAsStream(args.get("bbox")).mapToDouble(Double::parseDouble).toArray();
+//        ProjectionBounds projectionBounds = new ProjectionBounds();
+//        projectionBounds.extend(new EastNorth(bbox[0], bbox[1]));
+//        projectionBounds.extend(new EastNorth(bbox[2], bbox[3]));
+//        System.out.println(projectionBounds);
+//        Bounds bounds = ProjectionRegistry.getProjection().getLatLonBoundsBox(projectionBounds);
+//        System.out.println(bounds);
+        StyleData styleData = new StyleData();
+        styleData.styleUrl = "resource://styles/standard/elemstyles.mapcss";
+
+        RenderingHelper renderingHelper = new RenderingHelper(
+                MainApplication.getLayerManager().getActiveDataSet(),
+                MainApplication.getMap().mapView.getRealBounds(),
+                MainApplication.getMap().mapView.getScale(),
+                Collections.singleton(styleData));
+        try {
+            BufferedImage image = renderingHelper.render();
+            ImageIO.write(image, "png", out);
+        } catch (IllegalDataException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    protected void parseArgs() throws URISyntaxException {
+        super.parseArgs();
+        this.args = this.args.entrySet().stream().collect(Collectors.toMap(s -> s.getKey().toLowerCase(Locale.ROOT), Map.Entry::getValue));
+        this.args.computeIfAbsent("service", k -> "WMS");
+        this.args.computeIfAbsent("request", k -> "GetCapabilities");
+    }
+
+    @Override
+    public String[] getMandatoryParams() {
+        return new String[]{};
+    }
+
+    @Override
+    public String[] getOptionalParams() {
+        return new String[]{"service", "request", "version", "bbox", "layer"};
+    }
+
+    @Override
+    protected void validateRequest() throws RequestHandlerBadRequestException {
+        CheckParameterUtil.ensureThat("WMS".equalsIgnoreCase(args.get("service")), "service=WMS");
+        CheckParameterUtil.ensureParameterNotNull("WMS".equalsIgnoreCase(args.get("request")), "request");
+    }
+
+    @Override
+    public String getPermissionMessage() {
+        return null;
+    }
+
+    @Override
+    public PermissionPrefWithDefault getPermissionPref() {
+        return null;
+    }
+}
