mirror of
https://github.com/consbio/mbtileserver.git
synced 2024-09-11 07:15:24 +03:00
475 lines
14 KiB
Go
475 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type arcGISLOD struct {
|
|
Level int `json:"level"`
|
|
Resolution float64 `json:"resolution"`
|
|
Scale float64 `json:"scale"`
|
|
}
|
|
|
|
type arcGISSpatialReference struct {
|
|
Wkid uint16 `json:"wkid"`
|
|
}
|
|
|
|
type arcGISExtent struct {
|
|
Xmin float64 `json:"xmin"`
|
|
Ymin float64 `json:"ymin"`
|
|
Xmax float64 `json:"xmax"`
|
|
Ymax float64 `json:"ymax"`
|
|
SpatialReference arcGISSpatialReference `json:"spatialReference"`
|
|
}
|
|
|
|
type arcGISLayerStub struct {
|
|
ID uint8 `json:"id"`
|
|
Name string `json:"name"`
|
|
ParentLayerID int16 `json:"parentLayerId"`
|
|
DefaultVisibility bool `json:"defaultVisibility"`
|
|
SubLayerIDs []uint8 `json:"subLayerIds"`
|
|
MinScale float64 `json:"minScale"`
|
|
MaxScale float64 `json:"maxScale"`
|
|
}
|
|
|
|
type arcGISLayer struct {
|
|
ID uint8 `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Description string `json:"description"`
|
|
GeometryType string `json:"geometryType"`
|
|
CopyrightText string `json:"copyrightText"`
|
|
ParentLayer interface{} `json:"parentLayer"`
|
|
SubLayers []arcGISLayerStub `json:"subLayers"`
|
|
MinScale float64 `json:"minScale"`
|
|
MaxScale float64 `json:"maxScale"`
|
|
DefaultVisibility bool `json:"defaultVisibility"`
|
|
Extent arcGISExtent `json:"extent"`
|
|
HasAttachments bool `json:"hasAttachments"`
|
|
HTMLPopupType string `json:"htmlPopupType"`
|
|
DrawingInfo interface{} `json:"drawingInfo"`
|
|
DisplayField interface{} `json:"displayField"`
|
|
Fields []interface{} `json:"fields"`
|
|
TypeIDField interface{} `json:"typeIdField"`
|
|
Types interface{} `json:"types"`
|
|
Relationships []interface{} `json:"relationships"`
|
|
Capabilities string `json:"capabilities"`
|
|
CurrentVersion float32 `json:"currentVersion"`
|
|
}
|
|
|
|
// root of ArcGIS server is at /arcgis/rest/
|
|
// all map services are served under /arcgis/rest/services
|
|
|
|
const ArcGISRoot = "/arcgis/rest/"
|
|
const ArcGISServicesRoot = ArcGISRoot + "services/"
|
|
const ArcGISInfoRoot = ArcGISRoot + "info"
|
|
|
|
var webMercatorSR = arcGISSpatialReference{Wkid: 3857}
|
|
var geographicSR = arcGISSpatialReference{Wkid: 4326}
|
|
|
|
func arcgisInfoJSON() ([]byte, error) {
|
|
out := map[string]interface{}{
|
|
"currentVersion": 10.71,
|
|
"fullVersion": "10.7.1",
|
|
"soapUrl": nil,
|
|
"secureSoapUrl": nil,
|
|
}
|
|
|
|
bytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes, nil
|
|
}
|
|
|
|
func (svc *ServiceSet) arcgisInfoHandler(w http.ResponseWriter, r *http.Request) {
|
|
infoJSON, err := arcgisInfoJSON()
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
svc.logError("Could not render ArcGIS Server Info JSON for %v: %v", r.URL.Path, err)
|
|
return
|
|
}
|
|
|
|
err = wrapJSONP(w, r, infoJSON)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
svc.logError("Could not render ArcGIS Server Info JSON to JSONP for %v: %v", r.URL.Path, err)
|
|
}
|
|
}
|
|
|
|
// arcGISServiceJSON returns ArcGIS standard JSON describing the ArcGIS
|
|
// tile service.
|
|
func (ts *Tileset) arcgisServiceJSON() ([]byte, error) {
|
|
db := ts.db
|
|
imgFormat := db.GetTileFormat().String()
|
|
metadata, err := db.ReadMetadata()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name, _ := metadata["name"].(string)
|
|
description, _ := metadata["description"].(string)
|
|
attribution, _ := metadata["attribution"].(string)
|
|
tags, _ := metadata["tags"].(string)
|
|
credits, _ := metadata["credits"].(string)
|
|
|
|
// TODO: make sure that min and max zoom always populated
|
|
minZoom, _ := metadata["minzoom"].(int)
|
|
maxZoom, _ := metadata["maxzoom"].(int)
|
|
// TODO: extract dpi from the image instead
|
|
var lods []arcGISLOD
|
|
|
|
for i := minZoom; i <= maxZoom; i++ {
|
|
scale, resolution := calcScaleResolution(i, dpi)
|
|
lods = append(lods, arcGISLOD{
|
|
Level: i,
|
|
Resolution: resolution,
|
|
Scale: scale,
|
|
})
|
|
}
|
|
|
|
minScale := lods[0].Scale
|
|
maxScale := lods[len(lods)-1].Scale
|
|
|
|
bounds, ok := metadata["bounds"].([]float64)
|
|
if !ok {
|
|
bounds = []float64{-180, -85, 180, 85} // default to world bounds
|
|
}
|
|
extent := geoBoundsToWMExtent(bounds)
|
|
|
|
tileInfo := map[string]interface{}{
|
|
"rows": 256,
|
|
"cols": 256,
|
|
"dpi": dpi,
|
|
"origin": map[string]float64{
|
|
"x": -earthCircumference,
|
|
"y": earthCircumference,
|
|
},
|
|
"spatialReference": webMercatorSR,
|
|
"lods": lods,
|
|
}
|
|
|
|
documentInfo := map[string]string{
|
|
"Title": name,
|
|
"Author": attribution,
|
|
"Comments": "",
|
|
"Subject": "",
|
|
"Category": "",
|
|
"Keywords": tags,
|
|
"Credits": credits,
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"currentVersion": "10.4",
|
|
"id": ts.id,
|
|
"name": name,
|
|
"mapName": name,
|
|
"capabilities": "Map,TilesOnly",
|
|
"description": description,
|
|
"serviceDescription": description,
|
|
"copyrightText": attribution,
|
|
"singleFusedMapCache": true,
|
|
"supportedImageFormatTypes": strings.ToUpper(imgFormat),
|
|
"units": "esriMeters",
|
|
// TODO: enable for vector tiles
|
|
// "layers": []arcGISLayerStub{
|
|
// {
|
|
// ID: 0,
|
|
// Name: name,
|
|
// ParentLayerID: -1,
|
|
// DefaultVisibility: true,
|
|
// SubLayerIDs: nil,
|
|
// MinScale: minScale,
|
|
// MaxScale: maxScale,
|
|
// },
|
|
// },
|
|
"layers": []string{},
|
|
"tables": []string{},
|
|
"spatialReference": webMercatorSR,
|
|
"minScale": minScale,
|
|
"maxScale": maxScale,
|
|
"tileInfo": tileInfo,
|
|
"documentInfo": documentInfo,
|
|
"initialExtent": extent,
|
|
"fullExtent": extent,
|
|
"exportTilesAllowed": false,
|
|
"maxExportTilesCount": 0,
|
|
"resampling": false,
|
|
"supportsDynamicLayers": false,
|
|
}
|
|
|
|
bytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes, nil
|
|
}
|
|
|
|
// arcgisServiceHandler is an http.HandlerFunc that returns standard ArcGIS
|
|
// JSON for a given ArcGIS tile service
|
|
func (ts *Tileset) arcgisServiceHandler(w http.ResponseWriter, r *http.Request) {
|
|
// wait up to 30 seconds to see if tileset is ready and return it if possible
|
|
if ts.isLockedWithTimeout(30 * time.Second) {
|
|
tilesetLockedHandler(w, r)
|
|
return
|
|
}
|
|
|
|
svcJSON, err := ts.arcgisServiceJSON()
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS Service JSON for %v: %v", r.URL.Path, err)
|
|
return
|
|
}
|
|
|
|
err = wrapJSONP(w, r, svcJSON)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS Service JSON to JSONP for %v: %v", r.URL.Path, err)
|
|
}
|
|
}
|
|
|
|
// arcGISLayersJSON returns JSON for the layers in a given ArcGIS tile service
|
|
func (ts *Tileset) arcgisLayersJSON() ([]byte, error) {
|
|
// TODO: enable for vector tiles
|
|
|
|
// metadata, err := ts.db.ReadMetadata()
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
|
|
// name, _ := metadata["name"].(string)
|
|
// description, _ := metadata["description"].(string)
|
|
// attribution, _ := metadata["attribution"].(string)
|
|
|
|
// bounds, ok := metadata["bounds"].([]float32)
|
|
// if !ok {
|
|
// bounds = []float32{-180, -85, 180, 85} // default to world bounds
|
|
// }
|
|
// extent := geoBoundsToWMExtent(bounds)
|
|
|
|
// minZoom, _ := metadata["minzoom"].(int)
|
|
// maxZoom, _ := metadata["maxzoom"].(int)
|
|
// minScale, _ := calcScaleResolution(minZoom, dpi)
|
|
// maxScale, _ := calcScaleResolution(maxZoom, dpi)
|
|
|
|
// // for now, just create a placeholder root layer
|
|
// emptyArray := []interface{}{}
|
|
// emptyLayerArray := []arcGISLayerStub{}
|
|
|
|
// var layers [1]arcGISLayer
|
|
// layers[0] = arcGISLayer{
|
|
// ID: 0,
|
|
// DefaultVisibility: true,
|
|
// ParentLayer: nil,
|
|
// Name: name,
|
|
// Description: description,
|
|
// Extent: extent,
|
|
// MinScale: minScale,
|
|
// MaxScale: maxScale,
|
|
// CopyrightText: attribution,
|
|
// HTMLPopupType: "esriServerHTMLPopupTypeAsHTMLText",
|
|
// Fields: emptyArray,
|
|
// Relationships: emptyArray,
|
|
// SubLayers: emptyLayerArray,
|
|
// CurrentVersion: 10.4,
|
|
// Capabilities: "Map",
|
|
// }
|
|
|
|
out := map[string]interface{}{
|
|
// "layers": layers,
|
|
"layers": []string{},
|
|
"tables": []string{},
|
|
}
|
|
|
|
bytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes, nil
|
|
}
|
|
|
|
// arcgisLayersHandler is an http.HandlerFunc that returns standard ArcGIS
|
|
// Layers JSON for a given ArcGIS tile service
|
|
func (ts *Tileset) arcgisLayersHandler(w http.ResponseWriter, r *http.Request) {
|
|
// wait up to 30 seconds to see if tileset is ready and return it if possible
|
|
if ts.isLockedWithTimeout(30 * time.Second) {
|
|
tilesetLockedHandler(w, r)
|
|
return
|
|
}
|
|
|
|
layersJSON, err := ts.arcgisLayersJSON()
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS layer JSON for %v: %v", r.URL.Path, err)
|
|
return
|
|
}
|
|
|
|
err = wrapJSONP(w, r, layersJSON)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS layers JSON to JSONP for %v: %v", r.URL.Path, err)
|
|
}
|
|
}
|
|
|
|
// arcgisLegendJSON returns minimal ArcGIS legend JSON for a given ArcGIS
|
|
// tile service. Legend elements are not yet supported.
|
|
func (ts *Tileset) arcgisLegendJSON() ([]byte, error) {
|
|
metadata, err := ts.db.ReadMetadata()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name, _ := metadata["name"].(string)
|
|
|
|
// TODO: pull the legend from ArcGIS specific metadata tables
|
|
var elements [0]interface{}
|
|
var layers [1]map[string]interface{}
|
|
|
|
layers[0] = map[string]interface{}{
|
|
"layerId": 0,
|
|
"layerName": name,
|
|
"layerType": "",
|
|
"minScale": 0,
|
|
"maxScale": 0,
|
|
"legend": elements,
|
|
}
|
|
|
|
out := map[string]interface{}{
|
|
"layers": layers,
|
|
}
|
|
|
|
bytes, err := json.Marshal(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes, nil
|
|
}
|
|
|
|
// arcgisLegendHandler is an http.HandlerFunc that returns minimal ArcGIS
|
|
// legend JSON for a given ArcGIS tile service
|
|
func (ts *Tileset) arcgisLegendHandler(w http.ResponseWriter, r *http.Request) {
|
|
// wait up to 30 seconds to see if tileset is ready and return it if possible
|
|
if ts.isLockedWithTimeout(30 * time.Second) {
|
|
tilesetLockedHandler(w, r)
|
|
return
|
|
}
|
|
|
|
legendJSON, err := ts.arcgisLegendJSON()
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS legend JSON for %v: %v", r.URL.Path, err)
|
|
return
|
|
}
|
|
|
|
err = wrapJSONP(w, r, legendJSON)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("Could not render ArcGIS legend JSON to JSONP for %v: %v", r.URL.Path, err)
|
|
}
|
|
}
|
|
|
|
// arcgisTileHandler returns an image tile or blank image for a given
|
|
// tile request within a given ArcGIS tile service
|
|
func (ts *Tileset) arcgisTileHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// wait up to 30 seconds to see if tileset is ready and return it if possible
|
|
if ts.isLockedWithTimeout(30 * time.Second) {
|
|
tilesetLockedHandler(w, r)
|
|
return
|
|
}
|
|
|
|
db := ts.db
|
|
|
|
// split path components to extract tile coordinates x, y and z
|
|
pcs := strings.Split(r.URL.Path[1:], "/")
|
|
// strip off /arcgis/rest/services/ and then
|
|
// we should have at least <id> , "MapServer", "tiles", <z>, <y>, <x>
|
|
l := len(pcs)
|
|
if l < 6 || pcs[5] == "" {
|
|
http.Error(w, "requested path is too short", http.StatusBadRequest)
|
|
return
|
|
}
|
|
z, y, x := pcs[l-3], pcs[l-2], pcs[l-1]
|
|
tc, _, err := tileCoordFromString(z, x, y)
|
|
if err != nil {
|
|
http.Error(w, "invalid tile coordinates", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// flip y to match the spec
|
|
tc.y = (1 << uint64(tc.z)) - 1 - tc.y
|
|
|
|
var data []byte
|
|
err = db.ReadTile(tc.z, tc.x, tc.y, &data)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("cannot fetch tile from DB for z=%d, x=%d, y=%d for %v: %v", tc.z, tc.x, tc.y, r.URL.Path, err)
|
|
return
|
|
}
|
|
|
|
if data == nil || len(data) <= 1 {
|
|
// Return blank PNG for all image types
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, err = w.Write(BlankPNG(ts.tilesize))
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("could not return blank image for %v: %v", r.URL.Path, err)
|
|
}
|
|
} else {
|
|
w.Header().Set("Content-Type", db.GetTileFormat().MimeType())
|
|
_, err = w.Write(data)
|
|
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
ts.svc.logError("could not write tile data to response for %v: %v", r.URL.Path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func geoBoundsToWMExtent(bounds []float64) arcGISExtent {
|
|
xmin, ymin := geoToMercator(float64(bounds[0]), float64(bounds[1]))
|
|
xmax, ymax := geoToMercator(float64(bounds[2]), float64(bounds[3]))
|
|
return arcGISExtent{
|
|
Xmin: xmin,
|
|
Ymin: ymin,
|
|
Xmax: xmax,
|
|
Ymax: ymax,
|
|
SpatialReference: webMercatorSR,
|
|
}
|
|
}
|
|
|
|
// Cast interface to a string if not nil, otherwise empty string
|
|
func toString(s interface{}) string {
|
|
if s != nil {
|
|
return s.(string)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Convert a latitude and longitude to mercator coordinates, bounded to world domain.
|
|
func geoToMercator(longitude, latitude float64) (float64, float64) {
|
|
// bound to world coordinates
|
|
if latitude > 85 {
|
|
latitude = 85
|
|
} else if latitude < -85 {
|
|
latitude = -85
|
|
}
|
|
|
|
x := longitude * earthCircumference / 180
|
|
y := math.Log(math.Tan((90+latitude)*math.Pi/360)) / (math.Pi / 180) * (earthCircumference / 180)
|
|
|
|
return x, y
|
|
}
|