{
  "version": 3,
  "sources": ["route_markers_core.js"],
  "sourcesContent": ["/*\n * static/js/route_markers_core.js \u2014 the start and finish markers on a\n * saved route.\n *\n * A route line says where the track goes and nothing about which way round\n * it goes. These two symbols answer that: a filled dot where it starts, a\n * checkered flag where it ends. They are the Strava/Komoot convention and\n * they read the same way on a paper map \u2014 the point of a flag is that it\n * needs no legend.\n *\n * This module owns the PIXELS and the GEOMETRY, both of which are\n * arithmetic, and neither of which `map.js` could have tested. It owns no\n * colour: `map.js` resolves the design tokens (a CSS `@theme` value, so\n * possibly `oklch()`, which only a canvas fill parses reliably) and passes\n * the channels in \u2014 exactly the split `hatch_core.js` uses, and for the\n * same reason.\n *\n * ## Why the icons are raw pixels rather than canvas paths\n *\n * The favourite star and the community-report flag are Path2D fills\n * registered `sdf: true`, which is what lets `icon-color` retint them\n * per-layer. A CHECKERED flag cannot be an SDF: SDF discards colour and\n * keeps only the alpha mask, so a two-tone glyph collapses to a\n * silhouette \u2014 a black rectangle on a stick, which is not a checkered flag\n * and does not read as a finish line. It has to be a full-colour image.\n *\n * The other full-colour icons on this map (the weather symbol set)\n * are `<img>`-decoded SVGs, which is asynchronous and needs a file. A\n * checkerboard and a disc are a few lines of arithmetic, so these are\n * built as a MapLibre `StyleImage` \u2014 `{width, height, data}` over a\n * `Uint8ClampedArray` \u2014 the same shape `hatch_core.js` hands to\n * `map.addImage`. Synchronous, no asset to ship, and the invariant worth\n * testing (that a checker actually alternates) is a pure function of x\n * and y.\n *\n * ## Exports (frozen `self.pwaRouteMarkersCore`)\n *\n *   SIZE / PIXEL_RATIO\n *   endpointsGeojson(routesCollection)\n *   startDotPixels(r, g, b)\n *   finishFlagPixels(r, g, b)\n *   fallLineArrowPixels()\n */\n\n// @ts-check\n\n(function () {\n  'use strict';\n\n  /**\n   * Icon edge in DEVICE pixels. Registered at `pixelRatio: 2`, so both\n   * symbols occupy 20 CSS pixels \u2014 near the ~18px the community-report\n   * flag renders at, so a route's ends sit in the same size class as every\n   * other marker on the map rather than shouting over them.\n   */\n  const SIZE = 40;\n\n  /** The ratio `map.addImage` must be given to match `SIZE`. */\n  const PIXEL_RATIO = 2;\n\n  /** Radius of the start disc, in device pixels. */\n  const DOT_RADIUS = 11;\n\n  /** White ring around the start disc, in device pixels. */\n  const DOT_RING = 3;\n\n  /** The flagpole's left edge, width, top and bottom, in device pixels. */\n  const POLE_X = 8;\n  const POLE_W = 3;\n  const POLE_TOP = 5;\n  const POLE_BOTTOM = 35;\n\n  /**\n   * The fall-line arrow, in device pixels: tip, the head's base, the\n   * tail, and the two half-widths.\n   *\n   * It is drawn POINTING UP \u2014 towards the top of the buffer, which is\n   * north on an unrotated map \u2014 because `icon-rotate` turns it clockwise\n   * from there and the bearing it is given is a compass bearing. Drawing\n   * it any other way round would make every arrow wrong by a constant,\n   * which is the class of bug that looks plausible on screen.\n   */\n  const ARROW_TIP_Y = 6;\n  const ARROW_HEAD_Y = 21;\n  const ARROW_TAIL_Y = 34;\n  const ARROW_HEAD_HALF_W = 8;\n  const ARROW_SHAFT_HALF_W = 2.5;\n\n  /** The cloth's box, in device pixels. Four columns by three rows. */\n  const CLOTH_X = POLE_X + POLE_W;\n  const CLOTH_Y = 5;\n  const CLOTH_COLS = 4;\n  const CLOTH_ROWS = 3;\n  const CELL = 6;\n\n  /**\n   * Whether a coordinate pair is two finite numbers.\n   *\n   * @param {*} position A candidate GeoJSON position.\n   * @returns {boolean}\n   */\n  function isPosition(position) {\n    return (\n      Array.isArray(position)\n      && typeof position[0] === 'number'\n      && typeof position[1] === 'number'\n      && isFinite(position[0])\n      && isFinite(position[1])\n    );\n  }\n\n  /**\n   * Derive the start/finish marker points from the routes layer's data.\n   *\n   * The markers are their own point source rather than anything computed\n   * from the line layer, because MapLibre cannot symbolise \"the ends of a\n   * LineString\" \u2014 `symbol-placement: 'line'` repeats a symbol ALONG a\n   * line, which is a different thing entirely.\n   *\n   * A CLOSED TRACK GETS ONE MARKER, NOT TWO. An out-and-back finishes at\n   * the car it started from, and that is most ski tours: emitting both\n   * would stack a flag exactly on a dot, hiding one of them and leaving\n   * the reader unable to tell a loop from a one-way route whose other end\n   * is off screen. So when the two ends are the same position the flag is\n   * emitted alone \u2014 it is the marker that was asked for, and \"the track\n   * both starts and ends here\" is what one marker at one place says.\n   *\n   * Equality is exact rather than within a tolerance. These coordinates\n   * are the stored track's own endpoints, not a computation over them, so\n   * a genuine loop closes on the identical pair of numbers; an\n   * epsilon would additionally swallow a short there-and-back that ends a\n   * few metres from its start, which has two ends and should show two.\n   *\n   * @param {?{features?: Array<any>}} collection The routes\n   *   FeatureCollection, as served by `routes:geojson` (LineStrings;\n   *   positions may carry a third elevation ordinate, dropped here).\n   * @returns {{type: string, features: Array<object>}} A point\n   *   FeatureCollection. Each feature carries `role` (`'start'` or\n   *   `'end'`), plus the route's `uuid` and `name` so a marker can be\n   *   traced back to its line.\n   */\n  function endpointsGeojson(collection) {\n    const features = [];\n    const routes = (collection && collection.features) || [];\n\n    for (let i = 0; i < routes.length; i += 1) {\n      const route = routes[i] || {};\n      const geometry = route.geometry || {};\n      const coordinates = geometry.coordinates;\n      // Under two positions there is no line, so no direction to mark.\n      if (!Array.isArray(coordinates) || coordinates.length < 2) continue;\n\n      const first = coordinates[0];\n      const last = coordinates[coordinates.length - 1];\n      if (!isPosition(first) || !isPosition(last)) continue;\n\n      const props = route.properties || {};\n      /**\n       * @param {number[]} position A `[lon, lat]` (or `[lon, lat, ele]`).\n       * @param {string} role `'start'` or `'end'`.\n       * @returns {object} One point Feature.\n       */\n      const marker = (position, role) => ({\n        type: 'Feature',\n        // Longitude and latitude only: a symbol layer has no use for the\n        // elevation the line's own positions carry.\n        geometry: { type: 'Point', coordinates: [position[0], position[1]] },\n        properties: { role: role, uuid: props.uuid, name: props.name },\n      });\n\n      const closed = first[0] === last[0] && first[1] === last[1];\n      if (!closed) features.push(marker(first, 'start'));\n      features.push(marker(last, 'end'));\n    }\n\n    return { type: 'FeatureCollection', features: features };\n  }\n\n  /**\n   * Allocate a transparent RGBA buffer for one icon.\n   *\n   * @returns {Uint8ClampedArray} `SIZE * SIZE * 4` zeroed bytes.\n   */\n  function blank() {\n    return new Uint8ClampedArray(SIZE * SIZE * 4);\n  }\n\n  /**\n   * Paint one pixel.\n   *\n   * @param {Uint8ClampedArray} data The buffer.\n   * @param {number} x Column.\n   * @param {number} y Row.\n   * @param {Array<number>} rgba Four channels, 0-255.\n   */\n  function put(data, x, y, rgba) {\n    if (x < 0 || y < 0 || x >= SIZE || y >= SIZE) return;\n    const offset = (y * SIZE + x) * 4;\n    data[offset] = rgba[0];\n    data[offset + 1] = rgba[1];\n    data[offset + 2] = rgba[2];\n    data[offset + 3] = rgba[3];\n  }\n\n  /**\n   * Fill an axis-aligned rectangle.\n   *\n   * @param {Uint8ClampedArray} data The buffer.\n   * @param {number} x Left edge.\n   * @param {number} y Top edge.\n   * @param {number} width Width in pixels.\n   * @param {number} height Height in pixels.\n   * @param {Array<number>} rgba Four channels, 0-255.\n   */\n  function rect(data, x, y, width, height, rgba) {\n    for (let row = y; row < y + height; row += 1) {\n      for (let column = x; column < x + width; column += 1) {\n        put(data, column, row, rgba);\n      }\n    }\n  }\n\n  /**\n   * The start marker: a filled disc in the route's own colour, ringed white.\n   *\n   * The ring is not decoration. The disc is the same fuchsia as the line it\n   * sits on, so without a ring the marker dissolves into the track at the\n   * exact place the reader is looking; white separates the two and also\n   * carries the disc over a dark basemap.\n   *\n   * @param {number} r Red channel of the route colour, 0-255.\n   * @param {number} g Green channel.\n   * @param {number} b Blue channel.\n   * @returns {{width: number, height: number, data: Uint8ClampedArray}} A\n   *   MapLibre StyleImage.\n   */\n  function startDotPixels(r, g, b) {\n    const data = blank();\n    const centre = SIZE / 2 - 0.5;\n    const fill = [r, g, b, 255];\n    const ring = [255, 255, 255, 255];\n    const outer = DOT_RADIUS + DOT_RING;\n\n    for (let y = 0; y < SIZE; y += 1) {\n      for (let x = 0; x < SIZE; x += 1) {\n        const dx = x - centre;\n        const dy = y - centre;\n        const distance = Math.sqrt(dx * dx + dy * dy);\n        if (distance <= DOT_RADIUS) put(data, x, y, fill);\n        else if (distance <= outer) put(data, x, y, ring);\n      }\n    }\n    return { width: SIZE, height: SIZE, data: data };\n  }\n\n  /**\n   * The finish marker: a checkered flag on a pole.\n   *\n   * The dark cells take the caller's ink (the route line's own casing\n   * colour, so the flag belongs to the track it ends) and the light cells\n   * are white \u2014 a checkered flag is black-and-white by definition, and a\n   * retinted \"checker\" in two brand colours stops reading as a finish\n   * line, which is the entire reason to use one.\n   *\n   * The cloth carries a one-pixel border in the same ink. Without it the\n   * white cells on the cloth's edge bleed into a pale basemap and the flag\n   * loses its rectangle.\n   *\n   * @param {number} r Red channel of the ink, 0-255.\n   * @param {number} g Green channel.\n   * @param {number} b Blue channel.\n   * @returns {{width: number, height: number, data: Uint8ClampedArray}} A\n   *   MapLibre StyleImage.\n   */\n  function finishFlagPixels(r, g, b) {\n    const data = blank();\n    const ink = [r, g, b, 255];\n    const light = [255, 255, 255, 255];\n\n    rect(data, POLE_X, POLE_TOP, POLE_W, POLE_BOTTOM - POLE_TOP, ink);\n\n    for (let row = 0; row < CLOTH_ROWS; row += 1) {\n      for (let column = 0; column < CLOTH_COLS; column += 1) {\n        // The parity that makes it a checker: neighbours in either\n        // direction always differ.\n        const dark = (row + column) % 2 === 0;\n        rect(\n          data,\n          CLOTH_X + column * CELL,\n          CLOTH_Y + row * CELL,\n          CELL,\n          CELL,\n          dark ? ink : light,\n        );\n      }\n    }\n\n    const clothWidth = CLOTH_COLS * CELL;\n    const clothHeight = CLOTH_ROWS * CELL;\n    rect(data, CLOTH_X, CLOTH_Y - 1, clothWidth, 1, ink);\n    rect(data, CLOTH_X, CLOTH_Y + clothHeight, clothWidth, 1, ink);\n    rect(data, CLOTH_X + clothWidth, CLOTH_Y - 1, 1, clothHeight + 2, ink);\n\n    return { width: SIZE, height: SIZE, data: data };\n  }\n\n  /**\n   * The fall-line mark: an arrow pointing the way the ground falls.\n   *\n   * A head and a short shaft rather than a bare triangle. At 20 CSS\n   * pixels a triangle alone reads as a wedge of colour and its direction\n   * has to be worked out; the shaft is what makes it an arrow at a\n   * glance, which matters because the reader is being asked to compare\n   * its direction with the direction of the TRACK it sits on.\n   *\n   * **NO COLOUR OF ITS OWN.** Every pixel is opaque white and the layer\n   * registers it `sdf: true`, so `icon-color` paints it and the ink stays\n   * one paint property away from the layer.\n   * That also means it cannot be two-tone \u2014 an SDF keeps only the alpha\n   * mask \u2014 which is the trap `finishFlagPixels` documents.\n   *\n   * Antialiased across the boundary: at these sizes the fractional\n   * coverage is what stops a sloped edge\n   * reading as a staircase, and an SDF's alpha channel carries it\n   * exactly.\n   *\n   * @returns {{width: number, height: number, data: Uint8ClampedArray}} A\n   *   MapLibre StyleImage.\n   */\n  function fallLineArrowPixels() {\n    const data = blank();\n    const centre = SIZE / 2 - 0.5;\n\n    for (let y = ARROW_TIP_Y; y <= ARROW_TAIL_Y; y += 1) {\n      // The head is a triangle widening from the tip to its base, and\n      // the shaft below it is a constant width. One half-width per row\n      // is all either shape is.\n      const halfWidth = y <= ARROW_HEAD_Y\n        ? (ARROW_HEAD_HALF_W * (y - ARROW_TIP_Y)) / (ARROW_HEAD_Y - ARROW_TIP_Y)\n        : ARROW_SHAFT_HALF_W;\n      for (let x = 0; x < SIZE; x += 1) {\n        // Coverage falls off over one pixel at the edge, measured from\n        // the half-width \u2014 which is what an antialiased edge is.\n        const alpha = Math.min(1, Math.max(0, halfWidth - Math.abs(x - centre) + 0.5));\n        if (alpha > 0) put(data, x, y, [255, 255, 255, Math.round(alpha * 255)]);\n      }\n    }\n    return { width: SIZE, height: SIZE, data: data };\n  }\n\n  self.pwaRouteMarkersCore = Object.freeze({\n    SIZE: SIZE,\n    PIXEL_RATIO: PIXEL_RATIO,\n    endpointsGeojson: endpointsGeojson,\n    startDotPixels: startDotPixels,\n    finishFlagPixels: finishFlagPixels,\n    fallLineArrowPixels: fallLineArrowPixels,\n  });\n})();\n"],
  "mappings": "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",
  "names": ["isPosition", "position", "endpointsGeojson", "collection", "features", "routes", "i", "route", "coordinates", "first", "last", "props", "marker", "role", "blank", "put", "data", "x", "y", "rgba", "offset", "rect", "width", "height", "row", "column", "startDotPixels", "r", "g", "b", "centre", "fill", "ring", "outer", "dx", "dy", "distance", "finishFlagPixels", "ink", "light", "dark", "clothWidth", "clothHeight", "fallLineArrowPixels", "halfWidth", "alpha"]
}
