Add layer to show cells a selected cell edits

Also removes the link color to make links clearer.
This commit is contained in:
Tyler Hallada 2022-02-06 22:00:14 -05:00
parent 6005028439
commit 6d27db3122
8 changed files with 173 additions and 25 deletions

View File

@ -1,5 +1,6 @@
import { format } from "date-fns";
import React from "react";
import Link from "next/link";
import styles from "../styles/CellModList.module.css";
import type { Mod } from "./CellData";
@ -41,18 +42,27 @@ const CellModList: React.FC<Props> = ({ mods, counts }) => {
<li key={mod.id} className={styles["mod-list-item"]}>
<div className={styles["mod-title"]}>
<strong>
<a
href={`${NEXUS_MODS_URL}/mods/${mod.nexus_mod_id}`}
className={styles.link}
>
{mod.name}
</a>
<Link href={`/?mod=${mod.nexus_mod_id}`}>
<a className={styles.link}>{mod.name}</a>
</Link>
</strong>
</div>
<div>
<a
href={`${NEXUS_MODS_URL}/mods/${mod.nexus_mod_id}`}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
View on Nexus Mods
</a>
</div>
<div>
<strong>Category:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/mods/categories/${mod.category_id}`}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
{mod.category_name}
@ -62,6 +72,8 @@ const CellModList: React.FC<Props> = ({ mods, counts }) => {
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/users/${mod.author_id}`}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
{mod.author_name}

View File

