mark_tile.js

import { tile as d3tile } from "d3-tile";
import { geoMercator } from "d3-geo";
import { create } from "../container/create";
import { render } from "../container/render";
import { unique } from "../helpers/utils";

/**
 * @function tile
 * @description The `tile` function allows to display raster tiles. To use this mark, you must use the projection d3.geoMercator() (or directly "mercator"). The function adds a layer to the SVG container and returns the layer identifier. If the container is not defined, then the layer is displayed directly.
 * @see {@link https://observablehq.com/@neocartocnrs/tile-mark}
 *
 * @property {string} [id] - id of the layer
 * @property {number} [tileSize = 512] - tile size
 * @property {number} [zoomDelta = 1] - zoom offset
 * @property {number} [opacity = 1] - tile opacity
 * @property {function|string} [url = "openstreetmap"] - function like <code>(x, y, z) => \`https://something/\${z}/\${x}/\${y}.png\`</code>. You can also enter the following strings directly: "openstreetmap", "opentopomap", "worldterrain", "worldimagery", "worldStreet", "worldphysical", "shadedrelief", "stamenterrain", "cartodbvoyager", "stamentoner","stamentonerbackground","stamentonerlite","stamenwatercolor","hillshade","worldocean","natgeo" or "worldterrain".
 * @property {string} [clipPath] - clip-path. e.g. "url(#myclipid)"
 * @property {*} [svg_*]  - *parameters of the svg container created if the layer is not called inside a container (e.g svg_width)*
 * @example
 * // There are several ways to use this function
 * geoviz.tile() // no container
 * geoviz.tile(svg, {url: "worldterrain"}) // where svg is the container
 * svg.tile({url: "worldterrain"}) // where svg is the container
 * svg.plot({type: "tile", url: "worldterrain"}) // where svg is the container
 */

export function tile(arg1, arg2) {
  // Test if new container
  let newcontainer =
    (arguments.length <= 1 || arguments[1] == undefined) &&
    !arguments[0]?._groups
      ? true
      : false;
  arg1 = newcontainer && arg1 == undefined ? {} : arg1;
  arg2 = arg2 == undefined ? {} : arg2;
  // let svg = newcontainer
  //   ? create({ projection: geoMercator(), zoomable: true })
  //   : arg1;

  // Arguments
  const options = {
    mark: "tile",
    id: unique(),
    tileSize: 512,
    zoomDelta: 1,
    increasetilesize: 1,
    opacity: 1,
    clipPath: undefined,
    url: (x, y, z) => `https://tile.openstreetmap.org/${z}/${x}/${y}.png`,
  };

  let opts = { ...options, ...(newcontainer ? arg1 : arg2) };
  opts.url = geturl(opts.url);

  // New container
  let svgopts = { zoomable: true, projection: geoMercator() };
  Object.keys(opts)
    .filter((str) => str.slice(0, 4) == "svg_")
    .forEach((d) => {
      Object.assign(svgopts, {
        [d.slice(0, 4) == "svg_" ? d.slice(4) : d]: opts[d],
      });
      delete opts[d];
    });
  let svg = newcontainer ? create(svgopts) : arg1;

  // Warning
  if (svg.initproj == "none" && svg.warning) {
    svg.warning_message.push(
      `You must use projection: "mercator" in the svg container to display tile marks`
    );
  }

  // init layer
  let layer = svg.selectAll(`#${opts.id}`).empty()
    ? svg.append("g").attr("id", opts.id)
    : svg.select(`#${opts.id}`);
  layer.selectAll("*").remove();

  // zoomable layer
  if (svg.zoomable && !svg.parent) {
    if (!svg.zoomablelayers.map((d) => d.id).includes(opts.id)) {
      svg.zoomablelayers.push(opts);
    } else {
      let i = svg.zoomablelayers.indexOf(
        svg.zoomablelayers.find((d) => d.id == opts.id)
      );
      svg.zoomablelayers[i] = opts;
    }
  }

  let tile = d3tile()
    .size([svg.width, svg.height])
    .scale(svg.projection.scale() * 2 * Math.PI)
    .translate(svg.projection([0, 0]))
    .tileSize(opts.tileSize)
    .zoomDelta(opts.zoomDelta);

  layer
    .selectAll("image")
    .data(tile())
    .join("image")
    .attr("xlink:href", (d) => opts.url(...d))
    .attr("x", ([x]) => (x + tile().translate[0]) * tile().scale)
    .attr("y", ([, y]) => (y + tile().translate[1]) * tile().scale)
    .attr("width", tile().scale + opts.increasetilesize + "px")
    .attr("height", tile().scale + opts.increasetilesize + "px")
    .attr("opacity", opts.opacity)
    .attr("clip-path", opts.clipPath);

  // Output
  if (newcontainer) {
    return render(svg);
  } else {
    return `#${opts.id}`;
  }
}

