2022-01-24 05:59:36 +00:00
import React , { useCallback , useRef , useEffect , useState } from "react" ;
import { useRouter } from "next/router" ;
2022-01-15 03:46:39 +00:00
import Gradient from "javascript-color-gradient" ;
2022-01-15 04:27:29 +00:00
import mapboxgl from "mapbox-gl" ;
2022-01-19 06:06:19 +00:00
import useSWRImmutable from "swr/immutable" ;
2022-01-15 03:46:39 +00:00
import styles from "../styles/Map.module.css" ;
2022-01-16 07:26:17 +00:00
import Sidebar from "./Sidebar" ;
2022-01-15 03:46:39 +00:00
import ToggleLayersControl from "./ToggleLayersControl" ;
2022-01-24 05:59:36 +00:00
import SearchBar from "./SearchBar" ;
2022-01-15 03:46:39 +00:00
2022-01-15 04:27:29 +00:00
mapboxgl . accessToken = process . env . NEXT_PUBLIC_MAPBOX_TOKEN ? ? "" ;
2022-01-15 03:46:39 +00:00
const colorGradient = new Gradient ( ) ;
2022-01-19 06:06:19 +00:00
colorGradient . setGradient (
"#0000FF" ,
"#00FF00" ,
"#FFFF00" ,
"#FFA500" ,
"#FF0000"
) ;
2022-01-17 22:10:59 +00:00
colorGradient . setMidpoint ( 360 ) ;
2022-01-15 03:46:39 +00:00
2022-01-30 21:55:50 +00:00
const LIVE_DOWNLOAD_COUNTS_URL =
"https://staticstats.nexusmods.com/live_download_counts/mods/1704.csv" ;
2022-01-19 06:06:19 +00:00
const jsonFetcher = ( url : string ) = > fetch ( url ) . then ( ( res ) = > res . json ( ) ) ;
2022-01-30 21:55:50 +00:00
const csvFetcher = ( url : string ) = > fetch ( url ) . then ( ( res ) = > res . text ( ) ) ;
2022-01-19 06:06:19 +00:00
2022-01-16 07:26:17 +00:00
const Map : React.FC = ( ) = > {
2022-01-24 05:59:36 +00:00
const router = useRouter ( ) ;
2022-01-15 04:27:29 +00:00
const mapContainer = useRef < HTMLDivElement | null > (
null
) as React . MutableRefObject < HTMLDivElement > ;
const map = useRef < mapboxgl.Map | null > (
null
) as React . MutableRefObject < mapboxgl.Map > ;
2022-01-24 00:39:36 +00:00
const mapWrapper = useRef < HTMLDivElement | null > (
null
) as React . MutableRefObject < HTMLDivElement > ;
2022-01-24 05:59:36 +00:00
const [ mapLoaded , setMapLoaded ] = useState < boolean > ( false ) ;
const [ heatmapLoaded , setHeatmapLoaded ] = useState < boolean > ( false ) ;
const [ selectedCell , setSelectedCell ] = useState < {
x : number ;
y : number ;
} | null > ( null ) ;
2022-02-07 03:00:14 +00:00
const [ selectedCells , setSelectedCells ] = useState <
| {
x : number ;
y : number ;
} [ ]
| null
> ( null ) ;
2022-01-24 05:59:36 +00:00
const sidebarOpen = selectedCell !== null || router . query . mod !== undefined ;
2022-01-19 06:06:19 +00:00
2022-01-30 21:55:50 +00:00
const { data : cellsData , error : cellsError } = useSWRImmutable (
2022-01-19 06:06:19 +00:00
"https://cells.modmapper.com/edits.json" ,
jsonFetcher
) ;
2022-01-30 21:55:50 +00:00
// The live download counts are not really immutable, but I'd still rather load them once per session
const [ counts , setCounts ] = useState < Record <
number ,
[ number , number , number ]
> | null > ( null ) ;
const { data : countsData , error : countsError } = useSWRImmutable (
LIVE_DOWNLOAD_COUNTS_URL ,
csvFetcher
) ;
2022-01-15 03:46:39 +00:00
2022-01-24 05:59:36 +00:00
const selectMapCell = useCallback (
( cell : { x : number ; y : number } ) = > {
if ( ! map . current ) return ;
if ( map . current && ! map . current . getSource ( "grid-source" ) ) return ;
map . current . setFeatureState (
{
source : "grid-source" ,
id : ( cell . x + 57 ) * 100 + 50 - cell . y ,
} ,
{
selected : true ,
}
) ;
requestAnimationFrame ( ( ) = > map . current && map . current . resize ( ) ) ;
var zoom = map . current . getZoom ( ) ;
var viewportNW = map . current . project ( [ - 180 , 85.051129 ] ) ;
var cellSize = Math . pow ( 2 , zoom + 2 ) ;
const x = cell . x + 57 ;
const y = 50 - cell . y ;
let nw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y ,
] ) ;
let ne = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y ,
] ) ;
let se = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
let sw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
const selectedCellLines : GeoJSON.FeatureCollection <
GeoJSON . Geometry ,
GeoJSON . GeoJsonProperties
> = {
type : "FeatureCollection" ,
features : [
{
type : "Feature" ,
geometry : {
type : "LineString" ,
coordinates : [
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
[ se . lng , se . lat ] ,
[ sw . lng , sw . lat ] ,
[ nw . lng , nw . lat ] ,
] ,
} ,
properties : { x : x , y : y } ,
} ,
] ,
} ;
if ( map . current . getLayer ( "selected-cell-layer" ) ) {
map . current . removeLayer ( "selected-cell-layer" ) ;
}
if ( map . current . getSource ( "selected-cell-source" ) ) {
map . current . removeSource ( "selected-cell-source" ) ;
}
map . current . addSource ( "selected-cell-source" , {
type : "geojson" ,
data : selectedCellLines ,
} ) ;
map . current . addLayer ( {
id : "selected-cell-layer" ,
type : "line" ,
source : "selected-cell-source" ,
paint : {
"line-color" : "blue" ,
"line-width" : 3 ,
} ,
} ) ;
const bounds = map . current . getBounds ( ) ;
if ( ! bounds . contains ( nw ) || ! bounds . contains ( se ) ) {
map . current . panTo ( nw ) ;
}
} ,
[ map ]
) ;
2022-02-07 03:00:14 +00:00
const selectCells = useCallback (
( cells : { x : number ; y : number } [ ] ) = > {
if ( ! map . current ) return ;
if ( map . current && ! map . current . getSource ( "grid-source" ) ) return ;
var zoom = map . current . getZoom ( ) ;
var viewportNW = map . current . project ( [ - 180 , 85.051129 ] ) ;
var cellSize = Math . pow ( 2 , zoom + 2 ) ;
const selectedCellsLines : GeoJSON.FeatureCollection <
GeoJSON . Geometry ,
GeoJSON . GeoJsonProperties
> = {
type : "FeatureCollection" ,
features : [ ] ,
} ;
let bounds : mapboxgl.LngLatBounds | null = null ;
for ( const cell of cells ) {
const x = cell . x + 57 ;
const y = 50 - cell . y ;
let nw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y ,
] ) ;
let ne = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y ,
] ) ;
let se = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
let sw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
if ( bounds ) {
bounds . extend ( new mapboxgl . LngLatBounds ( sw , ne ) ) ;
} else {
bounds = new mapboxgl . LngLatBounds ( sw , ne ) ;
}
selectedCellsLines . features . push ( {
type : "Feature" ,
geometry : {
type : "LineString" ,
coordinates : [
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
[ se . lng , se . lat ] ,
[ sw . lng , sw . lat ] ,
[ nw . lng , nw . lat ] ,
] ,
} ,
properties : { x : x , y : y } ,
} ) ;
}
if ( map . current . getLayer ( "selected-cells-layer" ) ) {
map . current . removeLayer ( "selected-cells-layer" ) ;
}
if ( map . current . getSource ( "selected-cells-source" ) ) {
map . current . removeSource ( "selected-cells-source" ) ;
}
map . current . addSource ( "selected-cells-source" , {
type : "geojson" ,
data : selectedCellsLines ,
} ) ;
map . current . addLayer ( {
id : "selected-cells-layer" ,
type : "line" ,
source : "selected-cells-source" ,
paint : {
"line-color" : "purple" ,
"line-width" : 4 ,
} ,
} ) ;
requestAnimationFrame ( ( ) = > {
if ( map . current ) {
map . current . resize ( ) ;
if ( bounds ) {
map . current . fitBounds ( bounds , { padding : 20 } ) ;
}
}
} ) ;
} ,
[ map ]
) ;
2022-01-24 05:59:36 +00:00
const selectCell = useCallback (
( cell ) = > {
router . push ( { query : { cell : cell.x + "," + cell . y } } ) ;
setSelectedCell ( cell ) ;
selectMapCell ( cell ) ;
} ,
[ setSelectedCell , selectMapCell , router ]
) ;
const clearSelectedCell = useCallback ( ( ) = > {
setSelectedCell ( null ) ;
if ( map . current ) map . current . removeFeatureState ( { source : "grid-source" } ) ;
if ( map . current && map . current . getLayer ( "selected-cell-layer" ) ) {
map . current . removeLayer ( "selected-cell-layer" ) ;
}
if ( map . current && map . current . getSource ( "selected-cell-source" ) ) {
map . current . removeSource ( "selected-cell-source" ) ;
}
requestAnimationFrame ( ( ) = > {
if ( map . current ) map . current . resize ( ) ;
} ) ;
2022-01-30 21:14:32 +00:00
} , [ map ] ) ;
2022-02-07 03:00:14 +00:00
const clearSelectedCells = useCallback ( ( ) = > {
setSelectedCells ( null ) ;
if ( map . current && map . current . getLayer ( "selected-cells-layer" ) ) {
map . current . removeLayer ( "selected-cells-layer" ) ;
}
if ( map . current && map . current . getSource ( "selected-cells-source" ) ) {
map . current . removeSource ( "selected-cells-source" ) ;
}
requestAnimationFrame ( ( ) = > {
if ( map . current ) map . current . resize ( ) ;
} ) ;
} , [ map ] ) ;
2022-01-30 21:14:32 +00:00
const clearSelectedMod = useCallback ( ( ) = > {
requestAnimationFrame ( ( ) = > {
if ( map . current ) map . current . resize ( ) ;
} ) ;
} , [ map ] ) ;
2022-01-24 05:59:36 +00:00
useEffect ( ( ) = > {
if ( ! heatmapLoaded ) return ; // wait for all map layers to load
if ( router . query . cell && typeof router . query . cell === "string" ) {
const cellUrlParts = decodeURIComponent ( router . query . cell ) . split ( "," ) ;
const cell = {
x : parseInt ( cellUrlParts [ 0 ] ) ,
y : parseInt ( cellUrlParts [ 1 ] ) ,
} ;
if (
! selectedCell ||
selectedCell . x !== cell . x ||
selectedCell . y !== cell . y
) {
selectCell ( cell ) ;
}
} else {
if ( selectedCell ) {
clearSelectedCell ( ) ;
}
}
} , [
selectedCell ,
router . query . cell ,
2022-01-30 21:14:32 +00:00
router . query . mod ,
2022-01-24 05:59:36 +00:00
selectCell ,
clearSelectedCell ,
heatmapLoaded ,
] ) ;
2022-02-07 03:00:14 +00:00
useEffect ( ( ) = > {
if ( ! heatmapLoaded ) return ; // wait for all map layers to load
if (
router . query . mod &&
typeof router . query . mod === "string" &&
selectedCells
) {
selectCells ( selectedCells ) ;
} else {
if ( selectedCells ) {
clearSelectedCells ( ) ;
}
}
} , [
selectedCells ,
router . query . mod ,
selectCells ,
clearSelectedCells ,
heatmapLoaded ,
] ) ;
2022-01-30 21:14:32 +00:00
useEffect ( ( ) = > {
if ( ! heatmapLoaded ) return ; // wait for all map layers to load
if ( ! router . query . mod || typeof router . query . mod !== "string" ) {
clearSelectedMod ( ) ;
}
} , [ router . query . mod , clearSelectedMod , heatmapLoaded ] ) ;
2022-01-15 03:46:39 +00:00
useEffect ( ( ) = > {
if ( map . current ) return ; // initialize map only once
map . current = new mapboxgl . Map ( {
container : mapContainer.current ,
style : {
version : 8 ,
sources : {
"raster-tiles" : {
type : "raster" ,
tiles : [ "https://tiles.modmapper.com/{z}/{x}/{y}.jpg" ] ,
tileSize : 256 ,
attribution :
2022-02-07 03:13:06 +00:00
'Map tiles by <a href="https://en.uesp.net/wiki/Skyrim:Skyrim" target="_blank">UESP</a>. Mod data from <a href="https://nexusmods.com" target="_blank">Nexus Mods</a>.' ,
2022-01-15 03:46:39 +00:00
} ,
} ,
layers : [
{
id : "simple-tiles" ,
type : "raster" ,
source : "raster-tiles" ,
} ,
] ,
glyphs : "mapbox://fonts/mapbox/{fontstack}/{range}.pbf" ,
} ,
center : [ 0 , 0 ] ,
zoom : 0 ,
2022-01-16 07:26:17 +00:00
minZoom : 0 ,
2022-01-15 03:46:39 +00:00
maxZoom : 8 ,
2022-01-19 06:06:19 +00:00
maxBounds : [
[ - 180 , - 85.051129 ] ,
[ 180 , 85.051129 ] ,
] ,
2022-01-15 03:46:39 +00:00
} ) ;
2022-01-24 05:59:36 +00:00
map . current . on ( "load" , ( ) = > {
setMapLoaded ( true ) ;
} ) ;
} , [ setMapLoaded ] ) ;
2022-01-15 03:46:39 +00:00
useEffect ( ( ) = > {
2022-01-30 21:55:50 +00:00
if ( ! cellsData || ! router . isReady || ! mapLoaded ) return ; // wait for map to initialize and data to load
2022-01-24 05:59:36 +00:00
if ( map . current . getSource ( "graticule" ) ) return ; // don't initialize twice
2022-01-15 03:46:39 +00:00
2022-01-24 05:59:36 +00:00
const zoom = map . current . getZoom ( ) ;
const viewportNW = map . current . project ( [ - 180 , 85.051129 ] ) ;
const cellSize = Math . pow ( 2 , zoom + 2 ) ;
const graticule : GeoJSON.FeatureCollection <
GeoJSON . Geometry ,
GeoJSON . GeoJsonProperties
> = {
type : "FeatureCollection" ,
features : [ ] ,
} ;
for ( let x = 0 ; x < 128 ; x += 1 ) {
let lng = map . current . unproject ( [ x * cellSize + viewportNW . x , - 90 ] ) . lng ;
graticule . features . push ( {
type : "Feature" ,
geometry : {
type : "LineString" ,
coordinates : [
[ lng , - 90 ] ,
[ lng , 90 ] ,
] ,
} ,
properties : { value : x } ,
} ) ;
}
for ( let y = 0 ; y < 128 ; y += 1 ) {
let lat = map . current . unproject ( [ - 180 , y * cellSize + viewportNW . y ] ) . lat ;
graticule . features . push ( {
type : "Feature" ,
geometry : {
type : "LineString" ,
coordinates : [
[ - 180 , lat ] ,
[ 180 , lat ] ,
] ,
} ,
properties : { value : y } ,
} ) ;
}
map . current . addSource ( "graticule" , {
type : "geojson" ,
data : graticule ,
} ) ;
map . current . addLayer ( {
id : "graticule" ,
type : "line" ,
source : "graticule" ,
} ) ;
const gridLabelPoints : GeoJSON.FeatureCollection <
GeoJSON . Geometry ,
GeoJSON . GeoJsonProperties
> = {
type : "FeatureCollection" ,
features : [ ] ,
} ;
for ( let x = 0 ; x < 128 ; x += 1 ) {
for ( let y = 0 ; y < 128 ; y += 1 ) {
let nw = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize / 32 ,
y * cellSize + viewportNW . y + cellSize / 32 ,
] ) ;
gridLabelPoints . features . push ( {
2022-01-15 03:46:39 +00:00
type : "Feature" ,
geometry : {
2022-01-24 05:59:36 +00:00
type : "Point" ,
coordinates : [ nw . lng , nw . lat ] ,
} ,
properties : {
label : ` ${ x - 57 } , ${ 50 - y } ` ,
2022-01-15 03:46:39 +00:00
} ,
} ) ;
}
2022-01-24 05:59:36 +00:00
}
map . current . addSource ( "grid-labels-source" , {
type : "geojson" ,
data : gridLabelPoints ,
} ) ;
map . current . addLayer ( {
id : "grid-labels-layer" ,
type : "symbol" ,
source : "grid-labels-source" ,
layout : {
"text-field" : [ "get" , "label" ] ,
"text-font" : [ "Open Sans Semibold" , "Arial Unicode MS Bold" ] ,
"text-offset" : [ 0 , 0 ] ,
"text-anchor" : "top-left" ,
"text-rotation-alignment" : "map" ,
} ,
paint : {
"text-halo-width" : 1 ,
"text-halo-blur" : 3 ,
"text-halo-color" : "rgba(255,255,255,0.8)" ,
} ,
minzoom : 4 ,
} ) ;
const grid : GeoJSON.FeatureCollection <
GeoJSON . Geometry ,
GeoJSON . GeoJsonProperties
> = {
type : "FeatureCollection" ,
features : [ ] ,
} ;
for ( let x = 0 ; x < 128 ; x += 1 ) {
2022-01-15 03:46:39 +00:00
for ( let y = 0 ; y < 128 ; y += 1 ) {
2022-01-24 05:59:36 +00:00
let nw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
2022-01-15 03:46:39 +00:00
y * cellSize + viewportNW . y ,
2022-01-24 05:59:36 +00:00
] ) ;
let ne = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y ,
] ) ;
let se = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
let sw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
2022-01-30 21:55:50 +00:00
const editCount = ( cellsData as Record < string , number > ) [
2022-01-24 05:59:36 +00:00
` ${ x - 57 } , ${ 50 - y } `
] ;
grid . features . push ( {
2022-01-15 03:46:39 +00:00
type : "Feature" ,
2022-01-24 05:59:36 +00:00
id : x * 100 + y ,
2022-01-15 03:46:39 +00:00
geometry : {
2022-01-24 05:59:36 +00:00
type : "Polygon" ,
2022-01-15 03:46:39 +00:00
coordinates : [
2022-01-24 05:59:36 +00:00
[
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
[ se . lng , se . lat ] ,
[ sw . lng , sw . lat ] ,
[ nw . lng , nw . lat ] ,
] ,
2022-01-15 03:46:39 +00:00
] ,
} ,
2022-01-24 05:59:36 +00:00
properties : {
x : x ,
y : y ,
cellX : x - 57 ,
cellY : 50 - y ,
label : ` ${ x - 57 } , ${ 50 - y } ` ,
color : editCount ? colorGradient . getColor ( editCount ) : "#888888" ,
opacity : editCount ? Math . min ( ( editCount / 150 ) * 0.25 , 0.5 ) : 0 ,
} ,
2022-01-15 03:46:39 +00:00
} ) ;
}
2022-01-24 05:59:36 +00:00
}
2022-01-15 03:46:39 +00:00
2022-01-24 05:59:36 +00:00
map . current . addSource ( "grid-source" , {
type : "geojson" ,
data : grid ,
} ) ;
2022-01-15 03:46:39 +00:00
2022-01-24 05:59:36 +00:00
map . current . addLayer (
{
id : "grid-layer" ,
type : "fill" ,
source : "grid-source" ,
2022-01-19 06:06:19 +00:00
paint : {
2022-01-24 05:59:36 +00:00
"fill-opacity" : 0 ,
2022-01-17 22:10:59 +00:00
} ,
2022-01-24 05:59:36 +00:00
} ,
"grid-labels-layer"
) ;
2022-01-17 22:10:59 +00:00
2022-01-24 05:59:36 +00:00
map . current . addLayer (
{
id : "heatmap-layer" ,
type : "fill" ,
source : "grid-source" ,
paint : {
"fill-color" : [ "get" , "color" ] ,
"fill-opacity" : [ "get" , "opacity" ] ,
"fill-outline-color" : [
"case" ,
[ "boolean" , [ "feature-state" , "selected" ] , false ] ,
"white" ,
"transparent" ,
] ,
2022-01-15 03:46:39 +00:00
} ,
2022-01-24 05:59:36 +00:00
} ,
"grid-labels-layer"
) ;
const fullscreenControl = new mapboxgl . FullscreenControl ( ) ;
( fullscreenControl as unknown as { _container : HTMLElement } ) . _container =
mapWrapper . current ;
map . current . addControl ( fullscreenControl ) ;
map . current . addControl ( new mapboxgl . NavigationControl ( ) ) ;
2022-01-23 18:06:44 +00:00
2022-01-24 05:59:36 +00:00
let singleClickTimeout : NodeJS.Timeout | null = null ;
map . current . on ( "click" , "grid-layer" , ( e ) = > {
const features = e . features ;
if ( singleClickTimeout ) return ;
singleClickTimeout = setTimeout ( ( ) = > {
2022-01-23 23:29:30 +00:00
singleClickTimeout = null ;
2022-01-24 05:59:36 +00:00
if ( features && features [ 0 ] ) {
const cell = {
x : features [ 0 ] . properties ! . cellX ,
y : features [ 0 ] . properties ! . cellY ,
} ;
router . push ( { query : { cell : cell.x + "," + cell . y } } ) ;
}
} , 200 ) ;
} ) ;
2022-01-24 00:39:36 +00:00
2022-01-24 05:59:36 +00:00
map . current . on ( "dblclick" , "grid-layer" , ( e ) = > {
if ( singleClickTimeout ) clearTimeout ( singleClickTimeout ) ;
singleClickTimeout = null ;
2022-01-15 03:46:39 +00:00
} ) ;
2022-01-24 05:59:36 +00:00
setHeatmapLoaded ( true ) ;
2022-01-30 21:55:50 +00:00
} , [ cellsData , mapLoaded , router , setHeatmapLoaded ] ) ;
useEffect ( ( ) = > {
if ( countsData ) {
const newCounts : Record < number , [ number , number , number ] > = { } ;
for ( const line of countsData . split ( "\n" ) ) {
const nums = line . split ( "," ) . map ( ( count ) = > parseInt ( count , 10 ) ) ;
newCounts [ nums [ 0 ] ] = [ nums [ 1 ] , nums [ 2 ] , nums [ 3 ] ] ;
}
setCounts ( newCounts ) ;
}
} , [ setCounts , countsData ] ) ;
2022-01-15 03:46:39 +00:00
return (
2022-01-16 07:26:17 +00:00
< >
2022-01-19 06:06:19 +00:00
< div
className = { ` ${ styles [ "map-wrapper" ] } ${
2022-01-24 05:59:36 +00:00
sidebarOpen ? styles [ "map-wrapper-sidebar-open" ] : ""
2022-01-19 06:06:19 +00:00
} ` }
2022-01-24 00:39:36 +00:00
ref = { mapWrapper }
2022-01-19 06:06:19 +00:00
>
2022-01-24 00:39:36 +00:00
< div ref = { mapContainer } className = { styles [ "map-container" ] } >
< Sidebar
selectedCell = { selectedCell }
2022-01-24 05:59:36 +00:00
clearSelectedCell = { ( ) = > router . push ( { query : { } } ) }
2022-02-07 03:00:14 +00:00
setSelectedCells = { setSelectedCells }
2022-01-24 00:39:36 +00:00
map = { map }
2022-01-30 21:55:50 +00:00
counts = { counts }
countsError = { countsError }
2022-01-24 00:39:36 +00:00
/ >
< ToggleLayersControl map = { map } / >
2022-01-24 05:59:36 +00:00
< SearchBar
map = { map }
clearSelectedCell = { ( ) = > router . push ( { query : { } } ) }
2022-01-30 21:55:50 +00:00
counts = { counts }
2022-01-24 05:59:36 +00:00
/ >
2022-01-24 00:39:36 +00:00
< / div >
2022-01-16 07:26:17 +00:00
< / div >
< / >
2022-01-15 03:46:39 +00:00
) ;
} ;
export default Map ;