Skip to content

Commit 45c3a78

Browse files
authored
Merge pull request #60 from UW-Macrostrat/heatmap
Heatmap
2 parents ef8e694 + 70daf21 commit 45c3a78

File tree

6 files changed

+430
-231
lines changed

6 files changed

+430
-231
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ VITE_MACROSTRAT_TILESERVER_DOMAIN='https://tileserver.staging.svc.macrostrat.org
44
VITE_MACROSTRAT_INSTANCE='Development'
55
VITE_MACROSTRAT_API_DOMAIN='https://macrostrat.org'
66
VITE_ROCKD_API_URL='https://dev.rockd.org/api/v2/'
7+
VITE_MATOMO_API_TOKEN='<your-matomo-api-token>'
78

89
# Needed for map ingestion system
910
# SECRET_KEY='Replace with api signing key'

packages/settings/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const ingestPrefix = getRuntimeConfig(
2727
apiDomain + "/api/ingest"
2828
);
2929

30+
export const matomoToken = getRuntimeConfig("MATOMO_API_TOKEN");
31+
3032
/** Legacy settings object */
3133
export const SETTINGS = {
3234
rockdApiURL,
@@ -36,4 +38,5 @@ export const SETTINGS = {
3638
burwellTileDomain,
3739
mapboxAccessToken,
3840
rockdApiOldURL,
41+
matomoToken,
3942
};

pages/heatmap/+Page.client.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import h from "./main.module.sass";
2+
import { useAPIResult } from "@macrostrat/ui-components";
3+
import {
4+
MapAreaContainer,
5+
MapView,
6+
} from "@macrostrat/map-interface";
7+
import { mapboxAccessToken } from "@macrostrat-web/settings";
8+
import { Footer } from "~/components/general";
9+
import { Divider, Spinner } from "@blueprintjs/core";
10+
11+
export function Page() {
12+
const coords = getAllCoords();
13+
14+
return h('div.main', [
15+
h('div.heatmap-page', [
16+
h(PageHeader, { coords }),
17+
h(Map, { coords })
18+
]),
19+
h(Footer)
20+
])
21+
}
22+
23+
function PageHeader({ coords }) {
24+
const visitsToday = getVisitsToday();
25+
26+
const { visits, visitors } = visitsToday || {};
27+
28+
const Visit = !visitsToday ?
29+
h('p', 'Loading visits...') :
30+
h('div.visits-today', [
31+
h('h3', `${visits.toLocaleString()} visits today`),
32+
h.if(coords?.length)('h3', `${coords?.length.toLocaleString()} visits this year`),
33+
])
34+
35+
return h('div.page-header', [
36+
h('h1', 'Heatmap'),
37+
Visit,
38+
h(Divider),
39+
h('p', 'This is a heatmap of all the locations where Macrostrat has been accessed.'),
40+
h('p', 'The blue markers indicate today\'s accesses, while the grey markers indicate accesses from other days.'),
41+
]);
42+
}
43+
44+
function Map({coords}) {
45+
const today = getTodayCoords();
46+
47+
const style = 'mapbox://styles/mapbox/dark-v10';
48+
49+
if (!coords || !today) {
50+
return h("div.map-area-container.loading", [
51+
h(Spinner, { size: 50 }),
52+
]);
53+
}
54+
55+
const handleMapLoaded = (map) => {
56+
map.on('load', () => {
57+
// Combine coords and today coords, marking today's points
58+
const allFeatures = coords.map((coord) => ({
59+
type: 'Feature',
60+
geometry: {
61+
type: 'Point',
62+
coordinates: [coord.longitude, coord.latitude],
63+
},
64+
properties: {
65+
isToday: false,
66+
},
67+
})).concat(
68+
today.map((coord) => ({
69+
type: 'Feature',
70+
geometry: {
71+
type: 'Point',
72+
coordinates: [coord.longitude, coord.latitude],
73+
},
74+
properties: {
75+
isToday: true,
76+
},
77+
}))
78+
);
79+
80+
map.addSource('markers', {
81+
type: 'geojson',
82+
data: {
83+
type: 'FeatureCollection',
84+
features: allFeatures,
85+
},
86+
});
87+
88+
// Individual points - others (grey)
89+
map.addLayer({
90+
id: 'markers-other',
91+
type: 'circle',
92+
source: 'markers',
93+
filter: ['all', ['!', ['has', 'point_count']], ['==', ['get', 'isToday'], false]],
94+
paint: {
95+
'circle-radius': 2,
96+
'circle-color': '#888',
97+
},
98+
});
99+
100+
// Individual points - today (blue)
101+
map.addLayer({
102+
id: 'markers-today',
103+
type: 'circle',
104+
source: 'markers',
105+
filter: ['all', ['!', ['has', 'point_count']], ['==', ['get', 'isToday'], true]],
106+
paint: {
107+
'circle-radius': 3,
108+
'circle-color': '#007cbf',
109+
},
110+
});
111+
});
112+
};
113+
114+
return h(
115+
"div.map-container",
116+
[
117+
h(
118+
MapAreaContainer,
119+
{
120+
className: "map-area-container",
121+
},
122+
[
123+
h(MapView, {
124+
style,
125+
mapboxToken: mapboxAccessToken,
126+
onMapLoaded: handleMapLoaded,
127+
mapPosition: {
128+
camera: {
129+
lat: 39,
130+
lng: -98,
131+
altitude: 6000000,
132+
},
133+
},
134+
}),
135+
]),
136+
]
137+
);
138+
}
139+
140+
function getAllCoords() {
141+
return useAPIResult('/api/matomo', {
142+
date: '2025-01-01,today',
143+
period: 'range',
144+
filter_limit: 10000,
145+
filter_offset: 0,
146+
showColumns: 'latitude,longitude',
147+
doNotFetchActions: true,
148+
})
149+
}
150+
151+
function getTodayCoords(): Array<{ latitude: number, longitude: number }> | undefined {
152+
return useAPIResult('/api/matomo', {
153+
date: 'today',
154+
period: 'day',
155+
filter_limit: 10000,
156+
filter_offset: 0,
157+
showColumns: 'latitude,longitude',
158+
doNotFetchActions: true,
159+
})
160+
}
161+
162+
function getVisitsToday(): { visits: number, visitors: number } | undefined {
163+
return useAPIResult('/api/matomo', {
164+
method: "Live.getCounters",
165+
idSite: 1,
166+
lastMinutes: 1440
167+
})?.[0]
168+
}

pages/heatmap/main.module.sass

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.heatmap-page
2+
margin: 2em 20%
3+
4+
#map
5+
height: 100%
6+
width: 100%
7+
aspect-ratio: 3/2
8+
cursor: pointer
9+
z-index: 100
10+
11+
.map-area-container
12+
width: 100% !important
13+
height: 50vh !important
14+
15+
.loading
16+
padding-top: 100px

server/index.ts

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,20 +95,38 @@ async function startServer() {
9595
process.env.VITE_MACROSTRAT_INSTANCE
9696
);
9797

98-
/*
99-
// Redirect from /trip/:trip to /trip with query parameter
100-
app.get("/trip/:trip", (req, res) => {
101-
const { trip } = req.params;
102-
res.redirect(`/trip?trip=${trip}`);
103-
});
98+
99+
app.get('/api/matomo', async (req, res) => {
100+
const { date, period, filter_limit, filter_offset, lastMinutes = null, doNotFetchActions, method = 'Live.getLastVisitsDetails'} = req.query;
101+
102+
const baseUrl = 'https://analytics.svc.macrostrat.org/';
103+
const params = {
104+
module: 'API',
105+
idSite: '1',
106+
period,
107+
method,
108+
date,
109+
format: 'json',
110+
filter_limit,
111+
lastMinutes,
112+
token_auth: process.env.VITE_MATOMO_API_TOKEN
113+
};
114+
115+
// Conditionally add doNotFetchActions param
116+
if (doNotFetchActions) {
117+
params.doNotFetchActions = '1';
118+
}
119+
120+
const matomoUrl = `${baseUrl}?${new URLSearchParams(params).toString()}`;
104121

105-
// Redirect from /checkin/:checkin to /checkin with query parameter
106-
app.get("/checkin/:checkin", (req, res) => {
107-
const { checkin } = req.params;
108-
// Redirect to /test with query parameter `id`
109-
res.redirect(`/checkin?checkin=${checkin}`);
122+
try {
123+
const response = await fetch(matomoUrl);
124+
const data = await response.json();
125+
res.json(data);
126+
} catch (err) {
127+
res.status(500).json({ error: 'Matomo fetch failed', details: err });
128+
}
110129
});
111-
*/
112130

113131
/**
114132
* Vike route

0 commit comments

Comments
 (0)