<commit_before>local onearth_gc_service = {}

local lfs = require "lfs"
local lyaml = require "lyaml"
local request = require "http.request"
local JSON = require "JSON"
local xml = require "pl.xml"

-- Reference Constants
local EARTH_RADIUS = 6378137.0
local PROJECTIONS = {
    ["EPSG:3031"] = {
        wkt='PROJCS["WGS 84 / Antarctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",-71],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","3031"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -90}, upperCorner={180, -38.941373}},
        bbox={crs="urn:ogc:def:crs:EPSG::3031", lowerCorner={-4194304, -4194304}, upperCorner={194304, 4194304}}
    },
    ["EPSG:3413"] = {
        wkt='PROJCS["WGS 84 / NSIDC Sea Ice Polar Stereographic North",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",70],PARAMETER["central_meridian",-45],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],AUTHORITY["EPSG","3413"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, 38.807151}, upperCorner={180, 90}},
        bbox={crs="urn:ogc:def:crs:EPSG::3413", lowerCorner={-4194304, -4194304}, upperCorner={4194304, 4194304}}
    },
    ["EPSG:3857"] = {
        wkt='PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -85}, upperCorner={180, 85}},
        bbox={crs="urn:ogc:def:crs:EPSG::3857", lowerCorner={-20037508.34278925, -20037508.34278925}, upperCorner={20037508.34278925, 20037508.34278925}}
    },
    ["EPSG:4326"] = {
        wkt='GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -90}, upperCorner={180, 90}},
    }
}

-- Utility functions