function geturl(x) {
  let url = (x, y, z) => `https://tile.openstreetmap.org/${z}/${x}/${y}.png`;
  switch (typeof x) {
    case "function":
      url = x;
      break;
    case "string":
      url =
        providers.find((d) => d.name == x)?.url ||
        ((x, y, z) => `https://tile.openstreetmap.org/${z}/${x}/${y}.png`);
      break;
  }
  return url;
}

const providers = [
  {
    name: "openstreetmap",
    provider: "OpenStreetMap contributors",
    url: (x, y, z) => `https://tile.openstreetmap.org/${z}/${x}/${y}.png`,
  },
  {
    name: "opentopomap",
    provider: "OpenStreetMap contributors",
    url: (x, y, z) => `https://tile.opentopomap.org/${z}/${x}/${y}.png`,
  },
  {
    name: "worldterrain",
    provider: "USGS, Esri, TANA, DeLorme, and NPS",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/${z}/${y}/${x}.png`,
  },
  {
    name: "worldimagery",
    provider:
      "Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${z}/${y}/${x}.png`,
  },
  {
    name: "worldStreet",
    provider:
      "Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/${z}/${y}/${x}.png`,
  },
  {
    name: "worldphysical",
    provider: "Esri, US National Park Service",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/${z}/${y}/${x}`,
  },
  {
    name: "shadedrelief",
    provider: "ESRI",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/${z}/${y}/${x}.png`,
  },

  {
    name: "stamenterrain",
    provider: "stadiamaps",
    url: (x, y, z) =>
      `https://tiles.stadiamaps.com/tiles/stamen_terrain/${z}/${x}/${y}${
        devicePixelRatio > 1 ? "@2x" : ""
      }.png`,
  },

  {
    name: "cartodbvoyager",
    provider: "CartoDB",
    url: (x, y, z) =>
      `https://${
        "abc"[Math.abs(x + y) % 3]
      }.basemaps.cartocdn.com/rastertiles/voyager/${z}/${x}/${y}${
        devicePixelRatio > 1 ? "@2x" : ""
      }.png`,
  },

  {
    name: "stamentoner",
    provider: "stadiamaps",
    url: (x, y, z) =>
      `https://tiles.stadiamaps.com/tiles/stamen_toner/${z}/${x}/${y}${
        devicePixelRatio > 1 ? "@2x" : ""
      }.png`,
  },
  {
    name: "stamentonerbackground",
    provider: "stadiamaps",
    url: (x, y, z) =>
      `https://tiles.stadiamaps.com/tiles/stamen_toner_background/${z}/${x}/${y}${
        devicePixelRatio > 1 ? "@2x" : ""
      }.png`,
  },
  {
    name: "stamentonerlite",
    provider: "stadiamaps",
    url: (x, y, z) =>
      `https://tiles.stadiamaps.com/tiles/stamen_toner_lite/${z}/${x}/${y}${
        devicePixelRatio > 1 ? "@2x" : ""
      }.png`,
  },
  {
    name: "stamenwatercolor",
    provider: "stadiamaps",
    url: (x, y, z) =>
      `https://tiles.stadiamaps.com/tiles/stamen_watercolor/${z}/${x}/${y}.jpg`,
  },
  {
    name: "hillshade",
    provider: "ESRI",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/Elevation/World_Hillshade/MapServer/tile/${z}/${y}/${x}.png`,
  },

  {
    name: "worldocean",
    provider: "ESRI",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/Ocean/World_Ocean_Base/MapServer/tile/${z}/${y}/${x}`,
  },
  {
    name: "natgeo",
    provider: "ESRI",
    url: (x, y, z) =>
      `https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/${z}/${y}/${x}`,
  },

  {
    name: "worldterrain",
    provider: "ESRI",
    url: (x, y, z) =>
      `  https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/${z}/${y}/${x}`,
  },
];