Source: src/svgMap.js

/** @module svgMap */

import * as d3 from 'd3'
import { createTrans, namedTransOpts, getTweenTransOpts } from './svgCoords.js'
import { constants } from './constants.js'
import { optsDialog, showOptsDialog } from './svgOptsDialog.js'
import { dataAccessors } from './dataAccess.js'
import { showImage, setImagePriorities, transformImages } from './svgImages.js'
import { drawDots, removeDots } from './svgDots.js'
import { svgLegend } from './svgLegend.js'
import { saveMapImage, downloadCurrentData } from './download.js'

/** 
 * @param {Object} opts - Initialisation options.
 * @param {string} opts.selector - The CSS selector of the element which will be the parent of the SVG.
 * @param {string} opts.mapid - The id for the static map to be created.
 * @param {string} opts.proj - The projection of the map, should be 'gb', 'ir' or 'ci'. It should 
 * reflect the projection of boundary and grid data displayed on the map. It is used to generate the 'dots'
 * in the correct location.
 * @param {string} opts.captionId - The id of a DOM element into which feature-specific HTML will be displayed
 * as the mouse moves over a dot on the map. The HTML markup must be stored in an attribute called 'caption'
 * in the input data.boundaryFill
 * @param {function} opts.onclick - A function that will be called if user clicks on a map
 * element. The function will be passed these attributes, in this order, if they exist on the
 * element: gr, id, caption. (Default - null.)
 * @param {number} opts.height - The desired height of the SVG.
 * @param {boolean} opts.expand - Indicates whether or not the map will expand to fill parent element.
 * @param {legendOpts} opts.legendOpts - Sets options for a map legend.
 * @param {module:svgCoords~transOptsSel} opts.transOptsSel - Sets a collection of map transformation options.
 * @param {string} opts.transOptsKey - Sets the key of the selected map transformation options. Must be
 * present in as a key in the opts.transOptsSel object.
 * @param {boolean} opts.transOptsControl - Indicates whether or not a control should be shown in the
 * bottom-right of the map that can be used display a dialog to change the transformation options.
 * @param {Object} opts.mapTypesSel - Sets an object whose properties are data access functions. The property
 * names are the 'keys' which should be human readable descriptiosn of the map types.
 * @param {string} opts.mapTypesKey - Sets the key of the selected data accessor function (map type).
 * @param {boolean} opts.mapTypesControl - Indicates whether or not a control should be shown in the
 * bottom-right of the map that can be used display a dialog to change the data accessor (map type) options.
 * @param {string} opts.boundaryGjson - The URL of a boundary geoJson file to display.
 * @param {string} opts.boundaryColour - Specifies the line colour of the boundary geoJson.
 * @param {string} opts.boundaryFill - Specifies the fill colour of the boundary geoJson.
 * @param {string} opts.boundaryShadowColour - Specifies the colour of a 'glowing' border.
 * @param {string} opts.boundaryShadowWidth - Species the width of the glowing border in pixels.
 * @param {string | number} opts.boundaryLineWidth - Specifies the width of the line to use for boundary geoJson lines. Can use any valid units for SVG stroke-width. (Default - 1.)
 * @param {string} opts.seaFill - Specifies the fill colour of the area outside the boundary geoJson.
 * @param {string} opts.insetColour - Specifies the line colour of map inset boxes.
 * @param {string} opts.gridGjson - The URL of a grid geoJson file to display.
 * @param {string} opts.gridLineColour - Specifies the line colour of grid line geoJson.
 * @param {string} opts.gridLineStyle - Specifies the line style of the grid line geoJson. Can be solid, dashed or none. (Default solid.)
 * @param {string | number} opts.gridLineWidth - Specifies the width of the line to use for grid lines geoJson. Can use any valid units for SVG stroke-width. (Default - 1.)
 * @param {string} opts.vcGjson - The URL of a vice county geoJson file to display.
 * @param {string} opts.vcColour - Specifies the line colour of the vice county geoJson.
 * @param {string} opts.vcLineStyle - Specifies the line style of the vice county line geoJson. Can none or something else (which gives solid line). (Default none.)
 * @param {string | number} opts.vcLineWidth - Specifies the width of the line to use for vice counties geoJson. Can use any valid units for SVG stroke-width. (Default - 1.)
 * @param {string} opts.countryGjson - The URL of a country geoJson file to display.
 * @param {string} opts.countryColour - Specifies the line colour of the country geoJson.
 * @param {string} opts.countryLineStyle - Specifies the line style of the country geoJson. Can none or something else (which gives solid line). (Default none.)
 * @param {string | number} opts.countryLineWidth - Specifies the width of the line to use for country lines geoJson. Can use any valid units for SVG stroke-width. (Default - 1.)
 * @param {string} opts.legendFont - Specifies the font to use for the legend. (Default - 'sans-serif'.)
 * @param {string} opts.legendFontSize - Specifies the font size to use for the legend. (Default - 14px.)
 * @param {string} opts.legendInteractivity - Indicates the behaviour of legend interactivity, can be 'mousemove', 'mouseclick' or 'none'. (Default - 'none'.)
 * @param {string} opts.highlightClass - For legend interactivity, specify a unique name to use for the highlight class. (Default - ''.)
 * @param {string} opts.highlightStyle - For legend interactivity, specify a string of css property and value pairs for highlight style. Separate pairs with semi-colon and property from value with colon. (Default - ''.)
 * @param {string} opts.lowlightStyle - For legend interactivity, specify a string of css property and value pairs for lowlight style. Separate pairs with semi-colon and property from value with colon. (Default - ''.)
 * @param {function} opts.callbackOptions - Specifies a callback function to be executed if user options dialog used.
 * @returns {module:svgMap~api} api - Returns an API for the map.
 */
