Skip to content

Commit

Permalink
Add CollectionDate retrieval for Bing
Browse files Browse the repository at this point in the history
Hidden behind beta flag in additional settings modal
Parses `X-Ve-Tilemeta-Capturedatesrange` header flag for all visible tiles from bing
  • Loading branch information
jo-chemla committed Jun 25, 2024
1 parent 686f3f7 commit bca2d77
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 9 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mapbox/mapbox-gl-geocoder": "^5.0.1",
"@math.gl/web-mercator": "^3.6.3",
"@mui/material": "^5.12.3",
"@mui/x-date-pickers": "^6.3.0",
"@tmcw/togeojson": "^5.6.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.7",
"mapbox-gl": "^2.14.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-map-gl": "^7.0.23",
"react-split": "^2.0.14",
"@tmcw/togeojson": "^5.6.0"
"react-split": "^2.0.14"
},
"devDependencies": {
"@types/react": "^18.0.28",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ControlPanel, { type MapSplitMode } from "./control-panel";
import { set, subMonths } from "date-fns";
import Split from "react-split";

import { ToggleButton, ToggleButtonGroup } from "@mui/material";
import { ToggleButton, ToggleButtonGroup, Button } from "@mui/material";

import {
planetBasemapUrl,
Expand Down Expand Up @@ -76,11 +76,11 @@ function App() {
// const [rightSelectedTms, setRightSelectedTms] = useState<BasemapsIds>(
// BasemapsIds.GoogleHybrid
// );
const [leftSelectedTms, setLeftSelectedTms] = useLocalStorage(
const [leftSelectedTms, setLeftSelectedTms]: [BasemapsIds, (e: BasemapsIds)=>void] = useLocalStorage(
"ui_leftSelectedTms",
BasemapsIds.PlanetMonthly
);
const [rightSelectedTms, setRightSelectedTms] = useLocalStorage(
const [rightSelectedTms, setRightSelectedTms]: [BasemapsIds, (e: BasemapsIds)=>void] = useLocalStorage(
"ui_rightSelectedTms",
BasemapsIds.GoogleHybrid
);
Expand Down Expand Up @@ -325,6 +325,8 @@ function App() {
onMove={activeMap === "left" ? onMoveDebounce : () => ({})}
style={LeftMapStyle}
mapStyle={leftMapboxMapStyle}
// transformRequest={transformRequest}

// projection={"naturalEarth"} // globe mercator naturalEarth equalEarth // TODO: eventually make projection controllable
>
<GeocoderControl
Expand Down Expand Up @@ -358,6 +360,9 @@ function App() {
maxzoom={basemapsTmsSources[leftSelectedTms].maxzoom || 20}
tileSize={256}
key={leftSelectedTms}
// https://github.com/maptiler/tilejson-spec/tree/custom-projection/2.2.0
// yandex is in CRS/SRS EPSG:3395 but mapbox source only supports CRS 3857 atm
// crs={"EPSG:3395"}
>
<Layer type="raster" layout={{}} paint={{}} />
</Source>
Expand Down
45 changes: 43 additions & 2 deletions src/control-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import { useState } from "react";
import { useState } from "react";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs from "dayjs";
Expand All @@ -22,6 +22,8 @@ import {
FormControl,
InputLabel,
MenuItem,
Button,
Tooltip,
} from "@mui/material";
import { differenceInMonths, eachMonthOfInterval, isValid } from "date-fns";
import {
Expand All @@ -35,6 +37,7 @@ import {
MIN_DATE,
MAX_DATE,
useLocalStorage,
getBingViewportDate,
// convertLatlonTo3857,
} from "./utilities";

Expand Down Expand Up @@ -152,6 +155,25 @@ function ControlPanel(props:any) {
const validMaxDate = maxDate && isValid(maxDate) ? maxDate : MAX_DATE;
const monthsCount = differenceInMonths(validMaxDate, validMinDate);
const marks = getSliderMarks(validMinDate, validMaxDate);
const [collectionDateStr, setCollectionDateStr] = useState('?');


const collectionDateRetrievable: BasemapsIds[] = [BasemapsIds.Bing, BasemapsIds.PlanetMonthly]
async function getCollectionDateViewport(selectedTms: BasemapsIds) {
let collectionDate = {minDate: '?', maxDate: '?'};
const map = props.mapRef?.current?.getMap() as any;

switch (+selectedTms) {
case BasemapsIds.Bing:
collectionDate = await getBingViewportDate(map)
break;

default:
console.log(`Cannot retrieve collection date for ${selectedTms}.`);
}
console.log('\n\nRETRIEVED COLLECTION DATE', collectionDate)
setCollectionDateStr(`${collectionDate?.minDate} - ${collectionDate?.maxDate}`)
}

// const [exportInterval, setExportInterval] = useState<number>(12);
// const [titilerEndpoint, setTitilerEndpoint] =
Expand All @@ -170,9 +192,13 @@ function ControlPanel(props:any) {
"export_maxFrameResolution",
MAX_FRAME_RESOLUTION
);
const [collectionDateActivated, setCollectionDateActivated] = useLocalStorage(
"collectionDateActivated",
true
);

const handleBasemapChange = (event: SelectChangeEvent) => {
props.setSelectedTms(event.target.value as string);
props.setSelectedTms(event.target.value as BasemapsIds); // as string
};
// ------------------------------------------
// HANDLE EXPORT SAVE TO DISK
Expand Down Expand Up @@ -418,6 +444,19 @@ function ControlPanel(props:any) {
download=""
/>
</>{" "}
{(( collectionDateActivated && props.selectedTms == BasemapsIds.Bing )) && (
// {(( collectionDateActivated && collectionDateRetrievable.includes((props.selectedTms as BasemapsIds)) )) && (
<Tooltip title={"Caution, Beta feature, only for Bing for now, Seems inacurate"}>
<Button
variant="outlined" // outlined or text
size="small"
onClick={() => {
getCollectionDateViewport(props.selectedTms)
}}>
Collection Date: {collectionDateStr}
</Button>
</Tooltip>
)}
<SettingsModal
playbackSpeedFPS={playbackSpeedFPS}
setPlaybackSpeedFPS={setPlaybackSpeedFPS}
Expand All @@ -432,6 +471,8 @@ function ControlPanel(props:any) {
setTitilerEndpoint={setTitilerEndpoint}
maxFrameResolution={maxFrameResolution}
setMaxFrameResolution={setMaxFrameResolution}
collectionDateActivated={collectionDateActivated}
setCollectionDateActivated={setCollectionDateActivated}
/>
</div>
{props.selectedTms == BasemapsIds.PlanetMonthly && (
Expand Down
2 changes: 1 addition & 1 deletion src/links-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function LinksSection(props: { mapRef: any }) {
</Link>
{") | ESRI "}
<Link
href={`https://livingatlas.arcgis.com/wayback/#active=37890&ext=${bounds?.getWest()},${bounds?.getSouth()},${bounds?.getEast()},${bounds?.getNorth()}`}
href={`https://livingatlas.arcgis.com/wayback/#active=37890&ext=${bounds?.getWest()},${bounds?.getSouth()},${bounds?.getEast()},${bounds?.getNorth()}&localChangesOnly=true`}
target={"_blank"}
>
Imagery Wayback Machine
Expand Down
22 changes: 21 additions & 1 deletion src/settings-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
Typography,
IconButton,
Box,
Tooltip,
Tooltip,
Checkbox,
FormControlLabel,
// CheckboxChangeEvent,
} from "@mui/material";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGear } from "@fortawesome/free-solid-svg-icons";
Expand All @@ -31,6 +34,12 @@ export default function SettingsModal(props: any) {
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const errorMessage = "start Date should be before end date!";

const handleCollectionDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log('handleCollectionDateChange',event, event.target, event.target.value, event.target.checked)
props.setCollectionDateActivated( event.target.checked);
};

return (
<Fragment>
<IconButton aria-label="delete" onClick={handleOpen} size={"small"}>
Expand Down Expand Up @@ -148,6 +157,17 @@ export default function SettingsModal(props: any) {
shrink: true,
}}
/>

<FormControlLabel
label="Activate Collection Date (Beta, caution)"
control={
<Checkbox
checked={props.collectionDateActivated}
onChange={handleCollectionDateChange}
inputProps={{ 'aria-label': 'controlled' }}
/>
}
/>
</Box>
</Modal>
</Fragment>
Expand Down
134 changes: 134 additions & 0 deletions src/utilities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from "date-fns";
import { useState, useEffect } from "react";

import { lngLatToWorld } from "@math.gl/web-mercator";

// Helper functions to convert between date for date-picker and slider-value
// Conversion between slider value and datepicker date
function sliderValToDate(val: number, minDate: Date) {
Expand Down Expand Up @@ -212,6 +214,137 @@ function objectsHaveSameKeys(...objects: any): boolean {
);
}


// For bing Aerial, can retrieve the imagery tile collection date min/max properties - and aggregate into a viewport min/max
// Could also be displayed on top of each tile, in a corner, so no aggregation needed.
// type mapboxgl.TransformRequestFunction = (url: string, resourceType: mapboxgl.ResourceType) => mapboxgl.RequestParameters
let numTilesLoaded = 0;
const tiles_dates = []
const transformRequest = function (
url: string,
resourceType: mapboxgl.ResourceType
) {
if (numTilesLoaded === undefined) numTilesLoaded = 0;
if (resourceType == "Tile") {
numTilesLoaded++;
console.log(
"numTilesLoaded from beginning/component load",
numTilesLoaded
);

if (url.includes("virtualearth.net")) getBingDatesFromUrl(url);
}
return { url };
} as mapboxgl.TransformRequestFunction;

function getBingDatesFromResponse(response: Response) {
const dates_str = response.headers
.get("X-Ve-Tilemeta-Capturedatesrange")
?.split("-");
const dates = dates_str?.map((s) => new Date(s));
return dates;
}
function getVisibleTilesXYZ(map: mapboxgl.Map, tileSize: number) {
const tiles = [];
const zoom = Math.floor(map.getZoom()) + 1;
const bounds = map.getBounds();
// const topLeft = map.project(bounds.getNorthWest());
// const bottomRight = map.project(bounds.getSouthEast());
const topLeft = lngLatToWorld(bounds.getNorthWest().toArray()).map(
(x) => (x / 512) * 2 ** zoom
);
const bottomRight = lngLatToWorld(bounds.getSouthEast().toArray()).map(
(x) => (x / 512) * 2 ** zoom
);
console.log("getVisibleTilesXYZ", map, zoom, bounds, topLeft, bottomRight);

for (
let x = Math.floor(topLeft[0]); // .x
x <= Math.floor(bottomRight[0]);
x++
) {
for (
let y = Math.floor(topLeft[1]); // .y
y >= Math.floor(bottomRight[1]);
y--
) {
tiles.push({ x, y, z: zoom });
}
}

return tiles;
}
function toQuad(x: number, y: number, z: number) {
var quadkey = "";
for (var i = z; i >= 0; --i) {
var bitmask = 1 << i;
var digit = 0;
if ((x & bitmask) !== 0) {
digit |= 1;
}
if ((y & bitmask) !== 0) {
digit |= 2;
}
quadkey += digit;
}
return quadkey;
}
function getBingUrl(quadkey: string) {
// return "https://t.ssl.ak.tiles.virtualearth.net/tiles/a12022010003311020210.jpeg?g=13578&n=z&prx=1";
return basemapsTmsSources[BasemapsIds.Bing].url.replace(
"{quadkey}",
quadkey
);
}
async function getBingDatesFromUrl(url: string) {
const dates = await fetch(url).then(function (response) {
// In the bing case, can look for a response header property
console.log(url, response.headers);
const dates = getBingDatesFromResponse(response);
console.log("getBingDatesFromUrl, in fetch", dates);
return dates;
});
return dates ?? "error on fetch ?";
}

async function getBingViewportDate(map: any) {
const urlArray = getVisibleTilesXYZ(map, 256); // source.tileSize)
console.log(urlArray);
const quadkeysArray = urlArray.map((xyz: any) => toQuad(xyz.x, xyz.y, xyz.z));
console.log(quadkeysArray);
const bingUrls = quadkeysArray.map((quadkey: string) => getBingUrl(quadkey));
console.log(bingUrls);

const promArray = bingUrls.map(async (url) => {
return await getBingDatesFromUrl(url);
});
// console.log("promArray", promArray);
// Promise.all(promArray).then((dates) => {
// console.log("after promise.all", dates);
// const minDate = Math.min(...(dates as any).map((d: number[]) => d[0]));
// const maxDate = Math.max(...(dates as any).map((d: number[]) => d[1]));
// console.log(dates, minDate, maxDate);
// document.a = dates;
// });

const tilesDates = await Promise.all(
bingUrls.map(async (url) => await getBingDatesFromUrl(url))
);
const minDate = new Date(
Math.min(...(tilesDates as any).map((d: number[]) => d[0]))
)
.toISOString()
.slice(0, 10);
const maxDate = new Date(
Math.max(...(tilesDates as any).map((d: number[]) => d[1]))
)
.toISOString()
.slice(0, 10);
console.log("yaya", tilesDates, "\n", minDate, maxDate);
return {minDate, maxDate}
}


export {
sliderValToDate,
dateToSliderVal,
Expand All @@ -223,4 +356,5 @@ export {
convertLatlonTo3857,
debounce,
useLocalStorage,
getBingViewportDate,
};

0 comments on commit bca2d77

Please sign in to comment.