{
  "openapi": "3.1.1",
  "info": {
    "title": "Cartograph GPS Art Route Designer (shape → roads → GPX) API",
    "version": "1.0.0",
    "summary": "Snap a heart, star or other shape to the streets of your OSM extract and get a GPX route.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://cartographrouteart.usefulweb.app"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Snap a heart, star or other shape to the streets of your OSM extract and get a GPX route.",
        "description": "Places a shape (heart, star, circle, square, triangle, arrow or zigzag) at a point, size and rotation, samples it into waypoints, snaps them to the walkable or rideable roads of your own OpenStreetMap extract and joins them with shortest paths that stay close to the shape. Returns the route as GPX with its length and how closely it follows the drawing. Routing runs on the extract itself — no routing service is used. It is the GPS-art engine of the Cartograph app.\n\nMethodology:\n- The shape is placed with geodesics: every outline point keeps its direction and distance from the centre (Karney’s direct problem on WGS84), so the size is true on the ground.\n- The road graph is built from the extract: highway ways usable on foot or by bike (motorways and trunk roads, access=no/private and explicit foot=no/bicycle=no excluded; bikes avoid steps and footways unless allowed and follow one-way streets). Junctions are shared coordinates; edge lengths are great-circle distances.\n- The outline is sampled into waypoints; each is snapped to the nearest graph node within twice the spacing (dead ends avoided where possible). Consecutive waypoints are joined by A* shortest paths whose cost adds “stay-on-shape weight × distance from the shape segment ÷ spacing” per metre, so routes prefer streets that hug the drawing. Out-and-back spurs are removed.\n- Quality: mean distance of the route from the shape, the largest gap in either direction (Hausdorff-style) and the share of the shape that has the route within 25 m — measured every 10 m in a local equirectangular frame.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "cartograph_route_art",
        "x-owner-app": "cartograph",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CartographRouteArtInput"
              },
              "examples": {
                "example1": {
                  "summary": "Heart run in Vaduz (built-in sample)",
                  "value": {
                    "shape": "heart",
                    "center": "47.145, 9.517",
                    "widthM": 1000,
                    "profile": "foot"
                  }
                },
                "example2": {
                  "summary": "Star ride by bike",
                  "value": {
                    "shape": "star",
                    "center": "47.142, 9.52",
                    "widthM": 800,
                    "profile": "bike"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CartographRouteArtResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "CartographRouteArtInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "file": {
            "title": "OpenStreetMap extract (.osm, .osm.pbf or GeoJSON)",
            "description": "Up to 25 MB. Leave empty to use the built-in Vaduz sample. Download extracts from your own OpenStreetMap export, Geofabrik or BBBike; Cartograph never fetches map data itself. Upload as {filename, contentType?, contentBase64}. Max 24414 KiB. Accepts .osm,.pbf,.geojson,.json,application/xml,text/xml,application/geo+json,application/json,application/octet-stream.",
            "type": "object",
            "additionalProperties": false,
            "required": [
              "filename",
              "contentBase64"
            ],
            "properties": {
              "filename": {
                "type": "string"
              },
              "contentType": {
                "type": "string"
              },
              "contentBase64": {
                "type": "string"
              }
            }
          },
          "shape": {
            "title": "Shape",
            "type": "string",
            "enum": [
              "heart",
              "star",
              "circle",
              "square",
              "triangle",
              "arrow",
              "zigzag"
            ],
            "default": "heart"
          },
          "center": {
            "title": "Shape centre",
            "description": "Empty: the middle of the extract.",
            "type": "string",
            "default": ""
          },
          "widthM": {
            "title": "Shape width",
            "type": "number",
            "minimum": 100,
            "maximum": 20000,
            "default": 1000,
            "x-unit": "m"
          },
          "rotationDeg": {
            "title": "Rotation (clockwise)",
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "default": 0,
            "x-unit": "°"
          },
          "profile": {
            "title": "Travel mode",
            "type": "string",
            "enum": [
              "foot",
              "bike"
            ],
            "default": "foot"
          },
          "spacingM": {
            "title": "Waypoint spacing",
            "description": "0 = automatic (shape length ÷ 60, at least 30 m).",
            "type": "number",
            "minimum": 0,
            "maximum": 1000,
            "default": 0,
            "x-unit": "m"
          },
          "guidance": {
            "title": "Stay-on-shape weight",
            "description": "0 = plain shortest paths between waypoints; higher values prefer streets close to the drawn line.",
            "type": "number",
            "minimum": 0,
            "maximum": 10,
            "default": 2
          }
        },
        "required": []
      },
      "CartographRouteArtResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "shape",
              "profile",
              "center",
              "widthM",
              "rotationDeg",
              "routeLengthM",
              "shapeLengthM",
              "waypoints",
              "skipped",
              "meanDeviationM",
              "maxDeviationM",
              "coverage",
              "sample",
              "attribution",
              "gpx"
            ],
            "properties": {
              "shape": {
                "type": "string"
              },
              "profile": {
                "type": "string",
                "enum": [
                  "foot",
                  "bike"
                ]
              },
              "center": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "lat",
                  "lon"
                ],
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lon": {
                    "type": "number"
                  }
                }
              },
              "widthM": {
                "type": "number"
              },
              "rotationDeg": {
                "type": "number"
              },
              "routeLengthM": {
                "type": "number"
              },
              "shapeLengthM": {
                "type": "number"
              },
              "waypoints": {
                "type": "integer"
              },
              "skipped": {
                "type": "integer"
              },
              "meanDeviationM": {
                "type": "number"
              },
              "maxDeviationM": {
                "type": "number"
              },
              "coverage": {
                "type": "number",
                "description": "Share of the shape within 25 m of the route (0–1)."
              },
              "sample": {
                "type": "boolean"
              },
              "attribution": {
                "type": "string"
              },
              "gpx": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "artifactId",
                  "filename",
                  "mime",
                  "bytes",
                  "sha256",
                  "path"
                ],
                "properties": {
                  "artifactId": {
                    "type": "string"
                  },
                  "filename": {
                    "type": "string"
                  },
                  "mime": {
                    "type": "string"
                  },
                  "bytes": {
                    "type": "integer"
                  },
                  "sha256": {
                    "type": "string"
                  },
                  "path": {
                    "type": "string",
                    "description": "Download path on the same site (valid for about 2 hours)."
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}