export function svgMap({
  // Default options in here
  selector = 'body',
  mapid = 'svgMap',
  proj = 'gb',
  captionId = '',
  onclick = null,
  height = 500,
  expand = false,
  legendOpts = {display: false},
  legendFontSize = '14px',
  legendFont = 'sans-serif',
  transOptsKey = 'BI1',
  transOptsSel = namedTransOpts,
  transOptsControl = true,
  mapTypesKey = 'Standard hectad',
  mapTypesSel = dataAccessors,
  mapTypesControl = false,
  boundaryGjson = `${constants.thisCdn}/assets/GB-I-CI-27700-reduced.geojson`,
  gridGjson = `${constants.thisCdn}/assets/GB-I-grid-27700-reduced.geojson`,
  vcGjson = `${constants.thisCdn}/assets/GB-I-vcs-27700-reduced.geojson`,
  countryGjson = `${constants.thisCdn}/assets/GB-I-countries-27700-reduced.geojson`,
  gridLineColour = '#7C7CD3',
  gridLineStyle = 'solid',
  gridLineWidth = 1,
  vcLineStyle = 'none',
  vcColour = '#7C7CD3',
  vcLineWidth = 1,
  countryLineStyle = 'none',
  countryColour = '#7C7CD3',
  countryLineWidth = 1,
  boundaryColour = '#7C7CD3',
  boundaryFill = 'white',
  boundaryLineWidth = 1,
  boundaryShadowColour = '#E6EFFF',
  boundaryShadowWidth = 0,
  seaFill = '#E6EFFF',
  insetColour = '#7C7CD3',
  insetLineWidth = 1,
  callbackOptions=null,
  legendInteractivity='none',
  highlightClass='',
  highlightStyle='',
  lowlightStyle='',
} = {}) {

  // Make a new style sheet from highlight/lowlight styles
  // Make separate ones for text with fill style removed
  // so that colour of legend text is not affected. 
  // Add !important to all styles in order to override
  // values set on elements.
  if (highlightClass) {
    const sheet = document.createElement('style')
    document.body.appendChild(sheet)
    sheet.innerHTML = `
      .${highlightClass} {${createStyle(highlightStyle)}}
      .${highlightClass}-low {${createStyle(lowlightStyle)}}
      .${highlightClass}-text {${createStyle(highlightStyle, true)}}
      .${highlightClass}-text-low {${createStyle(lowlightStyle, true)}}
    `
  }
  function createStyle(str, forText) {
    const styles = str.split(';')
    const styleStr = styles.reduce((a,s) => {
      const els = s.split(':')
      if (forText && els[0].trim() === 'fill') {
        return a
      } else {
        return `${a} ${els[0]}: ${els[1]} !important;`
      }
    }, '')
    return styleStr
  }

  let trans, basemaps, boundary, boundaryf, dataBoundary, grid, dataGrid, vc, dataVc, country, dataCountry, taxonIdentifier, title

  // Create a parent div for the SVG within the parent element passed
  // as an argument. Allows us to style correctly for positioning etc.
  const mainDiv = d3.select(`${selector}`)
    .append("div")
    .attr('id', mapid)
    .style("position", "relative")
    .style("display", "inline")

  // Map loading spinner
  let mapLoaderShowExplicit = false
  const mapLoader = mainDiv.append("div")
    .classed('map-loader', true)
  const mapLoaderInner = mapLoader.append("div")
    .classed('map-loader-inner', true)
  mapLoaderInner.append("div")
    .classed('map-loader-spinner', true)
  mapLoaderInner.append("div").text("Loading map data...")
    .classed('map-loader-text', true)

  // Create the SVG.
  const svg = mainDiv.append("svg")
    .style("background-color", seaFill)
   //svg.append('defs')
   const defs = svg.append('defs')
   const shadow = defs.append('filter').attr('id', 'shadow')
   shadow.append('feDropShadow').attr('dx', 0).attr('dy', 0).attr('stdDeviation', boundaryShadowWidth).attr('flood-color', boundaryShadowColour)

  // Create the SVG graphic objects that store the major map elements.
  // The order these is created is important since it affects the order
  // in which they are rendered (i.e. what is drawn over what).
  boundaryf = svg.append("g").attr("id", "boundaryf")
  basemaps = svg.append("g").attr("id", "backimage")
  boundary = svg.append("g").attr("id", "boundary")
  vc = svg.append("g").attr("id", "vc")
    .style("display", vcLineStyle === "none" ? "none" : "")
  country = svg.append("g").attr("id", "country")
    .style("display", countryLineStyle === "none" ? "none" : "")
  grid = svg.append("g").attr("id", "grid")
    .style("stroke-dasharray", gridLineStyle === "dashed" ? "3,2" : "")
    .style("display", gridLineStyle === "none" ? "none" : "")
  title = svg.append("g").attr("id", "title")

  // Options dialog. 
  if ((transOptsControl && Object.keys(transOptsSel).length > 1) || 
    (mapTypesControl && Object.keys(mapTypesSel).length > 1)) {
    // Add gear icon to invoke options dialog
    mainDiv.append("img")
      //.attr("src", "../images/gear.png")
      .attr("src", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD8AAAA/CAYAAABXXxDfAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAPlSURBVGhD7ZprT+pAEIYLKBrjDULiH+SjGo0kahQMASLGmBiF+Bf9YowEvOseXp1i4fQys93WYnkS425Ttvvuzu7sTJtRSllpJUv/U0mqxcdm9sfHx2p+fp5q7ry9vVnNZjND1ciJbeaDhAPOPSaZrfm0MhOvQ6VSYe+UNzc3kdwr6YMbYvHX19eqXq+rlZUVC//psi8PDw9UCoZ7r7QPbohc3dnZmXp+fqbaD4uLi9b+/r6ni6rVaiqXy1HNn8/PTwjybOv09FS9vr5S7Yd8Pm8dHh6K3CR75of+11U4wHX4caqO0e122cJBNuvdpWq16ioc4DoGhqosWDN/fn6uHh8fqebPycnJ1+hLZtsLpxVwzXttbc3a2dlhWQBLfJh19RvYExBEoNlPm3DA7bO2q/sLBIrnmlCSMGb2YJoGQNJXttnPzc1RKblIvQtb/NHRUQbxdlKBnx+eNUQWKtrwTCYaMJBeBxYdWq2WuG/iTE6n01F3d3dU48PJ0nCyPW4Ui0Vra2srevE6fl+6YcbxDCAyeykwa51O4Tdx7C8i8cNNjz0j6LzOOrTBEpEMgFdg5YdIPMJGLiY2R0kbOnvF2Jpvt9vq5eXFQgQHobq+vVAoWNvb26HFg4uLC9Xv96kmA5bz8fHxpWV9fd3a3d0d69PYzEM4WFpaCnWoMSUc7O3tabcFa0CiBTmCXq9HV38YiUeWhoqheH9/p5I5TLU5qXEk/unpiUrh2NjYoJI5Go2GEUsaDAZU+mYk3jb5sGxubhozedNg/TsR7fbTDtJiTkbi/RKHf5WpEI8MMBVDsbCwQKVvRoqXl5epFA6kl6lojNvbWyqFA27PyUi830sHCVEkPUy1WSqVqPTNmK3jJIRdP2ycjVMZFX8daME5AVmecrk8NsGikFYSaupEc25E+UzRLhd1lDWJpA2dEFgkXhplhRkAaVZHJ4oUiZeCzutkZfAbnRBVijiNdXV1pe7v76nGB2YZNDvS2bZJdA5vEuy+8CqZTCZU3sCJzgYrEq87M3GAAZVGf+w1j1g4qcIBrAeZKKqyYIv3+iojSUjDcpZ4E+s8LiR9DRQ/TcJtuH2O1M8nnUDxps7occLtM2vmkcrmggfjz0TGFW3Y7dGlQCZjdj/Yfn7oQ9VkDsyJ1wlO50To9TlZ0DkDh6ZarcYeKPaar1arw7bd20Ws7HV0xQuMyaypH7jX6zs6PMPrlRn8vEQ4EB9vLy8vlfP7WI5J2q/BOCDPdnBwENimc0dfXV3971UUB/Fuj1mxBXPXIjrHhXuvsw86woF45nXh+l7ugJpg5ufTykx8Woltw0siM7NPJ5b1D5gX4NZABY0OAAAAAElFTkSuQmCC")
      .style("width", "16px")
      .style("position", "absolute")
      .style("right", "5px")
      .style("bottom", "7px")
      .on("click", function(){
        showOptsDialog(mapid, mapTypesKey, transOptsSel, transOptsKey)
      })
    // Create options dialog
    optsDialog(mapid, transOptsSel, transOptsKey, transOptsControl, mapTypesSel, mapTypesKey, mapTypesControl, userChangedOptions)
  }

  // Initialise the display
  trans = createTrans(transOptsSel[transOptsKey], height)
  setSvgSize()
  drawInsetBoxes()

  // Load boundary data
  let pBoundary, pGrid, pVc, pCountry
  if (boundaryGjson){
    pBoundary = d3.json(boundaryGjson).then(data => {
      dataBoundary = data
    })
  } else {
    pBoundary = Promise.resolve()
  }

  // Load grid data
  if (gridGjson){
    pGrid = d3.json(gridGjson).then(data => {
      dataGrid = data
    })
  } else {
    pGrid = Promise.resolve()
  }

  // Load country data
  if (countryGjson) {
    pCountry = d3.json(countryGjson).then(data => {
      dataCountry = data
    })
  } else {
    pCountry = Promise.resolve()
  }

  // Load VC data
  if (vcGjson) {
    pVc = d3.json(vcGjson).then(data => {
      dataVc = data
    })
  } else {
    pVc = Promise.resolve()
  }

  // Once loaded, draw boundary and grid
  Promise.allSettled([pBoundary, pGrid, pVc, pCountry]).then(() => {
    drawBoundaryAndGrid()
    if (!mapLoaderShowExplicit) {
      mapLoader.classed('map-loader-hidden', true)
    }
  })

  // End of initialisation

  function userChangedOptions(opts) {
    if (opts.transOptsKey && transOptsKey !== opts.transOptsKey){
      transOptsKey = opts.transOptsKey
      trans = createTrans(transOptsSel[transOptsKey], height)
      drawBoundaryAndGrid()
      setSvgSize()
      drawInsetBoxes()
      refreshMapDots()
      transformImages(basemaps, trans, seaFill)
    }
    if (opts.mapTypesKey && mapTypesKey !== opts.mapTypesKey){
      mapTypesKey = opts.mapTypesKey
      drawMapDots()
    }
    if (callbackOptions) {
      callbackOptions()
    }
  }

  function setSvgSize(){
    if (svg) {
      // Set width/height or viewbox depending on required behaviour
      if (expand) {
        svg.attr("viewBox", "0 0 " + trans.width + " " +  trans.height)
      } else {
        svg.attr("width", trans.width)
        svg.attr("height", trans.height)
      }
    }
  }

  function drawBoundaryAndGrid() {
    boundaryf.selectAll("path").remove()
    boundary.selectAll("path").remove()
    if (dataBoundary) {
      boundaryf.append("path")
        .datum(dataBoundary)
        .attr("d", trans.d3Path)
        .style("stroke-opacity", 0)
        .style("fill", boundaryFill)
        .style("filter", "url(#shadow)")
      boundary.append("path")
        .datum(dataBoundary)
        .classed('svg-map-boundary', true)
        .attr("d", trans.d3Path)
        .style("fill-opacity", 0)
        .style("stroke", boundaryColour)
        .style('stroke-width', boundaryLineWidth)
    }
    grid.selectAll("path").remove()
    if (dataGrid) {
      grid.append("path")
        .datum(dataGrid)
        .classed('svg-map-grid', true)
        .attr("d", trans.d3Path)
        .style("fill-opacity", 0)
        .style("stroke", gridLineColour)
        .style('stroke-width', gridLineWidth)
    }
    vc.selectAll("path").remove()
    if (dataVc) {
      vc.append("path")
        .datum(dataVc)
        .classed('svg-map-vcs', true)
        .attr("d", trans.d3Path)
        .style("fill-opacity", 0)
        .style("stroke", vcColour)
        .style('stroke-width', vcLineWidth)
    }
    country.selectAll("path").remove()
    if (dataCountry) {
      country.append("path")
        .datum(dataCountry)
        .classed('svg-map-countries', true)
        .attr("d", trans.d3Path)
        .style("fill-opacity", 0)
        .style("stroke", countryColour)
        .style('stroke-width', countryLineWidth)
    }
  }

  function drawInsetBoxes() {
    svg.selectAll('.svg-map-inset').remove()
    trans.insetDims.forEach(function(i){
      const margin = 10 
      svg.append('rect')
        .classed('svg-map-inset', true)
        .attr('x', i.x - margin)
        .attr('y', i.y - margin)
        .attr('width', i.width + 2 * margin)
        .attr('height', i.height + 2 * margin)
        .style('fill', 'none')
        .style('stroke', insetColour)
        .style('stroke-width', insetLineWidth)
    })
  }


  function drawMapDots() {
    // Returns a promise so that caller knows when data is loaded and transitions complete
    // (drawDots returns a promise which resolves when transitions complete)
    svg.select('#legend').remove() // Remove here to avoid legend resizing if inset options changed.
    return drawDots(svg, captionId, onclick, trans.point, mapTypesSel[mapTypesKey], taxonIdentifier, proj)
      .then (data => {
        if (data) {
          svg.select('#legend').remove() // Also must remove here to avoid some bad effects. 
          legendOpts.accessorData = data.legend
          if (legendOpts.display && (legendOpts.data || legendOpts.accessorData)) {
            svgLegend(svg, legendOpts, legendFontSize, legendFont, legendInteractivity, highlightClass)
          }  
        }
      })    
  }

  function refreshMapDots() {
    removeDots(svg)
    drawMapDots().catch(e => {
      //if (e !== "Data accessor not a function") {
        console.warn (e)
      //}
    })
  }

/** @function setTransform
  * @param {string} newTransOptsKey - specifies the key of the transformation object in the parent object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method sets a map transformation by selecting the one with the passed in key. It also
  * redisplays the map 
  */
  function setTransform (newTransOptsKey) {
    transOptsKey = newTransOptsKey
    trans = createTrans(transOptsSel[transOptsKey], height)
    drawBoundaryAndGrid()
    setSvgSize()
    drawInsetBoxes()
    refreshMapDots()
    transformImages(basemaps, trans, seaFill)
  }

/** @function setHeight
  * @param {string} newHeight - specifies the height of the SVG object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method resets the height of the main map SVG. 
  */
  function setHeight (newHeight) {
    height = newHeight
    // Removed the stuff below because the transformations
    // interferred with transformations triggered by other
    // methods when called straight afterwards. It is envisaged
    // that other methods would normally be called straight
    // after this one.

    // trans = createTrans(transOptsSel[transOptsKey], height)
    // drawBoundaryAndGrid()
    // setSvgSize()
    // drawInsetBoxes()
    // refreshMapDots()
    // transformImages(basemaps, trans, seaFill)
  }

/** @function setBoundary
  * @param {string} newGeojson - specifies the path of a geojson object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method replaces any existing map boundary geojson with the geojson in the file passed. It also
  * redisplays the map.
  */
  function setBoundary(newGeojson) {
    if (newGeojson) {
      pBoundary = d3.json(newGeojson).then(data => {
        dataBoundary = data
        drawBoundaryAndGrid()
      })
    } else {
      dataBoundary = null
      drawBoundaryAndGrid()
    }
  }


/** @function setGrid
  * @param {string} newGeojson - specifies the path of a geojson object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method replaces any existing grid geojson with the geojson in the file passed. It also
  * redisplays the map.
  */
  function setGrid(newGeojson) {
    if (newGeojson) {
      pBoundary = d3.json(newGeojson).then(data => {
        dataGrid = data
        drawBoundaryAndGrid()
      })
    } else {
      dataGrid = null
      drawBoundaryAndGrid()
    }
  }

/** @function setProj
  * @param {string} newProj - specifies a new projection for the map.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method replaces any existing map projection so that, for example, a map can be switched between
  * British and Irish grid projections.
  */
  function setProj(newProj) {
    proj = newProj
  }

/** @function animateTransChange
  * @param {string} newTransOptsKey - specifies the key of the transformation object in the parent object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The method sets a map transformation by selecting the one with the passed in key. It also
  * redisplays the map but animates the transition between the new transformation object and the
  * previous.
  */
  function animateTransChange(newTransOptsKey) {
    const lastTransOptsKey = transOptsKey
    svg.selectAll('.svg-map-inset').remove() // remove inset boxes
    removeDots(svg)  // remove dots
    const incr = 10
    for (let i=1; i<=incr + 1; i++){
      const tto = getTweenTransOpts(lastTransOptsKey, newTransOptsKey, height, i/incr)
      setTimeout(() => {
        trans = createTrans(tto, height)
        drawBoundaryAndGrid()
        setSvgSize()
        transformImages(basemaps, trans, seaFill)
        if (i > incr) {
          setTransform(newTransOptsKey)
        }
      }, 1000 * i / incr)
    }
  }

/** @function setBoundaryColour
  * @param {string} c - a string specifying the colour which can be hex format, e.g. #FFA500, 
  * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the boundary colour to the specified colour.
  */
  function setBoundaryColour(c){
    boundary.style("stroke", c)
    boundary.selectAll('path').style("stroke", c)
  }

/** @function setGridColour
  * @param {string} c - a string specifying the colour which can be hex format, e.g. #FFA500, 
  * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the grid colour to the specified colour.
  */
  function setGridColour(c){
    grid.style("stroke", c)
    grid.selectAll('path').style("stroke", c)
  }

/** @function setGridLineStyle
  * @param {string} c - a string specifying the style which can be set to either solid, dashed or none. 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the grid style to the specified value.
  */
  function setGridLineStyle(c){
    grid.style("stroke-dasharray", c === "dashed" ? "3,2" : "1,0")
    grid.style("display", c === "none" ? "none" : "")
  }

/** @function setVcLineStyle
  * @param {string} c - a string specifying the style which can be set to either none or something else (which will create solid). 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the line style of the Vice County lines to the specified value.
  */
  function setVcLineStyle(c){
    vc.style("display", c === "none" ? "none" : "")
  }

/** @function setVcColour
  * @param {string} c - a string specifying the colour which can be hex format, e.g. #FFA500, 
  * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the colour ov the vice count lines to the specified colour.
  */
  function setVcColour(c){
    vc.style("stroke", c)
    vc.selectAll('path').style("stroke", c)
  }

/** @function setCountryLineStyle
  * @param {string} c - a string specifying the style which can be set to either none or something else (which will create solid). 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the line style of the countries to the specified value.
  */
  function setCountryLineStyle(c){
    country.style("display", c === "none" ? "none" : "")
  }

/** @function setCountryColour
  * @param {string} c - a string specifying the colour which can be hex format, e.g. #FFA500, 
  * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Sets the line colour of the countries to the specified colour.
  */
  function setCountryColour(c){
    country.style("stroke", c)
    country.selectAll('path').style("stroke", c)
  }
  
/** @function setIdentfier
  * @param {string} identifier - a string which identifies some data to 
  * a data accessor function.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The data accessor function, specified elsewhere, will use this identifier to access
  * the correct data.
  */
  function setIdentfier(identifier) {
    taxonIdentifier = identifier
  }

/** @function setMapType
  * @param {string} newMapTypesKey - a string which a key used to identify a data accessor function. 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The data accessor is stored in the mapTypesSel object and referenced by this key.
  */
  function setMapType(newMapTypesKey) {
    mapTypesKey = newMapTypesKey
  }

/** @function basemapImage
  * @param {string} mapId - a string which should specify a unique key by which the image can be referenced. 
  * @param {boolean} show - a boolean value that indicates whether or not to display this image. 
  * @param {string} imageFile - a string identifying an image file. 
  * @param {string} worldFile - a string identifying a 'world' file. 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The image and world files together make a raster file that can be displayed in GIS. GIS, such as
  * QGIS can be used to create the image and world file. If you do this, make sure that the image
  * is created with the same projection as used for the SVG map - i.e. same projection as the vector
  * data for boundary and/or grid files.
  */
  function basemapImage(mapId, show, imageFile, worldFile) {
    showImage(mapId, show, basemaps, imageFile, worldFile, trans, seaFill)
  }
  
/** @function baseMapPriorities
  * @param {Array.<string>} mapIds - an array of strings which identify keys of basemap iamges.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The order the keys appear in the array specifies their priority when more than one is displayed
  * at the same time. Those at the start of the array have higher priority than those and the end.
  */
  function baseMapPriorities(mapIds) {
    setImagePriorities(basemaps, mapIds)
  }

/** @function setLegendOpts
  * @param {legendOpts} lo - a legend options object.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * The legend options object can be used to specify properties of a legend and even the content
  * of the legend itself.
  */
  function setLegendOpts(lo) {
    legendOpts = lo
  }

/** @function redrawMap
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Redraw the map, e.g. after changing map accessor function or map identifier.
  */
  function redrawMap(){
    return drawMapDots()
  }

/** @function clearMap
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Clear the map of dots and legend.
  */
  function clearMap(){
    svg.select('#legend').remove()
    title.selectAll('*').remove()
    removeDots(svg)
  }

/** @function getMapWidth
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Return the width of the map.
  */
  function getMapWidth(){
    return trans.width
  }

/** @function saveMap
  * @param {boolean} asSvg - a boolean value that indicates whether to generate an SVG (if false, generates PNG image). 
  * @param {Object} svgInfo - Initialisation options. Whole arg can be set to null if no info options.
  * @param {string} svgInfo.text - A text string to be displayed at the foot of the map. 
  * This will be word-wrapped to the width of the image.
  * Some HTML tags, e.g. <i> are recognised, but in order to facilitate word wrapping, each word must be marked up
  * separately - there should be no white space within the tag.
  * @param {Array.<string>} svgInfo.textFormatted - An array of strings to be concatenated anddisplayed at the foot of the map. 
  * Each element in the array is preceded by one of the of the following tokens which indicates how it is to be formatted:
  * n# indicates no formatting; i# indicates italics and b# indicates emboldening.
  * This resultig string will be word-wrapped to the width of the image.
  * @param {string} svgInfo.img - The path of an image to be displayed at the foot of the map. If the image is wider
  * than the SVG, it is rescaled to the size of the SVG.
  * @param {number} svgInfo.fontSize - The size of the font to be used for the text string (defaults to 12)
  * @param {number} svgInfo.margin - The size of a margin, in pixels, to be placed around the text and/or image.
  * @param {string} filename - Name of the file (without extension) to generate and download.
  * Creates an image from the displayed map and downloads to user's computer. If
  * the filename is falsey (e.g. blank), it will not automatically download the
  * file. (Allows caller to do something else with the data URL which is returned
  * as the promises resolved value.)
  * @returns {Promise} promise object represents the data URL of the image.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * 
  */
  function saveMap(asSvg, svgInfo, filename) {
    return saveMapImage(svg, trans, expand, asSvg, svgInfo, filename)
  }

/** @function downloadData
  * @param {boolean} asGeojson - a boolean value that indicates whether to generate GeoJson (if false, generates CSV). 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  */
  function downloadData(asGeojson){
    const accessFunction = mapTypesSel[mapTypesKey]
    downloadCurrentData(accessFunction(taxonIdentifier), 10000, asGeojson)
  }

/** @function setMapTitle
  * @param {string} text - A text value for the map title (can include <b> and <i> tags). 
  * @param {number} fontSize - A number indicating font size in pixels.
  * @param {number} x - a number indicating origin x position of text in map svg.
  * @param {number} y - a number indicating origin y position of text in map svg.
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Creates a title which can be positioned independently of the legend. This is useful when the legend needs
  * to be moved, but the title does not.
  */
  function setMapTitle(text, fontSize, x, y) {

    let tText = text
    tText = tText.replaceAll('<i>', '<tspan style="font-style: italic">' )
    tText = tText.replaceAll('</i>', '</tspan>' )
    tText = tText.replaceAll('<b>', '<tspan style="font-weight: bold">' )
    tText = tText.replaceAll('</b>', '</tspan>' )

    title.selectAll('*').remove()
    title.append('text').html(tText)
      .attr('x', x)
      .attr('y', y)
      .attr('font-size', fontSize + 'px')
      .style('font-family','Arial, Helvetica, sans-serif')
  }

 /** @function showBusy
  * @param {boolean} show - A boolean value to indicate whether or not to show map data loading. 
  * @description <b>This function is exposed as a method on the API returned from the svgMap function</b>.
  * Allows calling application to display/hide an indicator showing the map data is loading.
  */
  function showBusy(show) {
    mapLoaderShowExplicit = show
    mapLoader.classed('map-loader-hidden', !mapLoaderShowExplicit)
  }

  /**
   * @typedef {Object} api
   * @property {module:svgMap~setBoundary} setBoundary - Change the map boundary. Pass a single argument
   * which is the path of a geojson file.
   * @property {module:svgMap~setGrid} setGrid - Change the grid lines for the map. Pass a single argument
   * which is the path of a geojson file.
   * @property {module:svgMap~setBoundaryColour} setBoundaryColour - Change the colour of the boundary. Pass a single argument
   * which is a string specifying the colour which can be hex format, e.g. #FFA500, 
   * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
   * @property {module:svgMap~setGridColour} setGridColour - Change the colour of the grid. Pass a single argument
   * which is a string specifying the colour which can be hex format, e.g. #FFA500, 
   * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
   * @property {module:svgMap~setCountryColour} setCountryColour - Change the colour of the country boundaries. Pass a single argument
   * which is a string specifying the colour which can be hex format, e.g. #FFA500, 
   * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
   * @property {module:svgMap~setVcColour} setVcColour - Change the colour of the vice county lines. Pass a single argument
   * which is a string specifying the colour which can be hex format, e.g. #FFA500, 
   * RGB format, e.g. rgb(100, 255, 0) or a named colour, e.g. red.
   * @property {module:svgMap~setGridLineStyle} setGridLineStyle - Set the line style of the grid line geoJson. 
   * Can be solid, dashed or none.
   * @property {module:svgMap~setCountryLineStyle} setCountryLineStyle - Set the line style of the country boundaries geoJson. 
   * Can be none or something else (which will give solid line).
   * @property {module:svgMap~setVcLineStyle} setVcLineStyle - Set the line style of the Vice County line geoJson. 
   * Can be none or something else (which will give solid line).
   * @property {module:svgMap~setTransform} setTransform - Set the transformation options object by passing a single argument
   * which is a string indicating the key of the transformation in the parent object.
   * @property {module:svgMap~getMapWidth} getMapWidth - Gets and returns the current width of the SVG map. 
   * @property {module:svgMap~animateTransChange} animateTransChange - Set the a new transormation object and animates the transition.
   * @property {module:svgMap~setIdentfier} setIdentfier - Identifies data to the data accessor function.
   * @property {module:svgMap~setMapType} setMapType - Set the key of the data accessor function.
   * @property {module:svgMap~basemapImage} basemapImage - Specifies an image and world file for a basemap.
   * @property {module:svgMap~baseMapPriorities} baseMapPriorities - Identifies the display order of the basemap images.
   * @property {module:svgMap~setLegendOpts} setLegendOpts - Sets options for the legend.
   * @property {module:svgMap~redrawMap} redrawMap - Redraw the map.
   * @property {module:svgMap~clearMap} clearMap - Clear the map.
   * @property {module:svgMap~saveMap} saveMap - Save and download the map as an image.
   * @property {module:svgMap~downloadData} downloadData - Download a the map data as a CSV or GeoJson file.
   * @property {module:svgMap~setProj} setProj - Change the map projection. The argument is a string of the form 'gb', 'ir' or 'ci'.
   * @property {module:svgMap~setProj} setHeight - Reset the height of the main map SVG object.
   * @property {module:svgMap~showBusy} showBusy - Set a boolean value to indicate whether or not to show map data loading.
   */
  return {
    setBoundary: setBoundary,
    setGrid: setGrid,
    setBoundaryColour: setBoundaryColour,
    setGridColour: setGridColour,
    setGridLineStyle: setGridLineStyle,
    setVcLineStyle: setVcLineStyle,
    setVcColour: setVcColour,
    setCountryLineStyle: setCountryLineStyle,
    setCountryColour: setCountryColour,
    setTransform: setTransform,
    getMapWidth: getMapWidth,
    animateTransChange: animateTransChange,
    setIdentfier: setIdentfier,
    setMapType: setMapType,
    basemapImage: basemapImage,
    baseMapPriorities: baseMapPriorities,
    setLegendOpts: setLegendOpts,
    redrawMap: redrawMap,
    clearMap: clearMap,
    saveMap: saveMap,
    downloadData: downloadData,
    setProj: setProj,
    setHeight: setHeight,
    setMapTitle: setMapTitle,
    showBusy: showBusy
  }
}