@@ -3,276 +3,139 @@ import { useAPIResult } from "@macrostrat/ui-components";
33import {
44 MapAreaContainer ,
55 MapView ,
6+ buildInspectorStyle
67} from "@macrostrat/map-interface" ;
78import { mapboxAccessToken , matomoToken , matomoApiURL } from "@macrostrat-web/settings" ;
89import { Footer } from "~/components/general" ;
910import { Divider , Spinner , Tabs , Tab } from "@blueprintjs/core" ;
1011import { useEffect , useState } from "react" ;
12+ import { mergeStyles } from "@macrostrat/mapbox-utils" ;
13+ import { useDarkMode } from "@macrostrat/ui-components" ;
1114
1215
1316export function Page ( ) {
14- const [ data , setData ] = useState < Array < { lat : number ; lng : number } > | null > ( null ) ;
15-
16- useEffect ( ( ) => {
17- const fetchData = async ( ) => {
18- const result = await fetchAllUsageStats ( ) ;
19- setData ( result ) ;
20- } ;
21-
22- fetchData ( ) ;
23- } , [ ] ) ;
24-
2517 return h ( 'div.main' , [
2618 h ( 'div.heatmap-page' , [
27- h ( PageHeader , { coords : data } ) ,
28- h (
29- Tabs ,
30- {
31- id : 'heatmap-tabs' ,
32- } ,
33- [
34- h ( Tab , { id : 'today' , title : 'Today' , panelClassName : 'today-tab-panel' , panel : h ( TodayMap , { data } ) } ) ,
35- ]
36- )
19+ h ( PageHeader ) ,
20+ h ( HeatMap )
3721 ] ) ,
3822 h ( Footer )
3923 ] ) ;
4024}
4125
42- function PageHeader ( { coords } ) {
26+ function PageHeader ( ) {
4327 return h ( 'div.page-header' , [
4428 h ( 'h1' , 'Heatmap' ) ,
45- h . if ( coords ?. length ) ( 'h3' , `${ coords ?. length ?. toLocaleString ( ) } visits this year` ) ,
4629 h ( Divider ) ,
4730 h ( 'p' , 'This is a heatmap of all the locations where Macrostrat has been accessed.' ) ,
4831 h ( 'p' , 'The blue markers indicate today\'s accesses, while the grey markers indicate accesses from other days.' ) ,
4932 ] ) ;
5033}
5134
52- function AllMap ( { coords, today} ) {
53- if ( ! coords || ! today ) {
54- return h ( "div.map-area-container.loading" , [
55- h ( Spinner , { size : 50 } ) ,
56- ] ) ;
57- }
58-
59- const handleMapLoaded = ( map ) => {
60- map . on ( 'load' , ( ) => {
61- // Combine coords and today coords, marking today's points
62- const allFeatures = coords . map ( ( coord ) => ( {
63- type : 'Feature' ,
64- geometry : {
65- type : 'Point' ,
66- coordinates : [ coord . longitude , coord . latitude ] ,
67- } ,
68- properties : {
69- isToday : false ,
70- } ,
71- } ) ) . concat (
72- today . map ( ( coord ) => ( {
73- type : 'Feature' ,
74- geometry : {
75- type : 'Point' ,
76- coordinates : [ coord . longitude , coord . latitude ] ,
77- } ,
78- properties : {
79- isToday : true ,
80- } ,
81- } ) )
82- ) ;
83-
84- map . addSource ( 'markers' , {
85- type : 'geojson' ,
86- data : {
87- type : 'FeatureCollection' ,
88- features : allFeatures ,
89- } ,
90- } ) ;
91-
92- // Individual points - others (grey)
93- map . addLayer ( {
94- id : 'markers-other' ,
95- type : 'circle' ,
96- source : 'markers' ,
97- filter : [ 'all' , [ '!' , [ 'has' , 'point_count' ] ] , [ '==' , [ 'get' , 'isToday' ] , false ] ] ,
98- paint : {
99- 'circle-radius' : 2 ,
100- 'circle-color' : '#888' ,
101- } ,
102- } ) ;
103-
104- // Individual points - today (blue)
105- map . addLayer ( {
106- id : 'markers-today' ,
107- type : 'circle' ,
108- source : 'markers' ,
109- filter : [ 'all' , [ '!' , [ 'has' , 'point_count' ] ] , [ '==' , [ 'get' , 'isToday' ] , true ] ] ,
110- paint : {
111- 'circle-radius' : 3 ,
112- 'circle-color' : '#007cbf' ,
113- } ,
114- } ) ;
115- } ) ;
116- } ;
117-
118- return h ( MapInner , { handleMapLoaded } ) ;
119- }
120-
121- function MapInner ( { handleMapLoaded} ) {
122- const style = 'mapbox://styles/mapbox/dark-v10' ;
123-
124- return h (
125- MapAreaContainer ,
126- {
127- className : "map-area-container" ,
128- } ,
129- [
130- h ( MapView , {
131- style,
132- mapboxToken : mapboxAccessToken ,
133- onMapLoaded : handleMapLoaded ,
134- mapPosition : {
135- camera : {
136- lat : 39 ,
137- lng : - 98 ,
138- altitude : 6000000 ,
139- } ,
140- } ,
141- } ) ,
142- ]
143- ) ;
144- }
145-
146- function TodayMap ( { data} ) {
147- if ( ! data ) {
148- return h ( "div.map-area-container.loading" , [
149- h ( Spinner , { size : 50 } ) ,
150- ] ) ;
151- }
152-
153- const handleMapLoaded = ( map ) => {
154- map . on ( 'load' , ( ) => {
155- // Combine coords and today coords, marking today's points
156- const allFeatures = data . map ( ( coord ) => ( {
157- type : 'Feature' ,
158- geometry : {
159- type : 'Point' ,
160- coordinates : [ coord . lng , coord . lat ] ,
161- } ,
162- } ) )
163-
164- map . addSource ( 'markers' , {
165- type : 'geojson' ,
166- data : {
167- type : 'FeatureCollection' ,
168- features : allFeatures ,
169- } ,
170- } ) ;
171-
172- map . addLayer ( {
173- id : 'markers-today' ,
174- type : 'circle' ,
175- source : 'markers' ,
176- paint : {
177- 'circle-radius' : 3 ,
178- 'circle-color' : '#007cbf' ,
179- } ,
180- } ) ;
181- } ) ;
182- } ;
183-
184- return h ( MapInner , { handleMapLoaded } ) ;
35+ function todayStyle ( ) {
36+ return {
37+ sources : {
38+ today : {
39+ type : "vector" ,
40+ tiles : [ "http://localhost:8000/usage-stats/rockd/{z}/{x}/{y}?today=true" ] ,
41+ }
42+ } ,
43+ layers : [
44+ {
45+ id : 'today-points' ,
46+ type : 'circle' ,
47+ source : 'today' ,
48+ "source-layer" : "default" ,
49+ paint : {
50+ 'circle-color' : "#373ec4" ,
51+ 'circle-radius' : 4 ,
52+ }
53+ } ,
54+ ] ,
55+ } ;
18556}
18657
187- async function getAllCoords ( ) : Promise < Array < { latitude : number , longitude : number } > > {
188- const allCoords : Array < { latitude : number , longitude : number } > = [ ] ;
189- const pageSize = 10000 ;
190- let offset = 0 ;
191- let hasMore = true ;
192-
193- while ( hasMore ) {
194- const result = await fetch ( `${ matomoApiURL } ?${ new URLSearchParams ( {
195- date : '2025-07-01,today' ,
196- period : 'range' ,
197- filter_limit : pageSize . toString ( ) ,
198- filter_offset : offset . toString ( ) ,
199- showColumns : 'latitude,longitude' ,
200- doNotFetchActions : 'true' ,
201- module : 'API' ,
202- idSite : '1' ,
203- format : 'json' ,
204- token_auth : matomoToken ,
205- method : 'Live.getLastVisitsDetails' ,
206- } ) } `) . then ( res => res . json ( ) ) ;
207-
208- if ( Array . isArray ( result ) && result . length > 0 ) {
209- allCoords . push ( ...result ) ;
210- offset += pageSize ;
211- if ( result . length < pageSize ) {
212- hasMore = false ;
213- }
214- } else {
215- hasMore = false ;
58+ function allStyle ( ) {
59+ return {
60+ sources : {
61+ all : {
62+ type : "vector" ,
63+ tiles : [ "http://localhost:8000/usage-stats/rockd/{z}/{x}/{y}" ] ,
64+ }
65+ } ,
66+ layers : [
67+ {
68+ id : 'all-points' ,
69+ type : 'circle' ,
70+ source : 'all' ,
71+ "source-layer" : "default" ,
72+ paint : {
73+ 'circle-color' : "#838383" ,
74+ 'circle-radius' : 4 ,
21675 }
217- }
218-
219- return allCoords ;
76+ } ,
77+ ] ,
78+ } ;
22079}
22180
22281
223- function getTodayCoords ( ) : Array < { latitude : number , longitude : number } > | undefined {
224- return useAPIResult ( matomoApiURL , {
225- date : 'today' ,
226- period : 'day' ,
227- filter_limit : 10000 ,
228- filter_offset : 0 ,
229- module : 'API' ,
230- format : 'json' ,
231- showColumns : 'latitude,longitude' ,
232- doNotFetchActions : true ,
233- idSite : '1' ,
234- method : 'Live.getLastVisitsDetails' ,
235- token_auth : matomoToken
236- } )
237- }
82+ function HeatMap ( {
83+ mapboxToken,
84+ } : {
85+ headerElement ?: React . ReactElement ;
86+ title ?: string ;
87+ children ?: React . ReactNode ;
88+ mapboxToken ?: string ;
89+ } ) {
90+
91+ const style = useMapStyle ( ) ;
92+ if ( style == null ) return null ;
93+
94+ const mapPosition = {
95+ camera : {
96+ lat : 39 ,
97+ lng : - 98 ,
98+ altitude : 6000000 ,
99+ } ,
100+ } ;
238101
239- function getVisitsToday ( ) : { visits : number , visitors : number } | undefined {
240- return useAPIResult ( matomoApiURL , {
241- method : "Live.getCounters" ,
242- lastMinutes : 1440 ,
243- module : 'API' ,
244- format : 'json' ,
245- idSite : '1' ,
246- token_auth : matomoToken
247- } ) ?. [ 0 ]
102+ return h (
103+ "div.map-container" ,
104+ [
105+ // The Map Area Container
106+ h (
107+ MapAreaContainer ,
108+ {
109+ className : 'map-area-container' ,
110+ } ,
111+ [
112+ h ( MapView , { style, mapboxToken : mapboxAccessToken , mapPosition } ) ,
113+ ]
114+ ) ,
115+ ]
116+ ) ;
248117}
249118
250- async function fetchAllUsageStats ( ) {
251- let allData = [ ] ;
252- let lastId = 0 ;
253- const limit = 10000 ;
254- let keepFetching = true ;
255-
256- while ( keepFetching ) {
257- const url = `http://localhost:5500/usage-stats?id=${ lastId } &limit=${ limit } ` ;
258- const response = await fetch ( url ) ;
259- if ( ! response . ok ) {
260- throw new Error ( `Failed to fetch: ${ response . statusText } ` ) ;
261- }
119+ function useMapStyle ( ) {
120+ const dark = useDarkMode ( ) ;
121+ const isEnabled = dark ?. isEnabled ;
262122
263- const json = await response . json ( ) ;
264- const batch = json . data || [ ] ;
123+ const baseStyle = isEnabled
124+ ? "mapbox://styles/mapbox/dark-v10"
125+ : "mapbox://styles/mapbox/light-v10" ;
265126
266- allData = allData . concat ( batch ) ;
127+ const [ actualStyle , setActualStyle ] = useState ( null ) ;
128+ const overlayStyle = mergeStyles ( allStyle ( ) , todayStyle ( ) ) ; // OVERLAY
267129
268- if ( batch . length < limit ) {
269- // Less than limit means no more data
270- keepFetching = false ;
271- } else {
272- // Update lastId to the max id from this batch
273- lastId = batch . reduce ( ( max , item ) => ( item . id > max ? item . id : max ) , lastId ) ;
274- }
275- }
130+ // Auto select sample type
131+ useEffect ( ( ) => {
132+ buildInspectorStyle ( baseStyle , overlayStyle , {
133+ mapboxToken : mapboxAccessToken ,
134+ inDarkMode : isEnabled ,
135+ } ) . then ( ( s ) => {
136+ setActualStyle ( s ) ;
137+ } ) ;
138+ } , [ isEnabled ] ) ;
276139
277- return allData ;
140+ return actualStyle ;
278141}
0 commit comments