Skip to content

Commit

Permalink
Merge pull request #1 from tsupinie/develop
Browse files Browse the repository at this point in the history
v2.0
  • Loading branch information
tsupinie committed Aug 23, 2023
2 parents ce30df0 + 4a1b0ca commit f058d29
Show file tree
Hide file tree
Showing 39 changed files with 1,745 additions and 2,000 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
node_modules/
docs/
dist/
lib/

public/maptiler.js
public/maptiler.js
public/tiles
public/style.json
public/tiles.json
695 changes: 21 additions & 674 deletions LICENSE

Large diffs are not rendered by default.

63 changes: 34 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,50 @@
Hardware-accelerated geospatial data plotting in the browser

## Links
[Github](https://github.com/tsupinie/autumnplot-gl) | [API docs](https://tsupinie.github.io/autumnplot-gl/)
[Github](https://github.com/tsupinie/autumnplot-gl) | [API docs](https://tsupinie.github.io/autumnplot-gl/) | [NPM](https://www.npmjs.com/package/autumnplot-gl)

## What is this?
Lots of meteorological data web sites have a model where the data live on a central server, get plotted on the server, and then the server serves static images to the client. This creates a bottleneck where adding fields and view sectors takes exponentially more processing power for the server. One way around this is to offload the plotting to the client and to have the browser plot the data on a pan-and-zoomable map. Unfortunately, in the past, this has required developing low-level plotting code, and depending on the mapping library, the performance may be poor.

autumnplot-gl provides a solution to this problem by making hardware-accelerated data plotting in the browser easy. This was designed with meteorological data in mind, but anyone wanting to contour geospatial data on a map can use autumnplot-gl.

## Usage
autumnplot-gl is designed to be used with either [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/guides/) or [MapLibre GL JS](https://maplibre.org/maplibre-gl-js-docs/) mapping libraries. Pre-built autumnplot-gl javascript files area available [here](https://tsupinie.github.io/autumnplot-gl/dist/). Adding them to your page exposes the API via the `apgl` environment variable.
autumnplot-gl is designed to be used with either [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/guides/) or [MapLibre GL JS](https://maplibre.org/maplibre-gl-js-docs/) mapping libraries. If you're using webpack or another node-based build tool, you can install by running

```bash
npm i autumnplot-gl
```

Additionally, pre-built autumnplot-gl javascript files area available [here](https://tsupinie.github.io/autumnplot-gl/dist/). Adding them to your page exposes the API via the `apgl` global variable (e.g., instead of `new PlateCarreeGrid(...)` in the examples, you'd call `new apgl.PlateCarreeGrid(...)`).

### A basic contour plot
The first step in plotting data is to create a grid. Currently, the only supported grid is PlateCarree (a.k.a. Lat/Lon), but support for a Lambert Conformal conic grid is planned.
The first step in plotting data is to create a grid. Currently, the only supported grids are PlateCarree (a.k.a. Lat/Lon) and Lambert Conformal Conic.

```javascript
// Create a grid object that covers the continental United States
const nx = 121, ny = 61;
const grid = new apgl.PlateCarreeGrid(nx, ny, -130, 20, -65, 55);
const grid = new PlateCarreeGrid(nx, ny, -130, 20, -65, 55);
```

Next, create a RawScalarField with the data. autumnplot-gl doesn't care about how data get to the browser, but it should end up in a Float32Array in row-major order with the first element being at the southwest corner of the grid. A future version might include support for reading from, say, a Zarr file. Once you have your data in that format, to create the raw data field:

```javascript
// Create the raw data field
const height_field = new apgl.RawScalarField(grid, height_data);
const height_field = new RawScalarField(grid, height_data);
```

Next, to contour the field, create a Contour object and pass it some options. At this time, a somewhat limited set of options is supported, but I do plan to expand this.

```javascript
// Contour the data
const height_contour = new apgl.Contour(height_field, {color: '#000000', interval: 30});
const height_contour = new Contour(height_field, {color: '#000000', interval: 30});
```

Next, create the actual layer that gets added to the map. The first argument (`'height-contour'` here) is an id. It doesn't mean much, but it does need to be unique between the different `PlotLayer`s you add to the map.

```javascript
// Create the map layer
const height_layer = new apgl.PlotLayer('height-contour', height_contour);
const height_layer = new PlotLayer('height-contour', height_contour);
```

Finally, add it to the map. The interface for Mapbox and MapLibre are the same, at least currently, though there's nothing that says they'll stay that way in the future. Assuming you're using MapLibre:
Expand All @@ -61,29 +67,29 @@ The `'railway_transit_tunnel'` argument is a layer in the map style, and this me

### Barbs

Wind barb plotting is similar to the contours, but it requires u and v data.
Wind barb plotting is similar to the contours, but it requires using a `RawVectorField` with u and v data.

```javascript
const vector_field = {u: new apgl.RawScalarField(grid, u_data),
v: new apgl.RawScalarField(grid, v_data)}
const barbs = new apgl.Barbs(vector_field, {color: '#000000', thin_fac: 16});
const barb_layer = new apgl.PlotLayer('barbs', barbs);
const vector_field = new RawVectorField(grid, u_data, v_data);
const barbs = new Barbs(vector_field, {color: '#000000', thin_fac: 16});
const barb_layer = new PlotLayer('barbs', barbs);

map.on('load', () => {
map.addLayer(barb_layer, 'railway_transit_tunnel');
});
```

The density of the wind barbs is automatically varied based on the map zoom level. The `'thin_fac': 16` option means to plot every 16th wind barb in the i and j directions, and this is defined at zoom level 1. So at zoom level 2, it will plot every 8th wind barb, and at zoom level 3 every 4th wind barb, and so on. Because it divides in 2 for every deeper zoom level, `'thin_fac'` should be a power of 2.
The wind barbs are automatically rotated based on the grid projection. Also, the density of the wind barbs is automatically varied based on the map zoom level. The `'thin_fac': 16` option means to plot every 16th wind barb in the i and j directions, and this is defined at zoom level 1. So at zoom level 2, it will plot every 8th wind barb, and at zoom level 3 every 4th wind barb, and so on. Because it divides in 2 for every deeper zoom level, `'thin_fac'` should be a power of 2.

### Filled contours

Plotting filled contours is also similar to plotting regular contours, but there's some additional steps for the color map. A couple color maps are available by default (see [here](#built-in-color-maps) for more details), but if you have the colors you want, creating your own is (relatively) painless (hopefully). First, set up the colormap. Here, we'll just use the bluered colormap included by default.

```javascript
const colormap = apgl.colormaps.bluered(-10, 10, 20);
const fills = new apgl.ContourFilled(height, {cmap: colormap});
const height_fill_layer = new apgl.PlotLayer('height-fill', fills);
// colormaps is imported via `import {colormaps} from 'autumnplot-gl'`
const colormap = colormaps.bluered(-10, 10, 20);
const fills = new ContourFilled(height, {cmap: colormap});
const height_fill_layer = new PlotLayer('height-fill', fills);

map.on('load', () => {
map.addLayer(height_fill_layer, 'railway_transit_tunnel');
Expand All @@ -93,25 +99,25 @@ map.on('load', () => {
Normally, when you have color-filled contours, you have a color bar on the plot. To create an SVG color bar:

```javascript
const colorbar_svg = apgl.makeColorBar(colormap, {label: "Height Perturbation (m)",
ticks: [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10],
orientation: 'horizontal',
tick_direction: 'bottom'});
const colorbar_svg = makeColorBar(colormap, {label: "Height Perturbation (m)",
ticks: [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10],
orientation: 'horizontal',
tick_direction: 'bottom'});

document.getElementById('colorbar-container').appendChild(colorbar_svg);
```

### Varying the data plots
The previous steps have gone through plotting a static dataset on a map, but in many instances, you want to view a dataset that changes, say over time. Rather than continually remove and add new layers when the user changes the time, which would get tedious and probably wouldn't perform very well, autumnplot-gl provides `MultiPlotLayer`, which allows the plotted data to easily and quickly change over time (or height or any other axis that might be relevant).
The previous steps have gone through plotting a static dataset on a map, but in many instances, you want to view a dataset that changes, say over time. Rather than continually remove and add new layers when the user changes the time, which would get tedious, waste video RAM, and probably wouldn't perform very well, autumnplot-gl provides `MultiPlotLayer`, which allows the plotted data to easily and quickly change over time (or height or any other axis that might be relevant).

```javascript
// Contour some data
const height_contour_f00 = new apgl.Contour(grid, height_f00);
const height_contour_f01 = new apgl.Contour(grid, height_f01);
const height_contour_f02 = new apgl.Contour(grid, height_f02);
const height_contour_f00 = new Contour(grid, height_f00);
const height_contour_f01 = new Contour(grid, height_f01);
const height_contour_f02 = new Contour(grid, height_f02);

// Create a varying map layer
const height_layer_time = new apgl.MultiPlotLayer('height-contour-time');
const height_layer_time = new MultiPlotLayer('height-contour-time');

// Add the contoured data to it
height_layer_time.addField(height_contour_f00, '20230112_1200');
Expand All @@ -132,10 +138,10 @@ height_layer.setActiveKey('20230112_1200');
```

## Built-in color maps
autumnplot-gl comes with several built-in color maps, accessible from `apgl.colormaps`. These are basic blue/red and red/blue diverging color maps plus a selection from [PivotalWeather](https://www.pivotalweather.com). The blue/red and red/blue are functions that take a minimum contour level, a maximum contour level, and a number of colors. For example, this creates a blue/red colormap starting at -10, ending at 10, and with 20 colors:
autumnplot-gl comes with several built-in color maps, accessible via `import {colormaps} from 'autumnplot-gl'`. These are basic blue/red and red/blue diverging color maps plus a selection from [PivotalWeather](https://www.pivotalweather.com). The blue/red and red/blue are functions that take a minimum contour level, a maximum contour level, and a number of colors. For example, this creates a blue/red colormap starting at -10, ending at 10, and with 20 colors:

```javascript
const colormap = apgl.colormaps.bluered(-10, 10, 20);
const colormap = colormaps.bluered(-10, 10, 20);
```

Here are all the colormaps available:
Expand All @@ -148,8 +154,7 @@ The above exmple uses map tiles from [Maptiler](https://www.maptiler.com/). Map
So, I've created some [less-detailed map tiles](https://tsupinie.github.io/autumnplot-gl/tiles/) that are small enough that they can be hosted without dedicated hardware. However the tradeoff is that they're only useful down to zoom level 8 or 9 on the map, such that the viewport is somewhere between half a US state and a few counties in size. If that's good enough for you, then these tiles could be useful.

## Conspicuous absences
A few capabilities are missing from this library as of v1.0.
* Support for grids other than lat/lon grids. I plan to add this in the near future.
A few capabilities are missing from this library as of v2.0.
* Helper functions for reading from specific data formats. For instance, I'd like to add support for reading from a zarr file.
* A whole bunch of little things that ought to be fairly straightforward like tweaking the size of the wind barbs and contour thicknesses.
* Support for contour labeling. I'd like to add it, but I'm not really sure how I'd do it with the contours as I've implemented them. Any WebGL gurus, get in touch.
Expand Down
26 changes: 24 additions & 2 deletions package-lock.json

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

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
{
"name": "autumnplot-gl",
"version": "1.0",
"version": "2.0.0",
"description": "",
"main": "index.ts",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/**/*"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"docs": "npx typedoc --options typedoc.json",
"start": "webpack serve --open --mode=development",
"build": "webpack --mode=production"
"build-dist": "webpack --mode=production",
"build-npm": "./scripts/build_npm.sh"
},
"keywords": [],
"author": "Tim Supinie",
"license": "GPL-3.0-only",
"license": "MIT",
"devDependencies": {
"@types/luxon": "^3.2.0",
"@types/mapbox-gl": "^2.7.10",
Expand All @@ -26,6 +31,7 @@
"webpack-glsl-loader": "^1.0.1"
},
"dependencies": {
"autumn-wgl": "^1.1.0",
"comlink": "^4.3.1"
}
}
Binary file not shown.
Binary file not shown.
80 changes: 28 additions & 52 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,14 @@
<html>
<head>
<title>AutumnPlotGL</title>
<script src='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css' rel='stylesheet' />
<script src='https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css' rel='stylesheet' />
<script src="autumnplot-gl.js"></script>
<script src="maptiler.js"></script>
<script>
const nx = 121, ny = 61;
const grid = new apgl.PlateCarreeGrid(nx, ny, -130, 20, -65, 55);
let hght = [], u = [], v = [];
for (i = 0; i < nx; i++) {
for (j = 0; j < ny; j++) {
const idx = i + j * nx;
hght[idx] = 10 * (Math.cos(4 * Math.PI * i / (nx - 1)) * Math.cos(2 * Math.PI * j / (ny - 1)) - 0.05 * j);
u[idx] = 60 * (Math.cos(4 * Math.PI * i / (nx - 1)) * Math.sin(2 * Math.PI * j / (ny - 1)) + 0.5);
v[idx] = -60 * Math.sin(4 * Math.PI * i / (nx - 1)) * Math.cos(2 * Math.PI * j / (ny - 1));
}
}

const colormap = apgl.colormaps.pw_speed500mb;
const svg = apgl.makeColorBar(colormap, {label: "Wind Speed (kts)", fontface: 'Trebuchet MS',
ticks: [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140],
orientation: 'horizontal', tick_direction: 'bottom'});

const raw_hght_field = new apgl.RawScalarField(grid, new Float32Array(hght));
const raw_u_field = new apgl.RawScalarField(grid, new Float32Array(u));
const raw_v_field = new apgl.RawScalarField(grid, new Float32Array(v));
const raw_ws_field = apgl.RawScalarField.aggregateFields(Math.hypot, raw_u_field, raw_v_field);

const cntr = new apgl.Contour(raw_hght_field, {interval: 1, color: '#000000', thinner: zoom => zoom < 5 ? 2 : 1});
const filled = new apgl.ContourFill(raw_ws_field, {'cmap': colormap, 'opacity': 0.8});
const barbs = new apgl.Barbs({'u': raw_u_field, 'v': raw_v_field}, {color: '#000000', thin_fac: 16});

const hght_layer = new apgl.PlotLayer('height', cntr);
const ws_layer = new apgl.PlotLayer('wind-speed', filled);
const barb_layer = new apgl.PlotLayer('barbs', barbs);
</script>
<script src="main.js"></script>
<style>
body {
margin: 0px;
font-family: sans-serif;
}

div#colorbar.left {
Expand All @@ -63,7 +33,7 @@
width: 100%;
height: 67px;
z-index: 10;
background-color: #ffffffbb;
background-color: #ffffffcc;
}
div#colorbar.bottom svg {
position: absolute;
Expand All @@ -77,27 +47,33 @@
width: 100%;
height: 100vh;
}

div#selection {
z-index: 10;
display: inline-block;
position: absolute;
left: 0px;
top: 0px;
height: 1.8em;
background-color: #ffffffcc;
padding: 1em;
font-size: 1.1em;

border-bottom-right-radius: 10px;
}

div#selection select {
font-size: 0.9em;
}
</style>
</head>
<body>
<div id='selection'>
View:
<select>
</select>
</div>
<div id='colorbar' class="bottom"></div>
<div id='map'></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/dbc6e216-3574-4e78-93f9-ab90ff4905a4/style.json?key=' + MAPTILER_API_KEY, // stylesheet location
center: [-100.5, 35.5], // starting position [lng, lat]
zoom: 6 // starting zoom
});

map.on('load', () => {
//console.log(map.style.stylesheet.layers.map(lyr => lyr.id));
map.addLayer(ws_layer, 'aeroway');
map.addLayer(hght_layer, 'aeroway');
map.addLayer(barb_layer, 'aeroway');
});

document.getElementById('colorbar').appendChild(svg);
</script>
</body>
</html>

0 comments on commit f058d29

Please sign in to comment.