import { useState, useEffect, useRef } from "react";
import { renderToString } from "react-dom/server";
import { PMTiles, TileType } from "../../js/index";
import { Protocol } from "../../js/adapters";
import { styled } from "./stitches.config";
import maplibregl from "maplibre-gl";
import { MapGeoJSONFeature } from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { schemeSet3 } from "d3-scale-chromatic";
const MapContainer = styled("div", {
height: "calc(100vh - $4 - $4)",
});
const PopupContainer = styled("div", {
color: "black",
});
const FeatureRow = styled("div", {
marginBottom: "0.5em",
"&:not(:last-of-type)": {
borderBottom: "1px solid black",
},
});
const Hamburger = styled("div", {
position: "absolute",
top: "10px",
right: "10px",
width: 40,
height: 40,
backgroundColor: "#444",
cursor: "pointer",
zIndex: 9999,
});
const Options = styled("div", {
position: "absolute",
backgroundColor: "#444",
top: "50px",
right: "10px",
padding: "$1",
zIndex: 9999,
});
const FeaturesProperties = (props: { features: MapGeoJSONFeature[] }) => {
const fs = props.features.map((f, i) => {
let tmp: [string, string][] = [];
for (var key in f.properties) {
tmp.push([key, f.properties[key]]);
}
const rows = tmp.map((d, i) => (
| {d[0]} |
{d[1]} |
));
return (
{(f.layer as any)["source-layer"]}
);
});
return {fs};
};
const rasterStyle = (file: PMTiles) => {
return {
version: 8,
sources: {
source: {
type: "raster",
tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"],
maxzoom: 4,
},
},
layers: [
{
id: "raster",
type: "raster",
source: "source",
},
],
};
};
const vectorStyle = async (file: PMTiles): Promise => {
let header = await file.getHeader();
let metadata = await file.getMetadata();
let layers: any[] = [];
var tilestats: any;
var vector_layers: any;
if (metadata.json) {
let j = JSON.parse(metadata.json);
tilestats = j.tilestats;
vector_layers = j.vector_layers;
} else {
tilestats = metadata.tilestats;
vector_layers = metadata.vector_layers;
}
if (vector_layers) {
for (let [i, layer] of vector_layers.entries()) {
layers.push({
id: layer.id + "_fill",
type: "fill",
source: "source",
"source-layer": layer.id,
paint: {
"fill-color": schemeSet3[i % 12],
"fill-opacity": 0.2,
},
filter: ["==", ["geometry-type"], "Polygon"],
});
layers.push({
id: layer.id + "_stroke",
type: "line",
source: "source",
"source-layer": layer.id,
paint: {
"line-color": schemeSet3[i % 12],
"line-width": 0.5,
},
filter: ["==", ["geometry-type"], "LineString"],
});
layers.push({
id: layer.id + "_point",
type: "circle",
source: "source",
"source-layer": layer.id,
paint: {
"circle-color": schemeSet3[i % 12],
},
filter: ["==", ["geometry-type"], "Point"],
});
}
} else if (tilestats) {
// TODO deprecate me...
for (let [i, layer] of tilestats.layers.entries()) {
if (layer.geometry === "Polygon") {
layers.push({
id: layer.layer + "_fill",
type: "fill",
source: "source",
"source-layer": layer.layer,
paint: {
"fill-color": schemeSet3[i % 12],
"fill-opacity": 0.2,
},
});
} else if (layer.geometry === "LineString") {
layers.push({
id: layer.layer + "_stroke",
type: "line",
source: "source",
"source-layer": layer.layer,
paint: {
"line-color": schemeSet3[i % 12],
"line-width": 0.5,
},
});
} else {
layers.push({
id: layer.layer + "_point",
type: "circle",
source: "source",
"source-layer": layer.layer,
paint: {
"circle-color": schemeSet3[i % 12],
},
});
}
}
}
for (let layer of layers) {
if (layer["source-layer"] === "mask" && layer['type'] === 'fill') {
layer.paint["fill-color"] = "black";
layer.paint["fill-opacity"] = 0.8;
}
}
return {
version: 8,
sources: {
source: {
type: "vector",
tiles: ["pmtiles://" + file.source.getKey() + "/{z}/{x}/{y}"],
minzoom: header.minZoom,
maxzoom: header.maxZoom,
},
},
layers: layers,
};
};
function MaplibreMap(props: { file: PMTiles }) {
let mapContainerRef = useRef(null);
let [hamburgerOpen, setHamburgerOpen] = useState(true);
let [showAttributes, setShowAttributes] = useState(false);
let [showTileBoundaries, setShowTileBoundaries] = useState(false);
const mapRef = useRef(null);
// make it accessible in hook
const showAttributesRef = useRef(showAttributes);
useEffect(() => {
showAttributesRef.current = showAttributes;
});
const toggleHamburger = () => {
setHamburgerOpen(!hamburgerOpen);
};
const toggleShowAttributes = () => {
setShowAttributes(!showAttributes);
};
const toggleShowTileBoundaries = () => {
setShowTileBoundaries(!showTileBoundaries);
mapRef.current!.showTileBoundaries = !showTileBoundaries;
};
useEffect(() => {
let protocol = new Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
protocol.add(props.file); // this is necessary for non-HTTP sources
const map = new maplibregl.Map({
container: mapContainerRef.current!,
hash: "map",
zoom: 0,
center: [0, 0],
style: {
version: 8,
sources: {},
layers: [],
},
});
map.addControl(new maplibregl.NavigationControl({}), "bottom-left");
map.on("load", map.resize);
const popup = new maplibregl.Popup({
closeButton: false,
closeOnClick: false,
});
mapRef.current = map;
map.on("mousemove", (e) => {
if (!showAttributesRef.current) {
popup.remove();
return;
}
var bbox = e.point;
var features = map.queryRenderedFeatures(bbox);
map.getCanvas().style.cursor = features.length ? "pointer" : "";
let content = renderToString();
if (!features.length) {
popup.remove();
} else {
popup.setHTML(content);
popup.setLngLat(e.lngLat);
popup.addTo(map);
}
});
return () => {
map.remove();
};
}, []);
useEffect(() => {
let initStyle = async () => {
if (mapRef.current) {
let map = mapRef.current;
let header = await props.file.getHeader();
map.fitBounds(
[
[header.minLon, header.minLat],
[header.maxLon, header.maxLat],
],
{ animate: false }
);
let style: any; // TODO maplibre types (not any)
if (
header.tileType === TileType.Png ||
header.tileType == TileType.Jpeg
) {
map.setStyle(rasterStyle(props.file) as any);
} else {
let style = await vectorStyle(props.file);
map.setStyle(style);
}
}
};
initStyle();
}, []);
return (
menu
{hamburgerOpen ? (
Filter
Popup
Tiles
) : null}
);
}
export default MaplibreMap;