@ -45,6 +45,13 @@ const Map: React.FC = () => {
x: number;
y: number;
} | null>(null);
const [selectedCells, setSelectedCells] = useState<
| {
x: number;
y: number;
}[]
| null
>(null);
const sidebarOpen = selectedCell !== null || router.query.mod !== undefined;
const { data: cellsData, error: cellsError } = useSWRImmutable(
@ -149,6 +156,96 @@ const Map: React.FC = () => {
[map]
);
const selectCells = useCallback(
(cells: { x: number; y: number }[]) => {
if (!map.current) return;
if (map.current && !map.current.getSource("grid-source")) return;
var zoom = map.current.getZoom();
var viewportNW = map.current.project([-180, 85.051129]);
var cellSize = Math.pow(2, zoom + 2);
const selectedCellsLines: GeoJSON.FeatureCollection<
GeoJSON.Geometry,
GeoJSON.GeoJsonProperties
> = {
type: "FeatureCollection",
features: [],
};
let bounds: mapboxgl.LngLatBounds | null = null;
for (const cell of cells) {
const x = cell.x + 57;
const y = 50 - cell.y;
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,
]);
if (bounds) {
bounds.extend(new mapboxgl.LngLatBounds(sw, ne));
} else {
bounds = new mapboxgl.LngLatBounds(sw, ne);
}
selectedCellsLines.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: [
[nw.lng, nw.lat],
[ne.lng, ne.lat],
[se.lng, se.lat],
[sw.lng, sw.lat],
[nw.lng, nw.lat],
],
},
properties: { x: x, y: y },
});
}
if (map.current.getLayer("selected-cells-layer")) {
map.current.removeLayer("selected-cells-layer");
}
if (map.current.getSource("selected-cells-source")) {
map.current.removeSource("selected-cells-source");
}
map.current.addSource("selected-cells-source", {
type: "geojson",
data: selectedCellsLines,
});
map.current.addLayer({
id: "selected-cells-layer",
type: "line",
source: "selected-cells-source",
paint: {
"line-color": "purple",
"line-width": 4,
},
});
requestAnimationFrame(() => {
if (map.current) {
map.current.resize();
if (bounds) {
map.current.fitBounds(bounds, { padding: 20 });
}
}
});
},
[map]
);
const selectCell = useCallback(
(cell) => {
router.push({ query: { cell: cell.x + "," + cell.y } });
@ -172,6 +269,19 @@ const Map: React.FC = () => {
});
}, [map]);
const clearSelectedCells = useCallback(() => {
setSelectedCells(null);
if (map.current && map.current.getLayer("selected-cells-layer")) {
map.current.removeLayer("selected-cells-layer");
}
if (map.current && map.current.getSource("selected-cells-source")) {
map.current.removeSource("selected-cells-source");
}
requestAnimationFrame(() => {
if (map.current) map.current.resize();
});
}, [map]);
const clearSelectedMod = useCallback(() => {
requestAnimationFrame(() => {
if (map.current) map.current.resize();
@ -207,6 +317,27 @@ const Map: React.FC = () => {
heatmapLoaded,
]);
useEffect(() => {
if (!heatmapLoaded) return; // wait for all map layers to load
if (
router.query.mod &&
typeof router.query.mod === "string" &&
selectedCells
) {
selectCells(selectedCells);
} else {
if (selectedCells) {
clearSelectedCells();
}
}
}, [
selectedCells,
router.query.mod,
selectCells,
clearSelectedCells,
heatmapLoaded,
]);
useEffect(() => {
if (!heatmapLoaded) return; // wait for all map layers to load
if (!router.query.mod || typeof router.query.mod !== "string") {
@ -500,6 +631,7 @@ const Map: React.FC = () => {
<Sidebar
selectedCell={selectedCell}
clearSelectedCell={() => router.push({ query: {} })}
setSelectedCells={setSelectedCells}
map={map}
counts={counts}
countsError={countsError}

View File

@ -47,9 +47,14 @@ const jsonFetcher = async (url: string): Promise<Mod | null> => {
type Props = {
selectedMod: number;
counts: Record<number, [number, number, number]> | null;
setSelectedCells: (cells: { x: number; y: number }[] | null) => void;
};
const ModData: React.FC<Props> = ({ selectedMod, counts }) => {
const ModData: React.FC<Props> = ({
selectedMod,
counts,
setSelectedCells,
}) => {
const { data, error } = useSWRImmutable(
`https://mods.modmapper.com/${selectedMod}.json`,
jsonFetcher
@ -71,12 +76,16 @@ const ModData: React.FC<Props> = ({ selectedMod, counts }) => {
const unique_downloads = modCounts ? modCounts[1] : 0;
const views = modCounts ? modCounts[2] : 0;
setSelectedCells(data.cells);
if (selectedMod && data) {
return (
<>
<h1>
<a
href={`${NEXUS_MODS_URL}/mods/${data.nexus_mod_id}`}
target="_blank"
rel="noreferrer noopener"
className={`${styles.link} ${styles.name}`}
>
{data.name}
@ -86,6 +95,8 @@ const ModData: React.FC<Props> = ({ selectedMod, counts }) => {
<strong>Category:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/mods/categories/${data.category_id}`}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
{data.category_name}
@ -95,6 +106,8 @@ const ModData: React.FC<Props> = ({ selectedMod, counts }) => {
<strong>Author:&nbsp;</strong>
<a
href={`${NEXUS_MODS_URL}/users/${data.author_id}`}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
{data.author_name}

View File

@ -16,6 +16,7 @@ interface Cell {
type Props = {
selectedCell: { x: number; y: number } | null;
clearSelectedCell: () => void;
setSelectedCells: (cells: { x: number; y: number }[] | null) => void;
map: React.MutableRefObject<mapboxgl.Map | null>;
counts: Record<number, [number, number, number]> | null;
countsError: Error | null;
@ -24,6 +25,7 @@ type Props = {
const Sidebar: React.FC<Props> = ({
selectedCell,
clearSelectedCell,
setSelectedCells,
counts,
countsError,
map,
@ -47,7 +49,13 @@ const Sidebar: React.FC<Props> = ({
if (countsError) return renderLoadError(countsError);
if (!counts) return renderLoading();
return <ModData selectedMod={selectedMod} counts={counts} />;
return (
<ModData
selectedMod={selectedMod}
counts={counts}
setSelectedCells={setSelectedCells}
/>
);
};
const onClose = () => {

View File

@ -10,7 +10,3 @@
.mod-title {
margin-bottom: 8px;
}
a.link {
color: #72030a;
}

View File

@ -10,7 +10,3 @@
.cell-title {
margin-bottom: 8px;
}
a.link {
color: #72030a;
}

View File

@ -2,10 +2,6 @@
margin-top: 24px;
}
a.link {
color: #72030a;
}
a.name {
line-height: 1.75rem;
}

View File

@ -9,11 +9,6 @@ div#__next {
height: 100%;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}