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
2022-03-15 04:22:08 +00:00
import { useAppDispatch , useAppSelector } from "../lib/hooks" ;
import { setFetchedPlugin , PluginFile } from "../slices/plugins" ;
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-02-27 06:17:52 +00:00
const jsonFetcher = ( url : string ) = >
fetch ( url ) . then ( async ( res ) = > ( {
lastModified : res.headers.get ( "Last-Modified" ) ,
data : await 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-02-27 06:17:52 +00:00
const [ sidebarOpen , setSidebarOpen ] = useState ( true ) ;
2022-03-15 04:22:08 +00:00
const dispatch = useAppDispatch ( ) ;
2022-02-27 06:17:52 +00:00
const plugins = useAppSelector ( ( state ) = > state . plugins . plugins ) ;
const pluginsPending = useAppSelector ( ( state ) = > state . plugins . pending ) ;
2022-03-12 23:34:45 +00:00
const fetchedPlugin = useAppSelector ( ( state ) = > state . plugins . fetchedPlugin ) ;
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 ;
2022-02-11 17:40:27 +00:00
map . current . removeFeatureState ( { source : "grid-source" } ) ;
2022-01-24 05:59:36 +00:00
map . current . setFeatureState (
{
source : "grid-source" ,
2022-02-27 06:17:52 +00:00
id : ( cell . x + 57 ) * 1000 + 50 - cell . y ,
2022-01-24 05:59:36 +00:00
} ,
{
selected : true ,
}
) ;
2022-02-11 17:40:27 +00:00
map . current . removeFeatureState ( { source : "selected-cell-source" } ) ;
2022-02-27 06:17:52 +00:00
map . current . removeFeatureState ( { source : "conflicted-cell-source" } ) ;
2022-02-11 17:40:27 +00:00
map . current . setFeatureState (
{
source : "selected-cell-source" ,
2022-02-27 06:17:52 +00:00
id : ( cell . x + 57 ) * 1000 + 50 - cell . y ,
2022-01-24 05:59:36 +00:00
} ,
2022-02-11 17:40:27 +00:00
{
cellSelected : true ,
modSelected : false ,
}
) ;
requestAnimationFrame ( ( ) = > map . current && map . current . resize ( ) ) ;
2022-02-07 03:00:14 +00:00
2022-02-11 17:40:27 +00:00
const panTo = ( ) = > {
const zoom = map . current . getZoom ( ) ;
const viewportNW = map . current . project ( [ - 180 , 85.051129 ] ) ;
const cellSize = Math . pow ( 2 , zoom + 2 ) ;
2022-02-07 03:00:14 +00:00
const x = cell . x + 57 ;
const y = 50 - cell . y ;
let nw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y ,
] ) ;
let se = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
2022-02-11 17:40:27 +00:00
const bounds = map . current . getBounds ( ) ;
if ( ! bounds . contains ( nw ) || ! bounds . contains ( se ) ) {
map . current . panTo ( nw ) ;
2022-02-07 03:00:14 +00:00
}
2022-02-11 17:40:27 +00:00
} ;
const bearing = map . current . getBearing ( ) ;
const pitch = map . current . getPitch ( ) ;
// This logic breaks with camera rotation / pitch
if ( bearing !== 0 || pitch !== 0 ) {
map . current . easeTo ( { bearing : 0 , pitch : 0 , duration : 300 } ) ;
setTimeout ( ( ) = > {
panTo ( ) ;
} , 300 ) ;
} else {
panTo ( ) ;
2022-02-07 03:00:14 +00:00
}
2022-02-11 17:40:27 +00:00
} ,
[ map ]
) ;
2022-02-07 03:00:14 +00:00
2022-02-11 17:40:27 +00:00
const selectCells = useCallback (
( cells : { x : number ; y : number } [ ] ) = > {
if ( ! map . current ) return ;
if ( map . current && ! map . current . getSource ( "grid-source" ) ) return ;
map . current . removeFeatureState ( { source : "selected-cell-source" } ) ;
2022-02-27 06:17:52 +00:00
map . current . removeFeatureState ( { source : "conflicted-cell-source" } ) ;
const visited : { [ id : number ] : boolean } = { } ;
2022-02-11 17:40:27 +00:00
for ( let cell of cells ) {
2022-02-27 06:17:52 +00:00
const id = ( cell . x + 57 ) * 1000 + 50 - cell . y ;
2022-02-11 17:40:27 +00:00
map . current . setFeatureState (
{
source : "selected-cell-source" ,
2022-02-27 06:17:52 +00:00
id ,
2022-02-11 17:40:27 +00:00
} ,
{
modSelected : true ,
cellSelected : false ,
}
) ;
2022-02-27 06:17:52 +00:00
map . current . setFeatureState (
{
source : "conflicted-cell-source" ,
id ,
} ,
{
conflicted : visited [ id ] === true ? true : false ,
}
) ;
visited [ id ] = true ;
2022-02-07 03:00:14 +00:00
}
2022-02-11 17:40:27 +00:00
let bounds : mapboxgl.LngLatBounds | null = null ;
const fitBounds = ( ) = > {
const zoom = map . current . getZoom ( ) ;
const viewportNW = map . current . project ( [ - 180 , 85.051129 ] ) ;
const cellSize = Math . pow ( 2 , zoom + 2 ) ;
for ( const cell of cells ) {
const x = cell . x + 57 ;
const y = 50 - cell . y ;
let ne = map . current . unproject ( [
x * cellSize + viewportNW . x + cellSize ,
y * cellSize + viewportNW . y ,
] ) ;
let sw = map . current . unproject ( [
x * cellSize + viewportNW . x ,
y * cellSize + viewportNW . y + cellSize ,
] ) ;
2022-02-07 03:00:14 +00:00
if ( bounds ) {
2022-02-11 17:40:27 +00:00
bounds . extend ( new mapboxgl . LngLatBounds ( sw , ne ) ) ;
} else {
bounds = new mapboxgl . LngLatBounds ( sw , ne ) ;
2022-02-07 03:00:14 +00:00
}
}
2022-02-11 17:40:27 +00:00
requestAnimationFrame ( ( ) = > {
if ( map . current ) {
map . current . resize ( ) ;
if ( bounds ) {
map . current . fitBounds ( bounds , { padding : 40 } ) ;
}
}
} ) ;
} ;
const bearing = map . current . getBearing ( ) ;
const pitch = map . current . getPitch ( ) ;
// This logic breaks with camera rotation / pitch
if ( bearing !== 0 || pitch !== 0 ) {
map . current . easeTo ( { bearing : 0 , pitch : 0 , duration : 300 } ) ;
setTimeout ( ( ) = > {
fitBounds ( ) ;
} , 300 ) ;
} else {
fitBounds ( ) ;
}
2022-02-07 03:00:14 +00:00
} ,
[ map ]
) ;
2022-01-24 05:59:36 +00:00
const selectCell = useCallback (
( cell ) = > {
router . push ( { query : { cell : cell.x + "," + cell . y } } ) ;
setSelectedCell ( cell ) ;
2022-02-27 06:17:52 +00:00
setSidebarOpen ( true ) ;
2022-01-24 05:59:36 +00:00
selectMapCell ( cell ) ;
} ,
[ setSelectedCell , selectMapCell , router ]
) ;
const clearSelectedCell = useCallback ( ( ) = > {
setSelectedCell ( null ) ;
if ( map . current ) map . current . removeFeatureState ( { source : "grid-source" } ) ;
2022-02-11 17:40:27 +00:00
if ( map . current ) {
map . current . removeFeatureState ( { source : "selected-cell-source" } ) ;
2022-02-27 06:17:52 +00:00
map . current . removeFeatureState ( { source : "conflicted-cell-source" } ) ;
2022-01-24 05:59:36 +00:00
}
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 ) ;
2022-03-15 04:22:08 +00:00
dispatch ( setFetchedPlugin ( undefined ) ) ;
2022-02-11 17:40:27 +00:00
if ( map . current ) {
map . current . removeFeatureState ( { source : "selected-cell-source" } ) ;
2022-02-27 06:17:52 +00:00
map . current . removeFeatureState ( { source : "conflicted-cell-source" } ) ;
2022-02-07 03:00:14 +00:00
}
requestAnimationFrame ( ( ) = > {
if ( map . current ) map . current . resize ( ) ;
} ) ;
2022-03-13 02:29:40 +00:00
} , [ map ] ) ;
2022-02-07 03:00:14 +00:00
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
2022-02-27 06:17:52 +00:00
const setSidebarOpenWithResize = useCallback (
( open ) = > {
setSidebarOpen ( open ) ;
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
) {
2022-02-11 17:40:27 +00:00
clearSelectedCells ( ) ;
2022-01-24 05:59:36 +00:00
selectCell ( cell ) ;
}
2022-03-03 04:23:06 +00:00
} else if ( router . query . mod && typeof router . query . mod === "string" ) {
if ( selectedCells ) {
clearSelectedCell ( ) ;
setSidebarOpen ( true ) ;
selectCells ( selectedCells ) ;
} else {
// TODO: this is so spaghetti
clearSelectedCell ( ) ;
}
2022-02-27 07:25:34 +00:00
} else if ( router . query . plugin && typeof router . query . plugin === "string" ) {
2022-03-15 03:33:37 +00:00
clearSelectedCell ( ) ;
2022-02-27 07:25:34 +00:00
setSidebarOpen ( true ) ;
if ( plugins && plugins . length > 0 && pluginsPending === 0 ) {
const plugin = plugins . find ( ( p ) = > p . hash === router . query . plugin ) ;
if ( plugin && plugin . parsed ) {
const cells = [ ] ;
const cellSet = new Set < number > ( ) ;
2022-03-12 23:34:45 +00:00
for ( const cell of plugin ? . parsed ? . cells ) {
2022-02-27 07:25:34 +00:00
if (
cell . x !== undefined &&
cell . y !== undefined &&
cell . world_form_id === 60 &&
cellSet . has ( cell . x + cell . y * 1000 ) === false
) {
cells . push ( { x : cell.x , y : cell.y } ) ;
cellSet . add ( cell . x + cell . y * 1000 ) ;
}
}
selectCells ( cells ) ;
}
2022-02-07 03:00:14 +00:00
}
2022-03-03 04:23:06 +00:00
} else if ( plugins && plugins . length > 0 && pluginsPending === 0 ) {
2022-02-27 06:17:52 +00:00
clearSelectedCells ( ) ;
2022-03-03 04:23:06 +00:00
clearSelectedCell ( ) ;
2022-02-27 06:17:52 +00:00
const cells = plugins . reduce (
( acc : { x : number ; y : number } [ ] , plugin : PluginFile ) = > {
if ( plugin . enabled && plugin . parsed ) {
const newCells = [ . . . acc ] ;
for ( const cell of plugin . parsed . cells ) {
if (
cell . x !== undefined &&
cell . y !== undefined &&
cell . world_form_id === 60
) {
newCells . push ( { x : cell.x , y : cell.y } ) ;
}
}
return newCells ;
}
return acc ;
} ,
[ ]
) ;
selectCells ( cells ) ;
2022-03-03 04:23:06 +00:00
} else {
clearSelectedCells ( ) ;
clearSelectedCell ( ) ;
2022-02-27 06:17:52 +00:00
}
2022-02-27 07:25:34 +00:00
} , [
2022-03-03 04:23:06 +00:00
selectedCell ,
selectedCells ,
2022-02-27 07:25:34 +00:00
router . query . cell ,
router . query . mod ,
router . query . plugin ,
2022-03-03 04:23:06 +00:00
selectCell ,
selectCells ,
clearSelectedCell ,
clearSelectedCells ,
heatmapLoaded ,
plugins ,
pluginsPending ,
2022-02-27 07:25:34 +00:00
] ) ;
2022-02-27 06:17:52 +00:00
2022-03-12 23:34:45 +00:00
useEffect ( ( ) = > {
2022-03-15 01:59:03 +00:00
if ( ! heatmapLoaded ) return ; // wait for all map layers to load
2022-03-15 01:55:52 +00:00
if (
router . query . plugin &&
typeof router . query . plugin === "string" &&
fetchedPlugin &&
fetchedPlugin . cells
) {
2022-03-12 23:34:45 +00:00
const cells = [ ] ;
const cellSet = new Set < number > ( ) ;
for ( const cell of fetchedPlugin . cells ) {
if (
cell . x !== undefined &&
cell . y !== undefined &&
cellSet . has ( cell . x + cell . y * 1000 ) === false
) {
cells . push ( { x : cell.x , y : cell.y } ) ;
cellSet . add ( cell . x + cell . y * 1000 ) ;
}
}
selectCells ( cells ) ;
}
2022-03-15 01:59:03 +00:00
} , [ heatmapLoaded , fetchedPlugin , selectCells , router . query . plugin ] ) ;
2022-03-12 23:34:45 +00:00
2022-03-03 04:23:06 +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-02-27 06:17:52 +00:00
const editCount = ( cellsData . data 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-02-27 06:17:52 +00:00
id : x * 1000 + 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"
) ;
2022-02-11 17:40:27 +00:00
const selectedCellLines : 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 ,
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 ,
] ) ;
selectedCellLines . features . push ( {
type : "Feature" ,
2022-02-27 06:17:52 +00:00
id : x * 1000 + y ,
2022-02-11 17:40:27 +00:00
geometry : {
type : "LineString" ,
coordinates : [
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
[ se . lng , se . lat ] ,
[ sw . lng , sw . lat ] ,
[ nw . lng , nw . lat ] ,
2022-02-27 06:17:52 +00:00
[ ne . lng , ne . lat ] ,
2022-02-11 17:40:27 +00:00
] ,
} ,
properties : { x : x , y : y } ,
} ) ;
}
}
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" : [
"case" ,
[ "boolean" , [ "feature-state" , "cellSelected" ] , false ] ,
"blue" ,
[ "boolean" , [ "feature-state" , "modSelected" ] , false ] ,
"purple" ,
"transparent" ,
] ,
"line-width" : [
"case" ,
[ "boolean" , [ "feature-state" , "modSelected" ] , false ] ,
4 ,
3 ,
] ,
} ,
2022-02-27 06:17:52 +00:00
layout : {
"line-join" : "round" ,
} ,
} ) ;
const conflictedCellLines : 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 ,
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 ,
] ) ;
conflictedCellLines . features . push ( {
type : "Feature" ,
id : x * 1000 + y ,
geometry : {
type : "LineString" ,
coordinates : [
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
[ se . lng , se . lat ] ,
[ sw . lng , sw . lat ] ,
[ nw . lng , nw . lat ] ,
[ ne . lng , ne . lat ] ,
] ,
} ,
properties : { x : x , y : y } ,
} ) ;
}
}
map . current . addSource ( "conflicted-cell-source" , {
type : "geojson" ,
data : conflictedCellLines ,
} ) ;
map . current . addLayer ( {
id : "conflicted-cell-layer" ,
type : "line" ,
source : "conflicted-cell-source" ,
paint : {
"line-color" : [
"case" ,
[ "boolean" , [ "feature-state" , "conflicted" ] , false ] ,
"red" ,
"transparent" ,
] ,
"line-width" : 4 ,
} ,
layout : {
"line-join" : "round" ,
} ,
2022-02-11 17:40:27 +00:00
} ) ;
2022-01-24 05:59:36 +00:00
const fullscreenControl = new mapboxgl . FullscreenControl ( ) ;
( fullscreenControl as unknown as { _container : HTMLElement } ) . _container =
mapWrapper . current ;
map . current . addControl ( fullscreenControl ) ;
2022-02-11 17:40:27 +00:00
map . current . addControl ( new mapboxgl . NavigationControl ( ) ) ;
2022-01-23 18:06:44 +00:00
2022-01-24 05:59:36 +00:00
map . current . on ( "click" , "grid-layer" , ( e ) = > {
const features = e . features ;
2022-02-11 17:40:27 +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 } } ) ;
}
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-30 21:55:50 +00:00
counts = { counts }
countsError = { countsError }
2022-02-27 06:17:52 +00:00
open = { sidebarOpen }
setOpen = { setSidebarOpenWithResize }
lastModified = { cellsData && cellsData . lastModified }
2022-01-24 00:39:36 +00:00
/ >
< ToggleLayersControl map = { map } / >
2022-02-27 06:17:52 +00:00
< SearchBar counts = { counts } sidebarOpen = { sidebarOpen } / >
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 ;