import { WxAPIControl } from "@metoceanapi/wxtiles-common/controls/WxAPIControl"; import { WxInfoControl } from "@metoceanapi/wxtiles-common/controls/WxInfoControl"; import { WxLegendControl } from "@metoceanapi/wxtiles-common/controls/WxLegendControl"; import { WxStyleEditorControl } from "@metoceanapi/wxtiles-common/controls/WxStyleEditorControl"; import { WxTimeControl } from "@metoceanapi/wxtiles-common/controls/WxTimeControl"; import { WXLOG, WxAPI, type WxColorStyleWeak, type WxTileSource } from "../src/index"; import { addControl, addLayer, addRaster, flyTo, initFrameWork, position, removeLayer, setURL, } from "./frwrkdeps"; const OPACITY = 0.7; // this is universal function for Leaflet and Mapbox. // Functions below are just framework specific wrappers for this universal function // start() is the fully interchangable function for Leaflet and Mapbox export async function start() { const map = await initFrameWork(); addRaster( map, "baseS", "baseL", "https://tile.openstreetmap.org/{z}/{x}/{y}.png", ); // addRaster(map, 'baseS', 'baseL', 'https://tiles.metoceanapi.com/base-lines/{z}/{x}/{y}', 5); // WxTilesLogging(console.trace); // const dataServerURL = 'data/'; // different sources manged in 'start' script in package.json // const dataServerURL = 'https://tilestest.metoceanapi.com/data/'; // const dataServerURL = 'http://localhost:9191/data/'; // const dataServerURL = 'https://68.171.214.87/data/'; // hihi1 // const dataServerURL = 'https://68.171.214.81/data/'; // hihi2 // const dataServerURL = 'https://hihi2.metoceanapi.com/data/'; const dataServerURL = "https://tiles.metoceanapi.com/data/"; const myHeaders = new Headers(); // myHeaders.append('x-api-key', 'SpV3J1RypVrv2qkcJE91gG'); const wxapi = new WxAPI({ dataServerURL, ext: "any", // wxtiles server doesn't bother about extensions, but in case regular NGINX servier you may need to set up proper extension. maskURL: "auto", maskChannel: "R", qtreeURL: "auto", requestInit: { headers: myHeaders }, }); // WxMultilayerManager DEMO // if (false) { // const addWxLayer = async (wxsource) => { // map.addSource(wxsource.id, wxsource); // //// Add wxlayer using CustomWxTilesLayer. Implements GLSL shader for vector field animation // map.addLayer( // new CustomWxTilesLayer(wxsource.id, wxsource.id, wxsource.opacity), // ); // await new Promise((resolve) => map.once("idle", resolve)); // }; // // Get the API ready - should be ONE per application // const dataServerURL = "https://tiles.metoceanapi.com/data/"; // const wxapi = new WxAPI({ // dataServerURL, // maskURL: "none", // qtreeURL: "none", // }); // const wxdatasetManager = await wxapi.createDatasetManager("gfs.global"); // const layerManager = new WxMultilayerManager(); // const layer1 = wxdatasetManager.createSourceLayer( // { variable: "air.visibility", time: 0 }, // { id: "wxsource1", opacity: 1.0 }, // ); // layerManager.addSource(layer1); // await addWxLayer(layer1); // const layer2 = wxdatasetManager.createSourceLayer( // { variable: "air.humidity.at-2m", time: 0 }, // { id: "wxsource2", opacity: 0.7 }, // ); // layerManager.addSource(layer2); // await addWxLayer(layer2); // const times = layer1.getAllTimes().slice(0, 10); // await layerManager.preloadTimes(times); // let t = 0; // const nextTimeStep = async () => { // await layerManager.setTime(t++ % times.length); // await always !! // setTimeout(nextTimeStep, 0); // }; // setTimeout(nextTimeStep); // return; // } let datasetName = "gfs.global"; /* 'mercator.global/'; */ /* 'gfs.global/'; */ /* 'obs-radar.rain.nzl.national/'; */ // let variable = 'air.temperature.at-2m'; // let variable = 'cloud.cover'; let variable = "wind.speed.eastward.at-10m"; // let variable = 'wave.direction.peak'; // let datasetName = 'ww3-ecmwf.global'; // let variable = 'wave.direction.mean'; // let datasetName = 'obs-radar.rain.nzl.national'; // let variable = 'reflectivity'; // let datasetName = 'him8_truecolor'; // let variable = 'h8_rgb'; // get datasetName from URL const urlParams = window.location.toString().split("##")[1]; const params = urlParams?.split("/"); datasetName = params?.[0] || datasetName; if (params?.[1]) variable = params[1]; let time = params?.[2] || ""; const zoom = (params && Number.parseFloat(params[3])) || 0; const lng = (params && Number.parseFloat(params[4])) || 0; const lat = (params && Number.parseFloat(params[5])) || 0; const bearing = (params && Number.parseFloat(params[6])) || 0; const pitch = (params && Number.parseFloat(params[7])) || 0; if (params?.length > 8) params[8] = params.slice(8).join("/"); const str = params?.[8] && params[8]; const sth = { style: {} as WxColorStyleWeak }; try { // get style from URL sth.style = str ? { ...JSON.parse(decodeURI(str)) } : {}; // reset levels if change units } catch (e) { /* ignore errors silently */ console.log("Can't parse style from URL"); } params && flyTo(map, zoom, lng, lat, bearing, pitch); // if no params stay at default position set in initFrameWork() map.on("zoom", () => setURL(map, time, datasetName, variable, sth.style)); map.on("drag", () => setURL(map, time, datasetName, variable, sth.style)); map.on("rotate", () => setURL(map, time, datasetName, variable, sth.style)); map.on("pitch", () => setURL(map, time, datasetName, variable, sth.style)); let wxsourceLayer: WxTileSource | undefined; const legendControl = new WxLegendControl(); addControl(map, legendControl, "top-right"); const frameworkOptions = { id: "wxsource", opacity: OPACITY, attribution: 'WxTiles DOCS', }; const apiControl = new WxAPIControl(wxapi, datasetName, variable); addControl(map, apiControl, "top-left"); apiControl.onchange = async ( _datasetName, _variable, resetStyleAndFlyTo = true, ): Promise => { datasetName = _datasetName; variable = _variable; WXLOG( "apiControl.onchange datasetName=", datasetName, "variable=", variable, ); // remove existing source and layer removeLayer(map, frameworkOptions.id, wxsourceLayer); // if (resetStyleAndFlyTo) { sth.style = {}; // reset style if change dataset/variable } wxsourceLayer = undefined; const wxdatasetManager = await wxapi.createDatasetManager(datasetName); const boundaries = wxdatasetManager.getBoundaries(); if (boundaries && resetStyleAndFlyTo) { const { east, west, north, south } = boundaries.boundariesnorm; const zoom = Math.round( Math.log( (360 * 360) / Math.max((east - west + 360) % 360, north - south) / 360, ) / Math.LN2, ); // from https://stackoverflow.com/questions/6048975/google-maps-v3-how-to-calculate-the-zoom-level-for-a-given-bounds flyTo(map, zoom, (east + west) / 2, (north + south) / 2, 0, 0); } const meta = wxdatasetManager.getVariableCurrentMeta(variable); if (meta?.units === "RGB") { const times = wxdatasetManager.getAllTimes(); addRaster( map, frameworkOptions.id, "wxtiles", wxdatasetManager.createURI(variable, times[0]), wxdatasetManager.getMaxZoom(), wxdatasetManager.getBoundaries()?.boundariesnorm, ); timeControl.setTimes(times); legendControl.clear(); } else { wxsourceLayer = wxdatasetManager.createSourceLayer( { variable, time, wxstyle: sth.style }, frameworkOptions, ); wxsourceLayer.setCoarseLevel(0); await addLayer(map, "wxtiles", wxsourceLayer); // wxsource.startAnimation(); const styleCopy = wxsourceLayer.getCurrentStyleObjectCopy(); legendControl.drawLegend(styleCopy); // first draw legend with current style styleCopy.levels = sth.style?.levels; // no need to show defaults it in the editor and URL styleCopy.colors = sth.style?.colors; // no need to show defaults it in the editor and URL await customStyleEditorControl.onchange?.(styleCopy, true); timeControl.updateSource(wxsourceLayer); } // apiControl.datasets.value = datasetName; // apiControl.variables.value = variable; }; const timeControl = new WxTimeControl(50); addControl(map, timeControl, "top-left"); timeControl.onchange = (time_) => { time = time_; setURL(map, time, datasetName, variable, sth.style); infoControl.update(wxsourceLayer, map); }; const customStyleEditorControl = new WxStyleEditorControl(); addControl(map, customStyleEditorControl, "top-right"); customStyleEditorControl.onchange = async (style, nonnativecall) => { WXLOG("customStyleEditorControl.onchange"); if (!wxsourceLayer) return; nonnativecall || (await wxsourceLayer.updateCurrentStyleObject(style)); // if called manually, do not update wxsource's style const nstyle = wxsourceLayer.getCurrentStyleObjectCopy(); legendControl.drawLegend(nstyle); nstyle.levels = style?.levels; // keep levels empty if they are not defined nstyle.colors = style?.colors; // keep colors empty if they are not defined customStyleEditorControl.setStyle(nstyle); // if called from wxsource, update customStyleEditorControl sth.style = nstyle; setURL(map, time, datasetName, variable, sth.style); }; const infoControl = new WxInfoControl(); addControl(map, infoControl, "bottom-left"); map.on("mousemove", (e) => infoControl.update(wxsourceLayer, map, position(e)), ); await apiControl.onchange(datasetName, variable, false); // initial load /*/ DEMO: more interactive - additional level and a bit of the red transparentness around the level made from current mouse position if (wxsource) { let busy = false; await wxsource.updateCurrentStyleObject({ levels: undefined }); // await always !! const levels = wxsource.getCurrentStyleObjectCopy().levels || []; // get current/default/any levels const colMap: [number, string][] = levels.map((level) => [level, '#' + Math.random().toString(16).slice(2, 8) + 'ff']); map.on('mousemove', async (e) => { if (!wxsource || busy) return; busy = true; const tileInfo: WxTileInfo | undefined = wxsource.getLayerInfoAtLatLon(position(e), map); if (tileInfo) { await customStyleEditorControl.onchange?.({ colorMap: [...colMap, [tileInfo.inStyleUnits[0], '#ff000000']] }); // await always !! } busy = false; }); } //*/ /*/ DEMO: abort if (wxsource) { const abortController = new AbortController(); console.log('setTime(5)'); const prom = wxsource.setTime(5, abortController); abortController.abort(); // aborts the request await prom; // await always !! even if aborted console.log('aborted'); await wxsource.setTime(5); // no abort console.log('setTime(5) done'); }//*/ /*/ DEMO: preload a timestep map.once('click', async () => { if (!wxsource) return; console.log('no preload time=5'); const t = Date.now(); await wxsource.setTime(5); // await always !! or abort console.log(Date.now() - t); await wxsource.preloadTime(10); // await always !! even if aborted await wxsource.preloadTime(20); // await always !! even if aborted console.log('preloaded timesteps 10, 20'); map.once('click', async () => { if (!wxsource) return; const t = Date.now(); await wxsource.setTime(10); // await always !! or abort console.log(Date.now() - t, ' step 10'); map.once('click', async () => { if (!wxsource) return; const t = Date.now(); await wxsource.setTime(20); // await always !! or abort console.log(Date.now() - t, ' step 20'); }); }); }); //*/ /*/ DEMO: change style's units let i = 0; map.on('click', async () => { if (!wxsource) return; const u = ['knots', 'm/s', 'km/h', 'miles/h']; await wxsource.updateCurrentStyleObject({ units: u[i], levels: undefined }); // levels: undefined - to recalculate levels legendControl.drawLegend(wxsource.getCurrentStyleObjectCopy()); i = (i + 1) % u.length; }); //*/ /*/ DEMO : read lon lat data map.on('mousemove', (e) => { if (!wxsource) return; const pos = position(e); // const tileInfo: WxTileInfo | undefined = wxsource.getLayerInfoAtLatLon(pos.wrap(), map); if (tileInfo) { console.log(tileInfo); } }); //*/ /*/ DEMO: timesteps let t = 0; const nextTimeStep = async () => { if (!wxsource) return; await wxsource.setTime(t++ % wxsource.wxdatasetManager.getTimes().length); // await always !! setTimeout(nextTimeStep, 0); }; setTimeout(nextTimeStep, 2000); //*/ /*/ DEMO: Dynamic blur effect / wxsource && (async function step(n: number = 0) { await wxsource.updateCurrentStyleObject({ isolineText: false, blurRadius: ~~(10 * Math.sin(n / 500) + 10) }); // await always !! requestAnimationFrame(step); })(); //*/ }