From b5b07ad2c2fd893afacaee2ecbeebbc67ddbaa7b Mon Sep 17 00:00:00 2001 From: mbthiery Date: Wed, 10 Jul 2024 13:14:21 -0400 Subject: [PATCH 01/67] Start new navbar --- src/app/new/HotspotsMap/Attribution.tsx | 21 + src/app/new/HotspotsMap/HexHotspotItem.tsx | 68 ++ src/app/new/HotspotsMap/HexHotspots.tsx | 113 +++ .../new/HotspotsMap/LoadingHexHotspots.tsx | 39 + .../new/HotspotsMap/NetworkCoverageLayer.tsx | 60 ++ src/app/new/HotspotsMap/index.tsx | 213 ++++++ src/app/new/HotspotsMap/mapLayersDark.tsx | 665 ++++++++++++++++++ src/app/new/HotspotsMap/mapLayersLight.tsx | 665 ++++++++++++++++++ src/app/new/HotspotsMap/utils.ts | 138 ++++ src/app/new/Nav.tsx | 89 +++ src/app/new/layout.tsx | 91 +++ src/app/new/page.tsx | 7 + src/components/Header/index.tsx | 1 + src/components/icons/DownCarret.tsx | 15 + src/components/icons/EnergyIcon.tsx | 17 + src/components/icons/HeliumIcon2.tsx | 19 + src/components/icons/HeliumIotIcon.tsx | 5 +- src/components/icons/IotIcon.tsx | 22 + src/components/icons/MobileIcon.tsx | 25 + 19 files changed, 2271 insertions(+), 2 deletions(-) create mode 100644 src/app/new/HotspotsMap/Attribution.tsx create mode 100644 src/app/new/HotspotsMap/HexHotspotItem.tsx create mode 100644 src/app/new/HotspotsMap/HexHotspots.tsx create mode 100644 src/app/new/HotspotsMap/LoadingHexHotspots.tsx create mode 100644 src/app/new/HotspotsMap/NetworkCoverageLayer.tsx create mode 100644 src/app/new/HotspotsMap/index.tsx create mode 100644 src/app/new/HotspotsMap/mapLayersDark.tsx create mode 100644 src/app/new/HotspotsMap/mapLayersLight.tsx create mode 100644 src/app/new/HotspotsMap/utils.ts create mode 100644 src/app/new/Nav.tsx create mode 100644 src/app/new/layout.tsx create mode 100644 src/app/new/page.tsx create mode 100644 src/components/icons/DownCarret.tsx create mode 100644 src/components/icons/EnergyIcon.tsx create mode 100644 src/components/icons/HeliumIcon2.tsx create mode 100644 src/components/icons/IotIcon.tsx create mode 100644 src/components/icons/MobileIcon.tsx diff --git a/src/app/new/HotspotsMap/Attribution.tsx b/src/app/new/HotspotsMap/Attribution.tsx new file mode 100644 index 0000000..6e52d03 --- /dev/null +++ b/src/app/new/HotspotsMap/Attribution.tsx @@ -0,0 +1,21 @@ +import clsx from "clsx" +import Link from "next/link" +import { HotspottyIcon } from "../icons/HotspottyIcon" + +export function Attribution({ className }: { className?: string }) { + return ( + +
+ + + Hotspotty + +
+ + ) +} diff --git a/src/app/new/HotspotsMap/HexHotspotItem.tsx b/src/app/new/HotspotsMap/HexHotspotItem.tsx new file mode 100644 index 0000000..6d6542a --- /dev/null +++ b/src/app/new/HotspotsMap/HexHotspotItem.tsx @@ -0,0 +1,68 @@ +"use client" + +import { usePreferences } from "@/context/usePreferences" +import animalHash from "angry-purple-tiger" +import Link from "next/link" +import { gaEvent } from "../GATracker" +import { HeliumIotIcon } from "../icons/HeliumIotIcon" +import { HeliumMobileIcon } from "../icons/HeliumMobileIcon" +import { Hotspot } from "./HexHotspots" + +type HexHotSpotItemProps = { + hotspot: Hotspot +} + +export const HexHotSpotItem = ({ hotspot }: HexHotSpotItemProps) => { + const { provider } = usePreferences() + + const hotspotName = animalHash(hotspot.hotspot_id) + const hasSmallCells = hotspot.cells.length > 0 + const Avatar = hasSmallCells ? HeliumMobileIcon : HeliumIotIcon + const subtitle = hasSmallCells + ? `${hotspot.cells.length} small cell${ + hotspot.cells.length === 1 ? "" : "s" + }` + : "IoT Hotspot" + + return ( +
  • +
    + { + if (!!provider) { + gaEvent({ + action: "outbound_click", + event: { + description: provider.label, + }, + }) + } + }} + > + +
  • + ) +} diff --git a/src/app/new/HotspotsMap/HexHotspots.tsx b/src/app/new/HotspotsMap/HexHotspots.tsx new file mode 100644 index 0000000..317855c --- /dev/null +++ b/src/app/new/HotspotsMap/HexHotspots.tsx @@ -0,0 +1,113 @@ +import { Tooltip } from "@/app/stats/components/Tooltip" +import { PreferencesProvider } from "@/context/usePreferences" +import clsx from "clsx" +import { HexHotSpotItem } from "./HexHotspotItem" + +interface SmallCell { + cell_id: string +} + +export interface Hotspot { + hotspot_id: string + active: boolean + cells: SmallCell[] +} + +interface HexData { + hex: string + resolution: number + hotspots: Hotspot[] +} + +const RECENT = "recently rewarded" +const NOT_RECENT = "not recently rewarded" + +const TOOLTIP_DESCRIPTIONS = { + [RECENT]: "A hotspot that has received rewards in the past 30 days.", + [NOT_RECENT]: + "A hotspot that has not received rewards in the past 30 days. Such a hotstop is most likely offline. It is also possible for it to be online but not rewarded if it is not transmitting data, not participating in PoC, or only recently online.", +} + +function getGroupedHotspots(hotspots: Hotspot[]) { + const groupedHotspots: { + [RECENT]: Hotspot[] + [NOT_RECENT]: Hotspot[] + } = { + [RECENT]: [], + [NOT_RECENT]: [], + } + + hotspots.forEach((hotspot) => { + const group = hotspot.active ? RECENT : NOT_RECENT + groupedHotspots[group].push(hotspot) + }) + + return groupedHotspots +} + +export async function HexHotspots({ hexId }: { hexId: string }) { + const { hotspots } = (await fetch( + `${process.env.NEXT_PUBLIC_HOTSPOTTY_EXPLORER_API_URL}/hex/${hexId}`, + { + headers: { + Authorization: `bearer ${process.env.NEXT_PUBLIC_HOTSPOTTY_EXPLORER_API_TOKEN}`, + }, + } + ).then((res) => res.json())) as HexData + + const groupedList = getGroupedHotspots(hotspots) + + if (hotspots.length === 0) { + return ( +
    + This hex contains no Hotspots. +
    + ) + } + + return ( +
    + {(Object.keys(groupedList) as Array).map( + (group) => { + if (groupedList[group].length === 0) return + return ( +
    +
    +
    + {group} + +
    + + {groupedList[group].length} Hotspots + +
    + +
      + {groupedList[group].map((hotspot) => ( + + ))} +
    +
    +
    + ) + } + )} +
    + ) +} diff --git a/src/app/new/HotspotsMap/LoadingHexHotspots.tsx b/src/app/new/HotspotsMap/LoadingHexHotspots.tsx new file mode 100644 index 0000000..348897f --- /dev/null +++ b/src/app/new/HotspotsMap/LoadingHexHotspots.tsx @@ -0,0 +1,39 @@ +import clsx from "clsx" + +export function LoadingHexHotspots({ count }: { count: number }) { + return ( +
    +
    + Active +
    + +
    + ) +} diff --git a/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx b/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx new file mode 100644 index 0000000..9560ab9 --- /dev/null +++ b/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx @@ -0,0 +1,60 @@ +import { useTheme } from "next-themes" +import { Fragment } from "react" +import { Layer, Source } from "react-map-gl" +import { + MIN_HEXES_ZOOM, + MIN_HEX_LABELS_ZOOM, + NetworkCoverageLayerOption, + POINTS_AND_HEXES_OVERLAP, + getBlurredPointStyle, + getHexFillStyle, + getHexLabelStyle, + hexLabelLayout, +} from "./utils" + +export function NetworkCoverageLayer({ + layer: { color, sourceDomain, points, hexes }, + ...props +}: { + layer: NetworkCoverageLayerOption +}) { + const { resolvedTheme } = useTheme() + return ( + + + + + + + + + + ) +} diff --git a/src/app/new/HotspotsMap/index.tsx b/src/app/new/HotspotsMap/index.tsx new file mode 100644 index 0000000..ad4500f --- /dev/null +++ b/src/app/new/HotspotsMap/index.tsx @@ -0,0 +1,213 @@ +"use client" + +import maplibregl from "maplibre-gl" +import "maplibre-gl/dist/maplibre-gl.css" +import { Protocol } from "pmtiles" + +import { cellToLatLng, cellsToMultiPolygon, getResolution } from "h3-js" +import { useTheme } from "next-themes" +import { + usePathname, + useRouter, + useSelectedLayoutSegment, + useSelectedLayoutSegments, +} from "next/navigation" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import Map, { + Layer, + MapLayerMouseEvent, + MapProvider, + MapRef, + MapStyle, + NavigationControl, + Source, + useMap, +} from "react-map-gl" +import { gaEvent } from "../GATracker" +import { Attribution } from "./Attribution" +import { NetworkCoverageLayer } from "./NetworkCoverageLayer" +import { mapLayersDark } from "./mapLayersDark" +import { mapLayersLight } from "./mapLayersLight" +import { + HexFeatureDetails, + INITIAL_MAP_VIEW_STATE, + MAP_CONTAINER_STYLE, + MAX_MAP_ZOOM, + MIN_MAP_ZOOM, + ZOOM_BY_HEX_RESOLUTION, + getHexOutlineStyle, + networkLayers, +} from "./utils" + +export function HotspotsMap({ children }: { children: React.ReactNode }) { + const { resolvedTheme } = useTheme() + const router = useRouter() + const pathname = usePathname() + const segments = useSelectedLayoutSegments() + const segment = useSelectedLayoutSegment() + const mapRef = useRef(null) + const [selectedHex, setSelectedHex] = useState(null) + const [cursor, setCursor] = useState("") + + useEffect(() => { + let protocol = new Protocol() + maplibregl.addProtocol("pmtiles", protocol.tile) + return () => { + maplibregl.removeProtocol("pmtiles") + } + }, []) + + const mapStyle = useMemo(() => { + const style: MapStyle = { + version: 8, + sources: { + protomaps: { + type: "vector", + tiles: [`${process.env.NEXT_PUBLIC_PMTILES_URL}/{z}/{x}/{y}.mvt`], + }, + }, + glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf", + layers: resolvedTheme === "dark" ? mapLayersDark : mapLayersLight, + } + return style + }, [resolvedTheme]) + + const selectHex = useCallback((hexId: string | null) => { + if (!hexId) { + setSelectedHex(null) + return + } + + const selectedHex = { + hexId, + geojson: { + type: "MultiPolygon", + coordinates: cellsToMultiPolygon([hexId], true), + } as GeoJSON.Geometry, + } + + setSelectedHex(selectedHex) + + if (!mapRef.current) return + const map = mapRef.current.getMap() + const [lat, lng] = cellToLatLng(hexId) + const bounds = map.getBounds() + const zoom = map.getZoom() + const hexResolution = getResolution(hexId) + const newZoom = ZOOM_BY_HEX_RESOLUTION[hexResolution] + if (zoom < newZoom - 3 || !bounds.contains([lng, lat])) { + // Fly to the hex if it's not visible in the current viewport, or if it's not zoomed in enough + map.flyTo({ center: [lng, lat], zoom: newZoom }) + } + }, []) + + const selectHexByPathname = useCallback(() => { + if (!mapRef.current) return + + if (segments.length === 2 && segments[0] === "hex") { + const hexId = segments[1] + if (selectedHex?.hexId !== hexId) { + selectHex(hexId) + } + } else if (pathname === "/" && selectedHex?.hexId) { + selectHex(null) + } + }, [pathname, segments, selectHex, selectedHex?.hexId]) + + useEffect(() => { + selectHexByPathname() + }, [selectHexByPathname]) + + const onClick = useCallback( + (event: MapLayerMouseEvent) => { + event.features?.forEach(({ layer, properties }) => { + if (layer.id !== "hexes_layer" || !properties?.id) return + if (selectedHex?.hexId === properties.id) { + router.push("/") + } else { + router.push(`/hex/${properties.id}`) + } + }) + }, + [router, selectedHex?.hexId] + ) + + useEffect(() => { + gaEvent({ action: "map_load" }) + }, []) + + const onMouseEnter = useCallback(() => setCursor("pointer"), []) + const onMouseLeave = useCallback(() => setCursor(""), []) + + return ( + + + {false && ( + + )} + {children} + + {segment !== "stats" && } + + {segment !== "mobile" && ( + + )} + + {selectedHex && ( + + + + )} + + + + ) +} + +const MapTest = () => { + const map = useMap() + const [currentZoom, setCurrentZoom] = useState(map.current?.getZoom()) + const zoom = map.current?.getZoom() + useEffect(() => { + const interval = setInterval(() => { + setCurrentZoom(map.current?.getZoom()) + }, 250) + return () => clearInterval(interval) + }, [map, setCurrentZoom]) + console.log(zoom) + + return ( +
    +

    Some random text

    + + +
    + ) +} diff --git a/src/app/new/HotspotsMap/mapLayersDark.tsx b/src/app/new/HotspotsMap/mapLayersDark.tsx new file mode 100644 index 0000000..4691d16 --- /dev/null +++ b/src/app/new/HotspotsMap/mapLayersDark.tsx @@ -0,0 +1,665 @@ +import { AnyLayer } from "react-map-gl" + +export const mapLayersDark: AnyLayer[] = [ + { + id: "background", + type: "background", + paint: { + "background-color": "#2A2A2A", + }, + }, + { + id: "earth", + type: "fill", + source: "protomaps", + "source-layer": "earth", + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_park", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "park"], ["==", "landuse", "cemetery"]], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_hospital", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "hospital"]], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_industrial", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "industrial"]], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_school", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "school"]], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "natural_wood", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: [ + "any", + ["==", "natural", "wood"], + ["==", "leisure", "nature_reserve"], + ["==", "landuse", "forest"], + ], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_pedestrian", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "highway", "footway"]], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "natural_glacier", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: ["==", "natural", "glacier"], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "natural_sand", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: ["==", "natural", "sand"], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "landuse_aerodrome", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["==", "aeroway", "aerodrome"], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "transit_runway", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["has", "aeroway"], + paint: { + "line-color": "#2A2A2A", + "line-width": 6, + }, + }, + { + id: "landuse_runway", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: [ + "any", + ["==", "aeroway", "runway"], + ["==", "area:aeroway", "runway"], + ["==", "area:aeroway", "taxiway"], + ], + paint: { + "fill-color": "#2A2A2A", + }, + }, + { + id: "water", + type: "fill", + source: "protomaps", + "source-layer": "water", + paint: { + "fill-color": "#202020", + }, + }, + { + id: "roads_tunnels_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#3D3D3D", + "line-dasharray": [1, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_tunnels_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_tunnels_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_tunnels_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_tunnels_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "physical_line_waterway", + type: "line", + source: "protomaps", + "source-layer": "physical_line", + filter: ["==", ["get", "pmap:kind"], "waterway"], + paint: { + "line-color": "#202020", + "line-width": 0.5, + }, + }, + { + id: "roads_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#3D3D3D", + "line-dasharray": [2, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: [ + "all", + ["==", "pmap:level", 0], + ["==", "pmap:kind", "medium_road"], + ], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "transit_railway", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["all", ["==", "pmap:kind", "railway"]], + paint: { + "line-color": "#3D3D3D", + "line-width": 2, + }, + }, + { + id: "transit_railway_tracks", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["all", ["==", "pmap:kind", "railway"]], + paint: { + "line-color": "#3D3D3D", + "line-width": 0.8, + "line-dasharray": [6, 10], + }, + }, + { + id: "boundaries_country", + type: "line", + source: "protomaps", + "source-layer": "boundaries", + filter: ["<=", "pmap:min_admin_level", 2], + paint: { + "line-color": "#696969", + "line-width": 1.5, + }, + }, + { + id: "boundaries", + type: "line", + source: "protomaps", + "source-layer": "boundaries", + filter: [">", "pmap:min_admin_level", 2], + paint: { + "line-color": "#696969", + "line-width": 1, + "line-dasharray": [1, 2], + }, + }, + { + id: "roads_bridges_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#3D3D3D", + "line-dasharray": [2, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_bridges_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_bridges_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_bridges_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_bridges_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#3D3D3D", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "physical_line_waterway_label", + type: "symbol", + source: "protomaps", + "source-layer": "physical_line", + minzoom: 14, + layout: { + "symbol-placement": "line", + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 10, + "text-letter-spacing": 0.3, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 1, + }, + }, + { + id: "roads_labels", + type: "symbol", + source: "protomaps", + "source-layer": "roads", + layout: { + "symbol-placement": "line", + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 12, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 2, + }, + }, + { + id: "mask", + type: "fill", + source: "protomaps", + "source-layer": "mask", + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "physical_point_ocean", + type: "symbol", + source: "protomaps", + "source-layer": "physical_point", + filter: ["any", ["==", "place", "sea"], ["==", "place", "ocean"]], + layout: { + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 13, + "text-letter-spacing": 0.2, + }, + paint: { + "text-color": "#6D6D6D", + }, + }, + { + id: "places_subplace", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "neighbourhood"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 10, + "text-transform": "uppercase", + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 0.5, + }, + }, + { + id: "places_city", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "city"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": ["step", ["get", "pmap:rank"], 0, 1, 12, 2, 10], + "text-variable-anchor": ["bottom-left"], + "text-radial-offset": 0.2, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 1, + }, + }, + { + id: "places_state", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "state"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 14, + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase", + "text-letter-spacing": 0.1, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 1, + }, + }, + { + id: "places_country", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "place", "country"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 10, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#151515", + "text-halo-width": 1, + }, + }, +] diff --git a/src/app/new/HotspotsMap/mapLayersLight.tsx b/src/app/new/HotspotsMap/mapLayersLight.tsx new file mode 100644 index 0000000..6981b29 --- /dev/null +++ b/src/app/new/HotspotsMap/mapLayersLight.tsx @@ -0,0 +1,665 @@ +import { AnyLayer } from "react-map-gl" + +export const mapLayersLight: AnyLayer[] = [ + { + id: "background", + type: "background", + paint: { + "background-color": "#F3F3F1", + }, + }, + { + id: "earth", + type: "fill", + source: "protomaps", + "source-layer": "earth", + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_park", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "park"], ["==", "landuse", "cemetery"]], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_hospital", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "hospital"]], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_industrial", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "industrial"]], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_school", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "pmap:kind", "school"]], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "natural_wood", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: [ + "any", + ["==", "natural", "wood"], + ["==", "leisure", "nature_reserve"], + ["==", "landuse", "forest"], + ], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_pedestrian", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["any", ["==", "highway", "footway"]], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "natural_glacier", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: ["==", "natural", "glacier"], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "natural_sand", + type: "fill", + source: "protomaps", + "source-layer": "natural", + filter: ["==", "natural", "sand"], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "landuse_aerodrome", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: ["==", "aeroway", "aerodrome"], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "transit_runway", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["has", "aeroway"], + paint: { + "line-color": "#F3F3F1", + "line-width": 6, + }, + }, + { + id: "landuse_runway", + type: "fill", + source: "protomaps", + "source-layer": "landuse", + filter: [ + "any", + ["==", "aeroway", "runway"], + ["==", "area:aeroway", "runway"], + ["==", "area:aeroway", "taxiway"], + ], + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "water", + type: "fill", + source: "protomaps", + "source-layer": "water", + paint: { + "fill-color": "#CAD2D3", + }, + }, + { + id: "roads_tunnels_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#fff", + "line-dasharray": [1, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_tunnels_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_tunnels_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_tunnels_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_tunnels_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "physical_line_waterway", + type: "line", + source: "protomaps", + "source-layer": "physical_line", + filter: ["==", ["get", "pmap:kind"], "waterway"], + paint: { + "line-color": "#fff", + "line-width": 0.5, + }, + }, + { + id: "roads_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#fff", + "line-dasharray": [2, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: [ + "all", + ["==", "pmap:level", 0], + ["==", "pmap:kind", "medium_road"], + ], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "transit_railway", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["all", ["==", "pmap:kind", "railway"]], + paint: { + "line-color": "#fff", + "line-width": 2, + }, + }, + { + id: "transit_railway_tracks", + type: "line", + source: "protomaps", + "source-layer": "transit", + filter: ["all", ["==", "pmap:kind", "railway"]], + paint: { + "line-color": "#fff", + "line-width": 0.8, + "line-dasharray": [6, 10], + }, + }, + { + id: "boundaries_country", + type: "line", + source: "protomaps", + "source-layer": "boundaries", + filter: ["<=", "pmap:min_admin_level", 2], + paint: { + "line-color": "#9e9e9e", + "line-width": 1, + }, + }, + { + id: "boundaries", + type: "line", + source: "protomaps", + "source-layer": "boundaries", + filter: [">", "pmap:min_admin_level", 2], + paint: { + "line-color": "#9e9e9e", + "line-width": 1, + "line-dasharray": [1, 2], + }, + }, + { + id: "roads_bridges_other", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]], + paint: { + "line-color": "#fff", + "line-dasharray": [2, 1], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 14, + 0, + 14.5, + 0.5, + 20, + 12, + ], + }, + }, + { + id: "roads_bridges_minor", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_bridges_medium", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, + { + id: "roads_bridges_major", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "major_road"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + }, + }, + { + id: "roads_bridges_highway", + type: "line", + source: "protomaps", + "source-layer": "roads", + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + paint: { + "line-color": "#fff", + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + }, + }, + { + id: "physical_line_waterway_label", + type: "symbol", + source: "protomaps", + "source-layer": "physical_line", + minzoom: 14, + layout: { + "symbol-placement": "line", + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 10, + "text-letter-spacing": 0.3, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#fff", + "text-halo-width": 1, + }, + }, + { + id: "roads_labels", + type: "symbol", + source: "protomaps", + "source-layer": "roads", + layout: { + "symbol-placement": "line", + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 12, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#fff", + "text-halo-width": 2, + }, + }, + { + id: "mask", + type: "fill", + source: "protomaps", + "source-layer": "mask", + paint: { + "fill-color": "#F3F3F1", + }, + }, + { + id: "physical_point_ocean", + type: "symbol", + source: "protomaps", + "source-layer": "physical_point", + filter: ["any", ["==", "place", "sea"], ["==", "place", "ocean"]], + layout: { + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 13, + "text-letter-spacing": 0.2, + }, + paint: { + "text-color": "#6D6D6D", + }, + }, + { + id: "places_subplace", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "neighbourhood"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 10, + "text-transform": "uppercase", + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#fff", + "text-halo-width": 0.5, + }, + }, + { + id: "places_city", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "city"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": ["step", ["get", "pmap:rank"], 0, 1, 12, 2, 10], + "text-variable-anchor": ["bottom-left"], + "text-radial-offset": 0.2, + }, + paint: { + "text-color": "#6D6D6D", + "text-halo-color": "#fff", + "text-halo-width": 1, + }, + }, + { + id: "places_state", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "pmap:kind", "state"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 14, + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase", + "text-letter-spacing": 0.1, + }, + paint: { + "text-color": "#A8A8A8", + "text-halo-color": "#fff", + "text-halo-width": 1, + }, + }, + { + id: "places_country", + type: "symbol", + source: "protomaps", + "source-layer": "places", + filter: ["==", "place", "country"], + layout: { + "text-field": "{name:en}", + "text-font": ["NotoSans-Regular"], + "text-size": 10, + }, + paint: { + "text-color": "#848484", + "text-halo-color": "#fff", + "text-halo-width": 1, + }, + }, +] diff --git a/src/app/new/HotspotsMap/utils.ts b/src/app/new/HotspotsMap/utils.ts new file mode 100644 index 0000000..737558c --- /dev/null +++ b/src/app/new/HotspotsMap/utils.ts @@ -0,0 +1,138 @@ +import { CoordPair } from "h3-js" +import { HeliumIotIcon } from "../icons/HeliumIotIcon" +import { HeliumMobileIcon } from "../icons/HeliumMobileIcon" + +export const MIN_MAP_ZOOM = 2 +export const MAX_MAP_ZOOM = 14 + +const WORLD_BOUNDS: [CoordPair, CoordPair] = [ + [-134.827109, 57.785781], + [129.767893, -30.955724], +] + +export const INITIAL_MAP_VIEW_STATE = { + bounds: WORLD_BOUNDS, +} + +export const MAP_CONTAINER_STYLE: React.CSSProperties = { + height: "100%", + width: "100%", + overflow: "hidden", + position: "relative", + backgroundColor: "rgb(19,24,37)", +} + +export const MIN_HEXES_ZOOM = 7 +export const MIN_HEX_LABELS_ZOOM = 11 +export const POINTS_AND_HEXES_OVERLAP = 2 + +export const HELIUM_IOT_COLOR = "#27EE76" +export const HELIUM_MOBILE_COLOR = "#009FF9" + +export const getHexFillStyle = (color: string): mapboxgl.FillPaint => ({ + "fill-color": color, + "fill-opacity": 0.4, +}) + +export const getBlurredPointStyle = (color: string): mapboxgl.CirclePaint => ({ + "circle-color": color, + "circle-opacity": [ + "interpolate", + ["exponential", 2], + ["zoom"], + MIN_MAP_ZOOM, + 0.05, + MIN_HEXES_ZOOM + POINTS_AND_HEXES_OVERLAP, + 0.4, + ], + "circle-radius": [ + "interpolate", + ["exponential", 2], + ["zoom"], + MIN_MAP_ZOOM, + 3, + MIN_HEXES_ZOOM + POINTS_AND_HEXES_OVERLAP, + 2, + ], +}) + +export const getHexOutlineStyle = ( + theme: string | undefined +): mapboxgl.LinePaint => ({ + "line-color": theme === "dark" ? "#fff" : "rgb(113,113,122)", + "line-width": 4, +}) + +export const getHexLabelStyle = ( + theme: string | undefined +): mapboxgl.SymbolPaint => ({ + "text-color": theme === "dark" ? "white" : "#6D6D6D", +}) + +export const hexLabelLayout: mapboxgl.SymbolLayout = { + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "count"], + "text-allow-overlap": false, + "text-size": 23, +} + +export interface HexFeatureDetails { + hexId: string + geojson: GeoJSON.Geometry +} + +export const ZOOM_BY_HEX_RESOLUTION: { [resolution: number]: number } = { + 10: 14, + 9: 14, + 8: 13, + 7: 12, + 6: 11, + 5: 10, + 4: 9, +} + +interface LayerConfig { + sourcePath: string + sourceLayer: string +} + +export interface NetworkCoverageLayerOption { + name: string + icon: (props: any) => JSX.Element + color: string + sourceDomain: string + points: LayerConfig + hexes: LayerConfig +} + +export const networkLayers: { [network: string]: NetworkCoverageLayerOption } = + { + mobile: { + name: "MOBILE", + icon: HeliumMobileIcon, + color: HELIUM_MOBILE_COLOR, + sourceDomain: process.env.NEXT_PUBLIC_HOTSPOTTY_TILESERVER_URL!, + points: { + sourcePath: "public.helium_mobile_points.json", + sourceLayer: "public.helium_mobile_points", + }, + hexes: { + sourcePath: "public.helium_mobile_hexes.json", + sourceLayer: "public.helium_mobile_hexes", + }, + }, + iot: { + name: "IOT", + icon: HeliumIotIcon, + color: HELIUM_IOT_COLOR, + sourceDomain: process.env.NEXT_PUBLIC_HOTSPOTTY_TILESERVER_URL!, + points: { + sourcePath: "public.helium_iot_points.json", + sourceLayer: "public.helium_iot_points", + }, + hexes: { + sourcePath: "public.helium_iot_hexes.json", + sourceLayer: "public.helium_iot_hexes", + }, + }, + } diff --git a/src/app/new/Nav.tsx b/src/app/new/Nav.tsx new file mode 100644 index 0000000..92c73a8 --- /dev/null +++ b/src/app/new/Nav.tsx @@ -0,0 +1,89 @@ +"use client" + +import { DownCarret } from "@/components/icons/DownCarret" +import { EnergyIcon } from "@/components/icons/EnergyIcon" +import { HeliumIcon2 } from "@/components/icons/HeliumIcon2" +import { IotIcon } from "@/components/icons/IotIcon" +import { MobileIcon } from "@/components/icons/MobileIcon" +import clsx from "clsx" +import Link from "next/link" +import { useState } from "react" + +const Logo = () => { + return ( + +
    + +
    + + ) +} + +const NETWORKS = [ + { + Icon: , + name: "IOT", + }, + { + Icon: , + name: "MOBILE", + }, + { + Icon: ( +
    + +
    + ), + name: "ENERGY", + }, +] + +const Selector = () => { + const [selected, setSelected] = useState(NETWORKS[0]) + const [showOptions, setShowOptions] = useState(false) + + return ( +
    + { + + } + +
    + {NETWORKS.map((network) => ( + + ))} +
    +
    + ) +} + +export const Nav = () => { + return ( + + ) +} diff --git a/src/app/new/layout.tsx b/src/app/new/layout.tsx new file mode 100644 index 0000000..dee4d92 --- /dev/null +++ b/src/app/new/layout.tsx @@ -0,0 +1,91 @@ +import { GAScript } from "@/components/GAScript" +import { GATracker } from "@/components/GATracker" +import { Header } from "@/components/Header" +import { HotspotsMap } from "@/components/HotspotsMap" +import { Providers } from "@/components/Providers" +import "@/styles/tailwind.css" +import "focus-visible" +import Head from "next/head" +import { Suspense } from "react" +import "react-tooltip/dist/react-tooltip.css" +import { Nav } from "./Nav" + +export const metadata = { + manifest: "/manifest.json", + themeColor: "#000000", + icons: { + icon: [ + { + url: "/favicon.ico", + type: "image/x-icon", + sizes: "64x64 32x32 24x24 16x16", + }, + { + url: "/logo192.png", + type: "image/png", + sizes: "192x192", + }, + { + url: "/logo512.png", + type: "image/png", + sizes: "512x512", + }, + ], + }, + openGraph: { + title: "Helium Explorer", + description: + "Helium Explorer is an open source network explorer for the Helium network", + url: "https://explorer.helium.com", + siteName: "Helium Explorer", + images: [ + { + url: "/og.png", + width: 954, + height: 696, + }, + ], + locale: "en-US", + type: "website", + }, +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + + + + + + + + {/* + Wrapping in supsense to avoid pages getting deopted into client-side rendering + https://nextjs.org/docs/messages/deopted-into-client-rendering + */} + + + +
    +