local function split(sep, str)
    local results = {}
    for value in string.gmatch(str, "([^" .. sep .. "]+)") do
        results[#results + 1] = value
    end
    return results
end

local function join(arr, sep)
    local outStr = ""
    for idx, value in ipairs(arr) do
        outStr = outStr .. tostring(value) .. (idx < #arr and sep or "")
    end
    return outStr
end

local function get_query_param (param, query_string)
    if not query_string then
        return nil
    end
    local query_parts = split("&", query_string)
    local date_string = nil;
    for _, part in pairs(query_parts) do
        local query_pair = split("=", part)
        if string.lower(query_pair[1]) == param then
            return query_pair[2]
        end
    end
    return date_string
end
    
local function sendResponse(code, msg_string)
    return msg_string,
    {
        ["Content-Type"] = "text/xml"
    },
    code
end

local function getExtensionFromMimeType(mimeType)
    if mimeType == "image/jpeg" then
        return ".jpeg"
    end
    if mimeType == "image/png" then
        return ".png"
    end
    if mimeType == "image/tiff" then
        return ".tiff"
    end
    if mimeType == "image/lerc" then
        return ".lerc"
    end
    if mimeType == "application/x-protobuf;type=mapbox-vector" then
        return ".pbf"
    end
    if mimeType == "application/vnd.mapbox-vector-tile" then
        return ".mvt"
    end
end

local function getDateList(endpointConfig)
    local dateServiceUri = endpointConfig["time_service_uri"]
    local dateServiceKeys = endpointConfig["time_service_keys"]
    if dateServiceKeys then
        local formattedKeys = {}
        for idx, value in ipairs(dateServiceKeys) do
            formattedKeys[#formattedKeys + 1] = "key" .. tostring(idx) .. "=" .. value
        end
        local keyString = join(formattedKeys, "&")
        if string.sub(dateServiceUri, -1) ~= "?" then
            dateServiceUri = dateServiceUri .. "?"
        end
        dateServiceUri = dateServiceUri .. keyString
    end
    local headers, stream = assert(request.new_from_uri(dateServiceUri):go(5))
    local body = assert(stream:get_body_as_string())
    if headers:get ":status" ~= "200" then
        print("Error contacting date service: " .. body)
        return nil
    end
    local dateList = JSON:decode(body)
    return dateList or {}
end

local function getTmsDefs(tmsXml)
    local tmsDefs = {}
    for _, proj in ipairs(tmsXml:get_elements_with_name("Projection")) do
        local tileMatrixSets = {}
        for _, tms in ipairs(proj:get_elements_with_name("TileMatrixSet")) do
            local tmsName = tms:get_elements_with_name("ows:Identifier")[1]:get_text()
            local matrices = {}
            for matrix in tms:childtags() do
                if matrix.tag == "TileMatrix" then
                    local identifier = matrix:get_elements_with_name("ows:Identifier")[1]:get_text()
                    local topLeft = split(" ", matrix:get_elements_with_name("TopLeftCorner")[1]:get_text())
                    matrices[tonumber(identifier) + 1] = {
                        topLeft={tonumber(topLeft[1]), tonumber(topLeft[2])},
                        scaleDenominator = matrix:get_elements_with_name("ScaleDenominator")[1]:get_text(),
                        matrixWidth = matrix:get_elements_with_name("MatrixWidth")[1]:get_text(),
                        matrixHeight = matrix:get_elements_with_name("MatrixHeight")[1]:get_text(),
                        tileWidth = matrix:get_elements_with_name("TileWidth")[1]:get_text(),
                        tileHeight = matrix:get_elements_with_name("TileHeight")[1]:get_text()
                    }
                end
            end
        tileMatrixSets[tmsName] = matrices
        end
        tmsDefs[proj:get_attribs()["id"]] = tileMatrixSets
    end
    return tmsDefs
end

local function getReprojectedTms(sourceTms, targetEpsgCode, tmsDefs)
    -- Start by getting the maximum ScaleDenominator for the source TMS
    local function sortTms(a,b)
        return tonumber(a["scaleDenominator"]) < tonumber(b["scaleDenominator"])
    end
    table.sort(sourceTms, sortTms)
    local sourceScaleDenom = sourceTms[1]["scaleDenominator"]

    -- Now find the TMS in the destination projection that has the closest max scale denominator
    -- to the source TMS

    -- Sort the possible reprojected TileMatrixSets by the value of the biggest scale denominator in their TileMatrix(s).
    local sortedTmsDefs = {}
    for name, tms in pairs(tmsDefs[targetEpsgCode]) do
        table.sort(tms, sortTms)
        table.insert(sortedTmsDefs, {name = name, scaleDenominator = tms[1]["scaleDenominator"]})
    end
    table.sort(sortedTmsDefs, sortTms)

    local targetTms
    local targetTmsName
    local idx = #sortedTmsDefs
    while idx > 0 do
        local tmsInfo = sortedTmsDefs[idx]
        if tonumber(tmsInfo["scaleDenominator"]) < tonumber(sourceScaleDenom) then
            return targetTmsName, targetTms
        end
        targetTmsName = tmsInfo["name"]
        targetTms = tmsDefs[targetEpsgCode][targetTmsName]
        idx = idx - 1
    end
    return targetTmsName, targetTms
end

-- GetTileService functions

local function makeTiledGroupFromConfig(filename, tmsDefs, epsgCode, targetEpsgCode)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerName = assert(config.layer_name, "Can't find 'layer_name' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    local abstract = assert(config.abstract, "Can't find 'abstract' in YAML!")
    local projInfo = assert(PROJECTIONS[targetEpsgCode or epsgCode], "Can't find projection " .. epsgCode)
    local mimeType = assert(config.mime_type, "Can't find MIME type in YAML!")
    local bands = mimeType == "image/png" and "4" or "3"
    local static = true
    if config.static ~= nil then
        static = config.static
    end
    local bbox = projInfo["bbox"] or projInfo["bbox84"]
    local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")

    local tiledGroupNode = xml.elem("TiledGroup", {
        xml.new("Name"):text(layerName),
        xml.new("Title", {["xml:lang"]="en"}):text(layerTitle),
        xml.new("Abstract", {["xml:lang"]="en"}):text(abstract),
        xml.new("Projection"):text(projInfo["wkt"]),
        xml.new("Pad"):text("0"),
        xml.new("Bands"):text(bands),
        xml.new("LatLonBoundingBox", {
            minx=bbox["lowerCorner"][1],
            miny=bbox["lowerCorner"][2],
            maxx=bbox["upperCorner"][1],
            maxy=bbox["upperCorner"][2]
        }),
        xml.new("Key"):text("${time}")}
    )

    local matrices = tmsDefs[epsgCode][tmsName]
    if targetEpsgCode and targetEpsgCode ~= epsgCode then
        _, matrices = getReprojectedTms(matrices, targetEpsgCode, tmsDefs)
    end
    table.sort(matrices, function (a, b)
        return tonumber(a["scaleDenominator"]) < tonumber(b["scaleDenominator"])
    end)
    targetEpsgCode = targetEpsgCode or epsgCode

    for _, matrix in ipairs(matrices) do
        local widthInPx = math.ceil(2 * math.pi * EARTH_RADIUS / (matrix["scaleDenominator"] * 0.28E-3))
        local heightInPx = targetEpsgCode == "EPSG:4326" and widthInPx / 2 or widthInPx
        local widthRatio = widthInPx / (matrix["tileWidth"] * matrix["matrixWidth"])
        local heightRatio = heightInPx / (matrix["tileHeight"] * matrix["matrixHeight"])
        local resx = math.ceil((bbox["upperCorner"][1] - bbox["lowerCorner"][1]) / (matrix["matrixWidth"] * widthRatio))
        local resy = math.ceil((bbox["upperCorner"][2] - bbox["lowerCorner"][2]) / (matrix["matrixHeight"] * heightRatio))
        local xmax = matrix["topLeft"][1] + resx
        local ymax = matrix["topLeft"][2] - resy
        local template = "request=GetMap&layers=${layer}&srs=${epsg_code}&format=${mime_type}&styles=${time}&width=${width}&height=${height}&bbox=${bbox}"
        local bboxOut = {}
        for _, param in ipairs({matrix["topLeft"][1], ymax, xmax, matrix["topLeft"][2]}) do
            -- Old version of OE rounded zeroes
            if param < 1 and param > -1 then
                table.insert(bboxOut, 0)
            else
                table.insert(bboxOut, string.format("%.6f", param))
            end
        end
        local make_uri = function(hasTime)
            local time = hasTime and "${time}" or ""
            return string.gsub(template, "${layer}", layerId)
                :gsub("${epsg_code}", targetEpsgCode)
                :gsub("${mime_type}", mimeType)
                :gsub("${time}", time)
                :gsub("${width}", matrix["tileWidth"])
                :gsub("${height}", matrix["tileHeight"])
                :gsub("${bbox}", join(bboxOut, ","))
            end
        local outString = static and make_uri(false) or make_uri(true) .. "\n" .. make_uri(false)
        outString = "<![CDATA[" .. outString .. "]]>"
        tiledGroupNode:add_child(xml.new("TilePattern"):text(outString))
    end
    return tiledGroupNode
end

local function getAllGTSTiledGroups(endpointConfig, epsgCode, targetEpsgCode)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    local tmsDefs = getTmsDefs(tmsXml)
    tmsFile:close()

    local layerConfigSource = endpointConfig["layer_config_source"]

    local nodeList = {}
    local fileAttrs = lfs.attributes(layerConfigSource)
    
    if not fileAttrs then
        print("Can't open layer config location: " .. layerConfigSource)
        return
    end
    
    if fileAttrs["mode"] == "file" then
        nodeList[1] = makeTiledGroupFromConfig(layerConfigSource, tmsDefs, epsgCode, targetEpsgCode)
    end

    if fileAttrs["mode"] == "directory" then
        -- Only going down a single directory level
        for file in lfs.dir(layerConfigSource) do
            if lfs.attributes(layerConfigSource .. "/" .. file)["mode"] == "file" and
                string.sub(file, 0, 1) ~= "." then
                nodeList[#nodeList + 1] = makeTiledGroupFromConfig(layerConfigSource .. "/" .. file,
                 tmsDefs, epsgCode, targetEpsgCode)
            end
        end
    end
    return nodeList
end

local function makeGTS(endpointConfig)
    -- Parse header
    if not endpointConfig["gts_header_file"] then
        print("Error: no 'gts_header_file' configured!")
    end
    local headerFile = assert(io.open(endpointConfig["gts_header_file"], "r"),
        "Can't open GTS header file at:" .. endpointConfig["gts_header_file"])
    local mainXml = xml.parse(headerFile:read("*all"))
    headerFile:close()

    -- Load projection (add "EPSG:" part if omitted)
    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"]["target_epsg_code"]
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    local projection = PROJECTIONS[targetEpsgCode or epsgCode]
    local bbox = projection["bbox"] or projection["bbox84"]

    local serviceNode = mainXml:get_elements_with_name("Service")[1]
    local onlineResourceNode = serviceNode:get_elements_with_name("OnlineResource")[1]

    -- Build <TiledPatterns> section
    local href_url = onlineResourceNode:get_attribs()["xlink:href"]
    local tiledPatternsNode = xml.elem("TiledPatterns", {
        xml.new("OnlineResource", {["xlink:href"]=href_url,
            ["xmlns:xlink"]="http://www.w3.org/1999/xlink",
            ["xlink:type"]="simple"}),
        xml.new("LatLonBoundingBox",
            { miny=bbox["lowerCorner"][2],
            minx=bbox["lowerCorner"][1],
            maxx=bbox["upperCorner"][1],
            maxy=bbox["upperCorner"][2]})
    })

    local layers = getAllGTSTiledGroups(endpointConfig, epsgCode, targetEpsgCode)
    if not layers then
        return sendResponse(400, "No layers found!")
    end

    for _, tiledGroup in ipairs(layers) do
        tiledPatternsNode:add_child(tiledGroup)
    end

    -- Add contents to the rest of the XML
    mainXml:add_direct_child(tiledPatternsNode)
    return xml.tostring(mainXml)
end


-- GetCapabilities functions
local function makeTWMSGCLayer(filename, tmsDefs, dateList, epsgCode, targetEpsgCode)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    -- Look for the required data in the YAML config file, and throw errors if we can't find it
    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    local layerAbstract = assert(config.abstract, "Can't find 'abstract' in YAML!")
    -- local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")

    local layerElem = xml.new("Layer", {queryable="0"})
    layerElem:add_child(xml.new("Name"):text(layerId))
    layerElem:add_child(xml.new("Title",  {["xml:lang"]="en"}):text(layerTitle))
    layerElem:add_child(xml.new("Abstract",  {["xml:lang"]="en"}):text(layerAbstract))

    -- Get the information we need from the TMS definitions and add bbox node
    -- local tmsDef = tmsDefs[epsgCode][tmsName]
    -- if not tmsDef then
    --     print("Can't find TileMatrixSet (" .. tmsName .. ") for this layer in the TileMatrixSet definitions file." )
    -- end
    -- if targetEpsgCode ~= epsgCode then
    --     _, tmsDef = getReprojectedTms(tmsDef, targetEpsgCode, tmsDefs)
    -- end

    local bbox = PROJECTIONS[targetEpsgCode or epsgCode]["bbox"] or PROJECTIONS[targetEpsgCode or epsgCode]["bbox84"]

    layerElem:add_child(xml.new("LatLonBoundingBox", {
        minx=tostring(bbox["lowerCorner"][1]),
        miny=tostring(bbox["lowerCorner"][2]),
        maxx=tostring(bbox["upperCorner"][1]),
        maxy=tostring(bbox["upperCorner"][2])
    }))

    layerElem:add_child(xml.elem("Style", {
        xml.new("Name"):text("default"),
        xml.new("Title", {["xml:lang"]="en"}):text("(default) Default style")}
    ))

    layerElem:add_child(xml.new("ScaleHint", {min="10", max="100"}))
    layerElem:add_child(xml.new("MinScaleDenominator"):text("100"))

    return layerElem
end

local function makeGCLayer(filename, tmsDefs, dateList, epsgCode, targetEpsgCode, baseUriGC, baseUriMeta)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    -- Look for the required data in the YAML config file, and throw errors if we can't find it
    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    -- local layerName = assert(config.layer_name, "Can't find 'layer_name' in YAML!")
    local mimeType = assert(config.mime_type, "Can't find MIME type in YAML!")
    local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")
    local static = true
    if config.static ~= nil then
        static = config.static
    end

    local defaultDate
    local periods
    local dateInfo
    if not static then
        dateInfo = dateList[layerId] or dateList[config.alias]
        if dateInfo then
            defaultDate = config.default_date or dateInfo["default"]
            periods = config.periods or dateInfo["periods"]
        else
            print("Can't find entry for layer " .. layerId .. " in date service list.")
        end
    end

    local layerElem = xml.elem("Layer")

    layerElem:add_child(xml.new("ows:Title", {["xml:lang"]="en"}):text(layerTitle))

    -- Get the information we need from the TMS definitions and add bbox node
    local tmsDef = tmsDefs[epsgCode][tmsName]
    if not tmsDef then
        print("Can't find TileMatrixSet (" .. tmsName .. ") for this layer in the TileMatrixSet definitions file." )
    end
    if targetEpsgCode ~= epsgCode then
        tmsName, tmsDef = getReprojectedTms(tmsDef, targetEpsgCode, tmsDefs)
    end

    local upperCorner = tostring(tmsDef[1]["topLeft"][1] * -1) .. " " .. tostring(tmsDef[2]["topLeft"][2])
    local lowerCorner = tostring(tmsDef[1]["topLeft"][1]) .. " " .. tostring(tmsDef[2]["topLeft"][2] * -1)

    local bbox_elem_84 = xml.elem("ows:WGS84BoundingBox",
        {xml.elem("ows:LowerCorner"):text(lowerCorner), xml.elem("ows:UpperCorner"):text(upperCorner)})
    bbox_elem_84:set_attrib("crs","urn:ogc:def:crs:OGC:2:84")
    layerElem:add_child(bbox_elem_84)

    local bbox_elem = xml.elem("ows:BoundingBox",
        {xml.elem("ows:LowerCorner"):text(lowerCorner), xml.elem("ows:UpperCorner"):text(upperCorner)})
    bbox_elem:set_attrib("crs", "urn:ogc:def:crs:EPSG::" .. split(":", targetEpsgCode)[2])
    layerElem:add_child(bbox_elem)

    -- Add identifier node
    local id_elem = xml.new("ows:Identifier")
    id_elem:text(layerId)
    layerElem:add_child(id_elem)

    -- Build Metadata and add nodes
    if config.metadata then
        for _, metadata in pairs(config.metadata) do
            local metadataNode = xml.new("ows:Metadata")
            for key, value in pairs(metadata) do
                metadataNode:set_attrib(key, string.gsub(value, "{base_uri_meta}", baseUriMeta or ""))
            end
            layerElem:add_child(metadataNode)
        end
    end

    -- Build the Style element
    local styleNode = xml.new("Style", {isDefault="true"})
    styleNode:add_child(xml.new("ows:Title",{["xml:lang"]="en"}):text("default"))
    styleNode:add_child(xml.new("ows:Identifier"):text("default"))
    layerElem:add_child(styleNode)


    -- Build the <Dimension> element for time (if necessary)
    -- Note that we omit this if for some reason we don't have dates from the date service.
    if not static and dateInfo then
        local dimensionNode = xml.elem("Dimension", {
            xml.new("ows:Identifier"):text("Time"),
            xml.new("ows:UOM"):text("ISO8601"),
            xml.new("Default"):text(defaultDate),
            xml.new("Current"):text("false")
        })
        for _, period in pairs(periods) do
            dimensionNode:add_child(xml.new("Value"):text(period))
        end
        layerElem:add_child(dimensionNode)
    end


    -- Build <TileMatrixSetLink> and <Format>
    layerElem:add_child(xml.elem("TileMatrixSetLink", xml.new("TileMatrixSet"):text(tmsName)))
    layerElem:add_child(xml.new("Format"):text(mimeType))

    -- Build the ResourceURL element
    local timeString = not static and "/{Time}" or ""
    if baseUriGC[baseUriGC:len()] ~= "/" then
        baseUriGC = baseUriGC .. '/'
    end
    local template = baseUriGC .. layerId .. "/" .. "default" .. timeString .. "/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}" .. getExtensionFromMimeType(mimeType)
    layerElem:addtag("ResourceURL", {format=mimeType, resourceType="tile", template=template})

    return layerElem
end

local function getAllGCLayerNodes(endpointConfig, tmsXml, epsgCode, targetEpsgCode, twms)
    local tmsDefs = getTmsDefs(tmsXml)
    local dateList = getDateList(endpointConfig)

    local layerConfigSource = endpointConfig["layer_config_source"]

    local buildFunc = twms and makeTWMSGCLayer or makeGCLayer
        if not twms and not endpointConfig["base_uri_gc"] then
        print("Error: no 'base_uri_gc' configured")
    end

    local nodeList = {}

    local fileAttrs = lfs.attributes(layerConfigSource)
    if not fileAttrs then
        print("Can't open layer config location: " .. layerConfigSource)
        return
    end

    if fileAttrs["mode"] == "file" then
        nodeList[1] = buildFunc(layerConfigSource, tmsDefs, dateList, epsgCode, targetEpsgCode, endpointConfig["base_uri_gc"], endpointConfig["base_uri_meta"])
    end
    if fileAttrs["mode"] == "directory" then
        -- Only going down a single directory level
        for file in lfs.dir(layerConfigSource) do
            if lfs.attributes(layerConfigSource .. "/" .. file)["mode"] == "file" and
                string.sub(file, 0, 1) ~= "." then
                nodeList[#nodeList + 1] = buildFunc(layerConfigSource .. "/" .. file,
                 tmsDefs, dateList, epsgCode, targetEpsgCode, endpointConfig["base_uri_gc"], endpointConfig["base_uri_meta"])
            end
        end
    end
    return nodeList
end


local function makeGC(endpointConfig)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    tmsFile:close()

    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"] and endpointConfig["reproject"]["target_epsg_code"] or nil
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    -- Parse header
    local headerFile = assert(io.open(endpointConfig["gc_header_file"], "r"),
        "Can't open GC header file at:" .. endpointConfig["gc_header_file"])
    local dom = xml.parse(headerFile:read("*all"))
    headerFile:close()

    -- Build contents section
    local contentsElem = xml.elem("Contents")
    local layers = getAllGCLayerNodes(endpointConfig, tmsXml, epsgCode, targetEpsgCode)

    if not layers then
        return sendResponse(400, "No layers found!")
    end
   
    for _, layer in ipairs(layers) do
        contentsElem:add_child(layer)
    end

    for _, proj in ipairs(tmsXml:get_elements_with_name("Projection")) do
        if proj:get_attribs()["id"] == targetEpsgCode or not targetEpsgCode and epsgCode then
            for _, tms in ipairs(proj:get_elements_with_name("TileMatrixSet")) do
                contentsElem:add_child(tms)
            end
        end
    end

    -- Add contents section
    dom:add_direct_child(contentsElem)

    -- Move ServiceMetadataURL below contents
    local serviceMetadataURL = dom:get_elements_with_name("ServiceMetadataURL")[1]
    local function removeServiceMetadataURL(x)
      if (x == serviceMetadataURL) then
        return nil
      end
      return x
    end
    dom:maptags(removeServiceMetadataURL)
    dom:add_direct_child(serviceMetadataURL)
    return xml.tostring(dom)
    
end


local function makeTWMSGC(endpointConfig)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    tmsFile:close()

    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"]["target_epsg_code"]
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    -- Parse header
    local headerFile = assert(io.open(endpointConfig["twms_gc_header_file"], "r"),
        "Can't open TWMS GC header file at:" .. endpointConfig["twms_gc_header_file"])
    local content = {}
    local doctype = ""
    if headerFile then
      for line in headerFile:lines() do
          if string.match(line, "DOCTYPE") then
            doctype = line -- parser doesn't like DOCTYPE, so we take it out and prepend again later
          else
            content[#content+1] = line
          end
      end<commit_msg>GITC-1602 Fixed twms reproject target_epsg_code lookup in gc_service<commit_after>local onearth_gc_service = {}

local lfs = require "lfs"
local lyaml = require "lyaml"
local request = require "http.request"
local JSON = require "JSON"
local xml = require "pl.xml"

-- Reference Constants
local EARTH_RADIUS = 6378137.0
local PROJECTIONS = {
    ["EPSG:3031"] = {
        wkt='PROJCS["WGS 84 / Antarctic Polar Stereographic",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",-71],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","3031"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -90}, upperCorner={180, -38.941373}},
        bbox={crs="urn:ogc:def:crs:EPSG::3031", lowerCorner={-4194304, -4194304}, upperCorner={194304, 4194304}}
    },
    ["EPSG:3413"] = {
        wkt='PROJCS["WGS 84 / NSIDC Sea Ice Polar Stereographic North",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Polar_Stereographic"],PARAMETER["latitude_of_origin",70],PARAMETER["central_meridian",-45],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],AUTHORITY["EPSG","3413"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, 38.807151}, upperCorner={180, 90}},
        bbox={crs="urn:ogc:def:crs:EPSG::3413", lowerCorner={-4194304, -4194304}, upperCorner={4194304, 4194304}}
    },
    ["EPSG:3857"] = {
        wkt='PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["X",EAST],AXIS["Y",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -85}, upperCorner={180, 85}},
        bbox={crs="urn:ogc:def:crs:EPSG::3857", lowerCorner={-20037508.34278925, -20037508.34278925}, upperCorner={20037508.34278925, 20037508.34278925}}
    },
    ["EPSG:4326"] = {
        wkt='GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]',
        bbox84={crs="urn:ogc:def:crs:OGC:2:84", lowerCorner={-180, -90}, upperCorner={180, 90}},
    }
}

-- Utility functions

local function split(sep, str)
    local results = {}
    for value in string.gmatch(str, "([^" .. sep .. "]+)") do
        results[#results + 1] = value
    end
    return results
end

local function join(arr, sep)
    local outStr = ""
    for idx, value in ipairs(arr) do
        outStr = outStr .. tostring(value) .. (idx < #arr and sep or "")
    end
    return outStr
end

local function get_query_param (param, query_string)
    if not query_string then
        return nil
    end
    local query_parts = split("&", query_string)
    local date_string = nil;
    for _, part in pairs(query_parts) do
        local query_pair = split("=", part)
        if string.lower(query_pair[1]) == param then
            return query_pair[2]
        end
    end
    return date_string
end
    
local function sendResponse(code, msg_string)
    return msg_string,
    {
        ["Content-Type"] = "text/xml"
    },
    code
end

local function getExtensionFromMimeType(mimeType)
    if mimeType == "image/jpeg" then
        return ".jpeg"
    end
    if mimeType == "image/png" then
        return ".png"
    end
    if mimeType == "image/tiff" then
        return ".tiff"
    end
    if mimeType == "image/lerc" then
        return ".lerc"
    end
    if mimeType == "application/x-protobuf;type=mapbox-vector" then
        return ".pbf"
    end
    if mimeType == "application/vnd.mapbox-vector-tile" then
        return ".mvt"
    end
end

local function getDateList(endpointConfig)
    local dateServiceUri = endpointConfig["time_service_uri"]
    local dateServiceKeys = endpointConfig["time_service_keys"]
    if dateServiceKeys then
        local formattedKeys = {}
        for idx, value in ipairs(dateServiceKeys) do
            formattedKeys[#formattedKeys + 1] = "key" .. tostring(idx) .. "=" .. value
        end
        local keyString = join(formattedKeys, "&")
        if string.sub(dateServiceUri, -1) ~= "?" then
            dateServiceUri = dateServiceUri .. "?"
        end
        dateServiceUri = dateServiceUri .. keyString
    end
    local headers, stream = assert(request.new_from_uri(dateServiceUri):go(5))
    local body = assert(stream:get_body_as_string())
    if headers:get ":status" ~= "200" then
        print("Error contacting date service: " .. body)
        return nil
    end
    local dateList = JSON:decode(body)
    return dateList or {}
end

local function getTmsDefs(tmsXml)
    local tmsDefs = {}
    for _, proj in ipairs(tmsXml:get_elements_with_name("Projection")) do
        local tileMatrixSets = {}
        for _, tms in ipairs(proj:get_elements_with_name("TileMatrixSet")) do
            local tmsName = tms:get_elements_with_name("ows:Identifier")[1]:get_text()
            local matrices = {}
            for matrix in tms:childtags() do
                if matrix.tag == "TileMatrix" then
                    local identifier = matrix:get_elements_with_name("ows:Identifier")[1]:get_text()
                    local topLeft = split(" ", matrix:get_elements_with_name("TopLeftCorner")[1]:get_text())
                    matrices[tonumber(identifier) + 1] = {
                        topLeft={tonumber(topLeft[1]), tonumber(topLeft[2])},
                        scaleDenominator = matrix:get_elements_with_name("ScaleDenominator")[1]:get_text(),
                        matrixWidth = matrix:get_elements_with_name("MatrixWidth")[1]:get_text(),
                        matrixHeight = matrix:get_elements_with_name("MatrixHeight")[1]:get_text(),
                        tileWidth = matrix:get_elements_with_name("TileWidth")[1]:get_text(),
                        tileHeight = matrix:get_elements_with_name("TileHeight")[1]:get_text()
                    }
                end
            end
        tileMatrixSets[tmsName] = matrices
        end
        tmsDefs[proj:get_attribs()["id"]] = tileMatrixSets
    end
    return tmsDefs
end

local function getReprojectedTms(sourceTms, targetEpsgCode, tmsDefs)
    -- Start by getting the maximum ScaleDenominator for the source TMS
    local function sortTms(a,b)
        return tonumber(a["scaleDenominator"]) < tonumber(b["scaleDenominator"])
    end
    table.sort(sourceTms, sortTms)
    local sourceScaleDenom = sourceTms[1]["scaleDenominator"]

    -- Now find the TMS in the destination projection that has the closest max scale denominator
    -- to the source TMS

    -- Sort the possible reprojected TileMatrixSets by the value of the biggest scale denominator in their TileMatrix(s).
    local sortedTmsDefs = {}
    for name, tms in pairs(tmsDefs[targetEpsgCode]) do
        table.sort(tms, sortTms)
        table.insert(sortedTmsDefs, {name = name, scaleDenominator = tms[1]["scaleDenominator"]})
    end
    table.sort(sortedTmsDefs, sortTms)

    local targetTms
    local targetTmsName
    local idx = #sortedTmsDefs
    while idx > 0 do
        local tmsInfo = sortedTmsDefs[idx]
        if tonumber(tmsInfo["scaleDenominator"]) < tonumber(sourceScaleDenom) then
            return targetTmsName, targetTms
        end
        targetTmsName = tmsInfo["name"]
        targetTms = tmsDefs[targetEpsgCode][targetTmsName]
        idx = idx - 1
    end
    return targetTmsName, targetTms
end

-- GetTileService functions

local function makeTiledGroupFromConfig(filename, tmsDefs, epsgCode, targetEpsgCode)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerName = assert(config.layer_name, "Can't find 'layer_name' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    local abstract = assert(config.abstract, "Can't find 'abstract' in YAML!")
    local projInfo = assert(PROJECTIONS[targetEpsgCode or epsgCode], "Can't find projection " .. epsgCode)
    local mimeType = assert(config.mime_type, "Can't find MIME type in YAML!")
    local bands = mimeType == "image/png" and "4" or "3"
    local static = true
    if config.static ~= nil then
        static = config.static
    end
    local bbox = projInfo["bbox"] or projInfo["bbox84"]
    local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")

    local tiledGroupNode = xml.elem("TiledGroup", {
        xml.new("Name"):text(layerName),
        xml.new("Title", {["xml:lang"]="en"}):text(layerTitle),
        xml.new("Abstract", {["xml:lang"]="en"}):text(abstract),
        xml.new("Projection"):text(projInfo["wkt"]),
        xml.new("Pad"):text("0"),
        xml.new("Bands"):text(bands),
        xml.new("LatLonBoundingBox", {
            minx=bbox["lowerCorner"][1],
            miny=bbox["lowerCorner"][2],
            maxx=bbox["upperCorner"][1],
            maxy=bbox["upperCorner"][2]
        }),
        xml.new("Key"):text("${time}")}
    )

    local matrices = tmsDefs[epsgCode][tmsName]
    if targetEpsgCode and targetEpsgCode ~= epsgCode then
        _, matrices = getReprojectedTms(matrices, targetEpsgCode, tmsDefs)
    end
    table.sort(matrices, function (a, b)
        return tonumber(a["scaleDenominator"]) < tonumber(b["scaleDenominator"])
    end)
    targetEpsgCode = targetEpsgCode or epsgCode

    for _, matrix in ipairs(matrices) do
        local widthInPx = math.ceil(2 * math.pi * EARTH_RADIUS / (matrix["scaleDenominator"] * 0.28E-3))
        local heightInPx = targetEpsgCode == "EPSG:4326" and widthInPx / 2 or widthInPx
        local widthRatio = widthInPx / (matrix["tileWidth"] * matrix["matrixWidth"])
        local heightRatio = heightInPx / (matrix["tileHeight"] * matrix["matrixHeight"])
        local resx = math.ceil((bbox["upperCorner"][1] - bbox["lowerCorner"][1]) / (matrix["matrixWidth"] * widthRatio))
        local resy = math.ceil((bbox["upperCorner"][2] - bbox["lowerCorner"][2]) / (matrix["matrixHeight"] * heightRatio))
        local xmax = matrix["topLeft"][1] + resx
        local ymax = matrix["topLeft"][2] - resy
        local template = "request=GetMap&layers=${layer}&srs=${epsg_code}&format=${mime_type}&styles=${time}&width=${width}&height=${height}&bbox=${bbox}"
        local bboxOut = {}
        for _, param in ipairs({matrix["topLeft"][1], ymax, xmax, matrix["topLeft"][2]}) do
            -- Old version of OE rounded zeroes
            if param < 1 and param > -1 then
                table.insert(bboxOut, 0)
            else
                table.insert(bboxOut, string.format("%.6f", param))
            end
        end
        local make_uri = function(hasTime)
            local time = hasTime and "${time}" or ""
            return string.gsub(template, "${layer}", layerId)
                :gsub("${epsg_code}", targetEpsgCode)
                :gsub("${mime_type}", mimeType)
                :gsub("${time}", time)
                :gsub("${width}", matrix["tileWidth"])
                :gsub("${height}", matrix["tileHeight"])
                :gsub("${bbox}", join(bboxOut, ","))
            end
        local outString = static and make_uri(false) or make_uri(true) .. "\n" .. make_uri(false)
        outString = "<![CDATA[" .. outString .. "]]>"
        tiledGroupNode:add_child(xml.new("TilePattern"):text(outString))
    end
    return tiledGroupNode
end

local function getAllGTSTiledGroups(endpointConfig, epsgCode, targetEpsgCode)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    local tmsDefs = getTmsDefs(tmsXml)
    tmsFile:close()

    local layerConfigSource = endpointConfig["layer_config_source"]

    local nodeList = {}
    local fileAttrs = lfs.attributes(layerConfigSource)
    
    if not fileAttrs then
        print("Can't open layer config location: " .. layerConfigSource)
        return
    end
    
    if fileAttrs["mode"] == "file" then
        nodeList[1] = makeTiledGroupFromConfig(layerConfigSource, tmsDefs, epsgCode, targetEpsgCode)
    end

    if fileAttrs["mode"] == "directory" then
        -- Only going down a single directory level
        for file in lfs.dir(layerConfigSource) do
            if lfs.attributes(layerConfigSource .. "/" .. file)["mode"] == "file" and
                string.sub(file, 0, 1) ~= "." then
                nodeList[#nodeList + 1] = makeTiledGroupFromConfig(layerConfigSource .. "/" .. file,
                 tmsDefs, epsgCode, targetEpsgCode)
            end
        end
    end
    return nodeList
end

local function makeGTS(endpointConfig)
    -- Parse header
    if not endpointConfig["gts_header_file"] then
        print("Error: no 'gts_header_file' configured!")
    end
    local headerFile = assert(io.open(endpointConfig["gts_header_file"], "r"),
        "Can't open GTS header file at:" .. endpointConfig["gts_header_file"])
    local mainXml = xml.parse(headerFile:read("*all"))
    headerFile:close()

    -- Load projection (add "EPSG:" part if omitted)
    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"] and endpointConfig["reproject"]["target_epsg_code"] or endpointConfig["target_epsg_code"]
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    local projection = PROJECTIONS[targetEpsgCode or epsgCode]
    local bbox = projection["bbox"] or projection["bbox84"]

    local serviceNode = mainXml:get_elements_with_name("Service")[1]
    local onlineResourceNode = serviceNode:get_elements_with_name("OnlineResource")[1]

    -- Build <TiledPatterns> section
    local href_url = onlineResourceNode:get_attribs()["xlink:href"]
    local tiledPatternsNode = xml.elem("TiledPatterns", {
        xml.new("OnlineResource", {["xlink:href"]=href_url,
            ["xmlns:xlink"]="http://www.w3.org/1999/xlink",
            ["xlink:type"]="simple"}),
        xml.new("LatLonBoundingBox",
            { miny=bbox["lowerCorner"][2],
            minx=bbox["lowerCorner"][1],
            maxx=bbox["upperCorner"][1],
            maxy=bbox["upperCorner"][2]})
    })

    local layers = getAllGTSTiledGroups(endpointConfig, epsgCode, targetEpsgCode)
    if not layers then
        return sendResponse(400, "No layers found!")
    end

    for _, tiledGroup in ipairs(layers) do
        tiledPatternsNode:add_child(tiledGroup)
    end

    -- Add contents to the rest of the XML
    mainXml:add_direct_child(tiledPatternsNode)
    return xml.tostring(mainXml)
end


-- GetCapabilities functions
local function makeTWMSGCLayer(filename, tmsDefs, dateList, epsgCode, targetEpsgCode)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    -- Look for the required data in the YAML config file, and throw errors if we can't find it
    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    local layerAbstract = assert(config.abstract, "Can't find 'abstract' in YAML!")
    -- local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")

    local layerElem = xml.new("Layer", {queryable="0"})
    layerElem:add_child(xml.new("Name"):text(layerId))
    layerElem:add_child(xml.new("Title",  {["xml:lang"]="en"}):text(layerTitle))
    layerElem:add_child(xml.new("Abstract",  {["xml:lang"]="en"}):text(layerAbstract))

    -- Get the information we need from the TMS definitions and add bbox node
    -- local tmsDef = tmsDefs[epsgCode][tmsName]
    -- if not tmsDef then
    --     print("Can't find TileMatrixSet (" .. tmsName .. ") for this layer in the TileMatrixSet definitions file." )
    -- end
    -- if targetEpsgCode ~= epsgCode then
    --     _, tmsDef = getReprojectedTms(tmsDef, targetEpsgCode, tmsDefs)
    -- end

    local bbox = PROJECTIONS[targetEpsgCode or epsgCode]["bbox"] or PROJECTIONS[targetEpsgCode or epsgCode]["bbox84"]

    layerElem:add_child(xml.new("LatLonBoundingBox", {
        minx=tostring(bbox["lowerCorner"][1]),
        miny=tostring(bbox["lowerCorner"][2]),
        maxx=tostring(bbox["upperCorner"][1]),
        maxy=tostring(bbox["upperCorner"][2])
    }))

    layerElem:add_child(xml.elem("Style", {
        xml.new("Name"):text("default"),
        xml.new("Title", {["xml:lang"]="en"}):text("(default) Default style")}
    ))

    layerElem:add_child(xml.new("ScaleHint", {min="10", max="100"}))
    layerElem:add_child(xml.new("MinScaleDenominator"):text("100"))

    return layerElem
end

local function makeGCLayer(filename, tmsDefs, dateList, epsgCode, targetEpsgCode, baseUriGC, baseUriMeta)
    -- Load and parse the YAML config file
    local configFile = assert(io.open(filename, "r"))
    local config = lyaml.load(configFile:read("*all"))
    configFile:close()

    -- Look for the required data in the YAML config file, and throw errors if we can't find it
    local layerId = assert(config.layer_id, "Can't find 'layer_id' in YAML!")
    local layerTitle = assert(config.layer_title, "Can't find 'layer_title' in YAML!")
    -- local layerName = assert(config.layer_name, "Can't find 'layer_name' in YAML!")
    local mimeType = assert(config.mime_type, "Can't find MIME type in YAML!")
    local tmsName = assert(config.tilematrixset, "Can't find TileMatrixSet name in YAML!")
    local static = true
    if config.static ~= nil then
        static = config.static
    end

    local defaultDate
    local periods
    local dateInfo
    if not static then
        dateInfo = dateList[layerId] or dateList[config.alias]
        if dateInfo then
            defaultDate = config.default_date or dateInfo["default"]
            periods = config.periods or dateInfo["periods"]
        else
            print("Can't find entry for layer " .. layerId .. " in date service list.")
        end
    end

    local layerElem = xml.elem("Layer")

    layerElem:add_child(xml.new("ows:Title", {["xml:lang"]="en"}):text(layerTitle))

    -- Get the information we need from the TMS definitions and add bbox node
    local tmsDef = tmsDefs[epsgCode][tmsName]
    if not tmsDef then
        print("Can't find TileMatrixSet (" .. tmsName .. ") for this layer in the TileMatrixSet definitions file." )
    end
    if targetEpsgCode ~= epsgCode then
        tmsName, tmsDef = getReprojectedTms(tmsDef, targetEpsgCode, tmsDefs)
    end

    local upperCorner = tostring(tmsDef[1]["topLeft"][1] * -1) .. " " .. tostring(tmsDef[2]["topLeft"][2])
    local lowerCorner = tostring(tmsDef[1]["topLeft"][1]) .. " " .. tostring(tmsDef[2]["topLeft"][2] * -1)

    local bbox_elem_84 = xml.elem("ows:WGS84BoundingBox",
        {xml.elem("ows:LowerCorner"):text(lowerCorner), xml.elem("ows:UpperCorner"):text(upperCorner)})
    bbox_elem_84:set_attrib("crs","urn:ogc:def:crs:OGC:2:84")
    layerElem:add_child(bbox_elem_84)

    local bbox_elem = xml.elem("ows:BoundingBox",
        {xml.elem("ows:LowerCorner"):text(lowerCorner), xml.elem("ows:UpperCorner"):text(upperCorner)})
    bbox_elem:set_attrib("crs", "urn:ogc:def:crs:EPSG::" .. split(":", targetEpsgCode)[2])
    layerElem:add_child(bbox_elem)

    -- Add identifier node
    local id_elem = xml.new("ows:Identifier")
    id_elem:text(layerId)
    layerElem:add_child(id_elem)

    -- Build Metadata and add nodes
    if config.metadata then
        for _, metadata in pairs(config.metadata) do
            local metadataNode = xml.new("ows:Metadata")
            for key, value in pairs(metadata) do
                metadataNode:set_attrib(key, string.gsub(value, "{base_uri_meta}", baseUriMeta or ""))
            end
            layerElem:add_child(metadataNode)
        end
    end

    -- Build the Style element
    local styleNode = xml.new("Style", {isDefault="true"})
    styleNode:add_child(xml.new("ows:Title",{["xml:lang"]="en"}):text("default"))
    styleNode:add_child(xml.new("ows:Identifier"):text("default"))
    layerElem:add_child(styleNode)


    -- Build the <Dimension> element for time (if necessary)
    -- Note that we omit this if for some reason we don't have dates from the date service.
    if not static and dateInfo then
        local dimensionNode = xml.elem("Dimension", {
            xml.new("ows:Identifier"):text("Time"),
            xml.new("ows:UOM"):text("ISO8601"),
            xml.new("Default"):text(defaultDate),
            xml.new("Current"):text("false")
        })
        for _, period in pairs(periods) do
            dimensionNode:add_child(xml.new("Value"):text(period))
        end
        layerElem:add_child(dimensionNode)
    end


    -- Build <TileMatrixSetLink> and <Format>
    layerElem:add_child(xml.elem("TileMatrixSetLink", xml.new("TileMatrixSet"):text(tmsName)))
    layerElem:add_child(xml.new("Format"):text(mimeType))

    -- Build the ResourceURL element
    local timeString = not static and "/{Time}" or ""
    if baseUriGC[baseUriGC:len()] ~= "/" then
        baseUriGC = baseUriGC .. '/'
    end
    local template = baseUriGC .. layerId .. "/" .. "default" .. timeString .. "/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}" .. getExtensionFromMimeType(mimeType)
    layerElem:addtag("ResourceURL", {format=mimeType, resourceType="tile", template=template})

    return layerElem
end

local function getAllGCLayerNodes(endpointConfig, tmsXml, epsgCode, targetEpsgCode, twms)
    local tmsDefs = getTmsDefs(tmsXml)
    local dateList = getDateList(endpointConfig)

    local layerConfigSource = endpointConfig["layer_config_source"]

    local buildFunc = twms and makeTWMSGCLayer or makeGCLayer
        if not twms and not endpointConfig["base_uri_gc"] then
        print("Error: no 'base_uri_gc' configured")
    end

    local nodeList = {}

    local fileAttrs = lfs.attributes(layerConfigSource)
    if not fileAttrs then
        print("Can't open layer config location: " .. layerConfigSource)
        return
    end

    if fileAttrs["mode"] == "file" then
        nodeList[1] = buildFunc(layerConfigSource, tmsDefs, dateList, epsgCode, targetEpsgCode, endpointConfig["base_uri_gc"], endpointConfig["base_uri_meta"])
    end
    if fileAttrs["mode"] == "directory" then
        -- Only going down a single directory level
        for file in lfs.dir(layerConfigSource) do
            if lfs.attributes(layerConfigSource .. "/" .. file)["mode"] == "file" and
                string.sub(file, 0, 1) ~= "." then
                nodeList[#nodeList + 1] = buildFunc(layerConfigSource .. "/" .. file,
                 tmsDefs, dateList, epsgCode, targetEpsgCode, endpointConfig["base_uri_gc"], endpointConfig["base_uri_meta"])
            end
        end
    end
    return nodeList
end


local function makeGC(endpointConfig)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    tmsFile:close()

    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"] and endpointConfig["reproject"]["target_epsg_code"] or endpointConfig["target_epsg_code"]
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    -- Parse header
    local headerFile = assert(io.open(endpointConfig["gc_header_file"], "r"),
        "Can't open GC header file at:" .. endpointConfig["gc_header_file"])
    local dom = xml.parse(headerFile:read("*all"))
    headerFile:close()

    -- Build contents section
    local contentsElem = xml.elem("Contents")
    local layers = getAllGCLayerNodes(endpointConfig, tmsXml, epsgCode, targetEpsgCode)

    if not layers then
        return sendResponse(400, "No layers found!")
    end
   
    for _, layer in ipairs(layers) do
        contentsElem:add_child(layer)
    end

    for _, proj in ipairs(tmsXml:get_elements_with_name("Projection")) do
        if proj:get_attribs()["id"] == targetEpsgCode or not targetEpsgCode and epsgCode then
            for _, tms in ipairs(proj:get_elements_with_name("TileMatrixSet")) do
                contentsElem:add_child(tms)
            end
        end
    end

    -- Add contents section
    dom:add_direct_child(contentsElem)

    -- Move ServiceMetadataURL below contents
    local serviceMetadataURL = dom:get_elements_with_name("ServiceMetadataURL")[1]
    local function removeServiceMetadataURL(x)
      if (x == serviceMetadataURL) then
        return nil
      end
      return x
    end
    dom:maptags(removeServiceMetadataURL)
    dom:add_direct_child(serviceMetadataURL)
    return xml.tostring(dom)
    
end


local function makeTWMSGC(endpointConfig)
    -- Load TMS defs
    local tmsFile = assert(io.open(endpointConfig["tms_defs_file"], "r")
        , "Can't open tile matrixsets definition file at: " .. endpointConfig["tms_defs_file"])
    local tmsXml = xml.parse(tmsFile:read("*all"))
    tmsFile:close()

    local epsgCode = assert(endpointConfig["epsg_code"], "Can't find epsg_code in endpoint config!")
    if string.match(epsgCode:lower(), "^%d") then
        epsgCode = "EPSG:" .. epsgCode
    end

    local targetEpsgCode = endpointConfig["reproject"] and endpointConfig["reproject"]["target_epsg_code"] or endpointConfig["target_epsg_code"]
    if targetEpsgCode then
        if targetEpsgCode and string.match(targetEpsgCode:lower(), "^%d") then
            targetEpsgCode = "EPSG:" .. targetEpsgCode
        end
    else
        targetEpsgCode = epsgCode
    end

    -- Parse header
    local headerFile = assert(io.open(endpointConfig["twms_gc_header_file"], "r"),
        "Can't open TWMS GC header file at:" .. endpointConfig["twms_gc_header_file"])
    local content = {}
    local doctype = ""
    if headerFile then
      for line in headerFile:lines() do
          if string.match(line, "DOCTYPE") then
            doctype = line -- parser doesn't like DOCTYPE, so we take it out and prepend again later
          else
            content[#content+1] = line
          end
      end
{'fraction_non_alphanumeric': 0.1375008897430422, 'fraction_numerical': 0.024628087408356467, 'mean_word_length': 3.451247524752475, 'pattern_counts': {'":': 4, '<': 23, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 128}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1066500', 'n_tokens_mistral': 19527, 'n_tokens_neox': 18486, 'n_words': 3758}