mapbox-exif-layer 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,58 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Zifan Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ISC License
24
+
25
+ Copyright (c) 2016, Mapbox
26
+
27
+ Permission to use, copy, modify, and/or distribute this software for any purpose
28
+ with or without fee is hereby granted, provided that the above copyright notice
29
+ and this permission notice appear in all copies.
30
+
31
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
32
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
33
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
34
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
35
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
36
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
37
+ THIS SOFTWARE.
38
+
39
+ The MIT License (MIT)
40
+
41
+ Copyright (c) 2017 sakitam-fdd
42
+
43
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
44
+ this software and associated documentation files (the "Software"), to deal in
45
+ the Software without restriction, including without limitation the rights to
46
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
47
+ the Software, and to permit persons to whom the Software is furnished to do so,
48
+ subject to the following conditions:
49
+
50
+ The above copyright notice and this permission notice shall be included in all
51
+ copies or substantial portions of the Software.
52
+
53
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
54
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
55
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
56
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
57
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
58
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # Mapbox EXIF Layer
2
+
3
+ Custom Mapbox GL JS layers for rendering particle motion (e.g., wind) or smooth raster (e.g., temperature, relative humidity, precipitation) based on EXIF-enabled JPEG images
4
+
5
+ **Feature Highlights**
6
+ * A mapbox built-in [custom layer](https://docs.mapbox.com/mapbox-gl-js/api/properties/#customlayerinterface) instead of some canvas overlay so it is natively integrated with mapbox
7
+ * The particle position and age are stored as buffer, while the computation of new particle position is done in a vertex shader dedicated for updates, and particle motion is powered by transform feedback (overall, GPU-based instead of CPU-based)
8
+ * Single image with EXIF information as source (as simple as uploading the image to a public accessible AWS S3 bucket), no need to setup any tile server
9
+ * Works for browsers on both desktop/laptop and iPhone/iPad
10
+ * Wind particles can have varying colors based on speed, and particle movement respect the relative u- and v-component velocity rather than moving at the same rate
11
+ * Well-suited for displaying local or regional forecast results
12
+ * Method for updating the source url is available, so setting forecast for different timestamps can be done easily
13
+
14
+ ## Background and Data Requirement
15
+
16
+ Smooth raster layer (a.k.a. sample fill in [windgl](https://github.com/astrosat/windgl/tree/master), colorize in [wind-layer](https://blog.sakitam.com/wind-layer/playgrounds/mapbox-gl/colorize.html)) is just a different way to render the classic raster data on the web browser. The raw raster data consist of a grid of cells with each cell has one or more bands storing some kind of values (e.g., temperature), and a cell has a size (1/4 degrees, 5 km, 500 m, etc) making it looks like a box. The conventional way to render such data on the web is to generate a set of images by assigning colors to each cell and serving those images via a tile server; the eventual result is blocky, coarse cells appearing as a layer, just like what you typically see on a desktop GIS software like QGIS. For certain data such as weather data, we would expect strong spatial autocorrelation, and a smooth display of such data will be desired. With WebGL's varyings and fragment shader, automatic interpolation of colors across the space on clientside is possible (see [WebGL fundamentals](https://webglfundamentals.org/webgl/lessons/webgl-fundamentals.html)), and we do not need to worry about doing interpolation or down-scaling of the raster data ourselves to make the layer looks smooth for web visualization.
17
+
18
+ To use this package for displaying smooth raster layer or particle motion layer, we need to first **reproject the raw raster data into WGS 1984 (EPSG 4326)**.
19
+
20
+ For rendering smooth raster, the band of the attribute to map needs to have its values normalized to an integer between 0-255 and stored as R-band of a JPEG image. The min and max of the values without normalization is needed for the package to de-normalize the pixel values to the actual values. This package assumes such information to be stored as the EXIF image description, and in fact that is where the name of this package comes from. This idea of using EXIF is inspired by [wind-layer](https://github.com/sakitam-fdd/wind-layer/tree/master/packages/mapbox-gl). The idea of using an image to store normalized band values can be traced back to Vladimir Agafonkin's [article](https://blog.mapbox.com/how-i-built-a-wind-map-with-webgl-b63022b5537f). For smooth raster, the EXIF image description should be in the format of `min-attribute-value,max-attribute-value;`
21
+
22
+ For rendering wind as particles, the u- and v-component velocity need to be converted to an unit in mph (m/s or km/h might also be possible, but not tested; see Usage Reminder for details), normalized to an integer between 0-255, and stored as R-band and G-band of a JPEG image, respectively; there is no requirement for B-band. Additionally, the min and max of u- and v-component velocity (without normalization), as well as the min and max of speed in mph (sqrt(u * u, v * v)) need to be written to EXIF image description in the format of `min-u-velocity,max-u-velocity;min-v-velocity,max-v-velocity;min-speed,max-speed;`
23
+
24
+ Under pipeline folder of this repo, there is a Python script with associated sample json files (based on NOAA HIRESW forecast) for converting grib2 to EXIF-enabled JPEG images. Let us say we want to get NOAA HIRESW forecast for wind in southern California, we can write a bash script that utilizes following commands ([more public available data](https://nomads.ncep.noaa.gov/)):
25
+ ```bash
26
+ DATE=$(date -u +%Y%m%d)
27
+ HOUR=01 # 00..48
28
+ GRIB_FILE="wind_01.grib2"
29
+ REPROJECTED_GRIB="reprojected_01.grib2"
30
+ curl -f -s -o "$GRIB_FILE" "https://nomads.ncep.noaa.gov/cgi-bin/filter_hiresconus.pl?dir=%2Fhiresw.${DATE}&file=hiresw.t00z.arw_5km.f${HOUR}.conus.grib2&var_UGRD=on&var_VGRD=on&lev_10_m_above_ground=on&subregion=&toplat=36&leftlon=239&rightlon=243&bottomlat=32"
31
+ gdalwarp -t_srs EPSG:4326 -dstnodata -9999 -overwrite -te -121 32 -117 36 "$GRIB_FILE" "$REPROJECTED_GRIB"
32
+ python grib2_to_image.py "$REPROJECTED_GRIB" "${HOUR}" "jpeg_wind.json" "jpeg" # A text file containing bounds info will also be outputed
33
+
34
+ # aws s3 cp "$TEMP_DIR/wind/" s3://{AWS_S3_BUCKET_PATH}/wind-images/ --recursive --exclude "*" --include "*.jpeg"
35
+ ```
36
+
37
+ **Usage Reminder**
38
+ 1. The shader programs uses a formula to convert mph to lat and long per hour (applicable to ParticleMotion layer only) for determining particle displacement, and the data that I use is mph. Before I package the original code, I add an `unit` parameter in the constructor which you can set it to "kph" (km/h) or "mps" (m/s), and the package will performs an unit conversion on the values parsed from the EXIF info. I am unsure how such an addition will work.
39
+ 2. When initializing the map canvas, projection needs to be explicitly set to 'mercator' because the custom layers available in this package only works for mercator projection (many Mapbox styles assume a globe projection).
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ npm install mapbox-exif-layer
45
+ ```
46
+
47
+ Then import the layer classes in your JavaScript code:
48
+ ```javascript
49
+ import { ParticleMotion, SmoothRaster } from 'mapbox-exif-layer';
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```javascript
55
+ // Initialize a map
56
+ const map = new mapboxgl.Map({
57
+ container: 'map',
58
+ style: 'mapbox://styles/mapbox/dark-v11',
59
+ zoom: 7,
60
+ center: [-119.699944,34.432546],
61
+ projection: 'mercator' // Projection must be explicitly set to mercator (not globe which is the default for style such as dark-v11)
62
+ });
63
+
64
+ // Defining particle motion layer for wind
65
+ const particleLayer = new ParticleMotion({
66
+ id: 'wind-particle',
67
+ source: 'path/to/your/exif/image.jpeg', // For simple deployment, you can upload the image to your public AWS S3 bucket with proper CORS policy and use its URL
68
+ color: [[0, [0, 195, 255]],
69
+ [2, [0, 228, 248]],
70
+ [4, [26, 255, 221]],
71
+ [6, [53, 255, 194]],
72
+ [8, [80, 255, 167]],
73
+ [10, [109, 255, 138]],
74
+ [12, [137, 255, 110]],
75
+ [14, [165, 255, 82]],
76
+ [16, [193, 255, 54]],
77
+ [18, [219, 255, 27]],
78
+ [20, [249, 243, 1]],
79
+ [22, [255, 212, 0]],
80
+ [24, [255, 182, 0]],
81
+ [26, [255, 151, 0]],
82
+ [28, [255, 120, 0]],
83
+ [30, [255, 89, 0]],
84
+ [32, [255, 55, 0]],
85
+ [34, [255, 21, 0]],
86
+ [36, [220, 0, 0]],
87
+ [38, [182, 0, 0]],
88
+ [40, [144, 0, 0]],
89
+ [42, [128, 0, 0]]], // [ [Wind speed in mph, [R, G, B]] ...]
90
+ bounds: [-121, 36, -117, 32], // [minX, maxY, maxX, minY]
91
+ readyForDisplay: true // Only set this parameter to true if you want this layer to show up when the map is initially loaded. Otherwise (you have many layers but this layer is not to be shown up without toggeling), you do not need to specify this parameter
92
+ });
93
+
94
+ // Defining smooth raster layer for relative humidity
95
+ const relativeHumidityLayer = new SmoothRaster({
96
+ id: 'relative-humidity',
97
+ source: 'path/to/your/exif/image.jpeg',
98
+ color: [ [5, [149, 89, 16]], // value less than 5 will have the same color as a pixel with value 5
99
+ [10, [169, 107, 30]],
100
+ [15, [190, 128, 45]],
101
+ [20, [203, 154, 75]],
102
+ [25, [215, 181, 109]],
103
+ [30, [227, 202, 138]],
104
+ [35, [238, 216, 166]],
105
+ [40, [246, 232, 195]],
106
+ [45, [245, 237, 214]],
107
+ [50, [245, 242, 235]],
108
+ [55, [237, 243, 243]],
109
+ [60, [217, 237, 235]],
110
+ [65, [197, 233, 229]],
111
+ [70, [171, 222, 215]],
112
+ [75, [140, 210, 200]],
113
+ [80, [113, 195, 183]],
114
+ [85, [81, 171, 162]],
115
+ [90, [52, 149, 142]],
116
+ [95, [30, 130, 122]],
117
+ [100, [10, 111, 103]]
118
+ ],
119
+ bounds: [-121, 36, -117, 32],
120
+ readyForDisplay: true,
121
+ opacity: 0.6
122
+ });
123
+
124
+ // Defining smooth raster layer for hourly precipitation amount
125
+ const precipitationLayer = new SmoothRaster({
126
+ id: 'precipitation',
127
+ source: 'path/to/your/exif/image.jpeg',
128
+ color: [ [0.249999, [4, 232, 231, 0]], // this line ensures any pixel with precipitation less than 0.25 will be shown as transparent
129
+ [0.25, [4, 232, 231]],
130
+ [1, [4, 159, 243]],
131
+ [2, [4, 0, 243]],
132
+ [4, [2, 253, 2]],
133
+ [6, [1, 197, 1]],
134
+ [8, [0, 141, 0]],
135
+ [10, [253, 247, 1]],
136
+ [12, [229, 188, 0]],
137
+ [14, [253, 149, 0]],
138
+ [15, [253, 1, 0]],
139
+ [20, [212, 0, 0]],
140
+ [30, [188, 0, 0]],
141
+ [40, [247, 0, 254]],
142
+ [50, [152, 83, 199]]
143
+ ], // Note that the value intervals do not have to be the same/constant (1-2 vs 2-4 vs 15-20)
144
+ bounds: [-121, 36, -117, 32],
145
+ opacity: 0.6
146
+ // Note that I did not add readyForDisplay: true to this layer so it will not be rendered when map is loaded initially
147
+ });
148
+
149
+ map.on('load', () => {
150
+ // Add the custom layers like what you typically will do for other layers
151
+ // If readyForDisplay is not set to true, the custom layers in this package will not render until you set it to true
152
+ map.addLayer(relativeHumidityLayer, 'road-label-simple');
153
+ map.addLayer(precipitationLayer, 'road-label-simple');
154
+ map.addLayer(particleLayer, 'road-label-simple'); // the second argument 'road-label-simple' is a layer name in Mapbox style dark-v11, and it is optional. I specify this parameter to ensure the custom layer will be below all the map labels; other Mapbox styles do not necessarily have a layer with name 'road-label-simple'
155
+ });
156
+ ```
157
+
158
+ If you would like to make the layer appear on the map sometimes after initial map load (e.g., an user clicks a button to try to turn on the layer), you can directly modify the object's corresponding property
159
+ ```javascript
160
+ precipitationLayer.readyForDisplay = true;
161
+ ```
162
+
163
+ It is possible to control the custom layers' visibility via map's conventional setLayoutProperty method like you will do when working with other layers, but readyForDisplay property always needs to be true for the layer to be visible. readyForDisplay property is just a mechanism to prevent rendering when the layer is initially added to the map, and once it is set to true we should use setLayoutProperty method of map object to control its visibility.
164
+ ```javascript
165
+ map.setLayoutProperty('precipitation','visibility','none');
166
+ map.setLayoutProperty('precipitation','visibility','visible');
167
+ ```
168
+
169
+ For both smooth raster and particle motion layers, you can change their sources to match a different timestamp, and the layers will update automatically:
170
+ ```javascript
171
+ precipitationLayer.setSource("url/to/a/different/precipitation/img.jpeg");
172
+ particleLayer.setSource("url/to/a/different/wind/img.jpeg");
173
+ ```
174
+
175
+ For the smooth raster layer, there is an optional second argument for color, which enables simultaneous updates on both source url and color schema. This optional argument is useful when you have only one smooth raster layer added to the map, but the content of the layer can be any of temperature, relatively humidity, or precipitation, in which each has its own color schema; in such a case, both color schema and source url will need to be updated.
176
+ ```javascript
177
+ precipitationLayer.setSource("url/to/a/different/relativehumidity/img.jpeg", relativeHumidityColorArray);
178
+ ```
179
+
180
+ For the particle motion layer, there is also an optional second argument specifying the proportion of particles whose positions must be randomly reset when the source is changed (default 0.5). This argument aims to reduce the new source particle initial positions' dependency on the previous state.
181
+ ```javascript
182
+ particleLayer.setSource("url/to/a/different/wind/img.jpeg", 0.7);
183
+ ```
184
+
185
+ **Aside**
186
+
187
+ Although the color parameter defines an array of discrete value-RGB mappings, the package will always interpolate based on the given mappings and the min/max info in EXIF to create a texture with a total of 256 discrete color steps, and the final effect will be a color schema that seems to be continuous. If you want to color the raster in a complete discrete manner, this package will not be suitable. A continuous color schema is important in helping smooth raster layer look smooth.
188
+
189
+ ## Available Class Reference
190
+
191
+ ### ParticleMotion
192
+
193
+ A particle-based visualization layer that creates animated particles, suitable for wind direction and speed visualization
194
+
195
+ #### Options
196
+
197
+ - `id` (string): Unique layer ID
198
+ - `source` (string): URL of the EXIF-enabled raster image
199
+ - `color` (array): Array of color stops `[value, [r, g, b]]`. Values do not have to be ordered since sorting is performed internally by the package.
200
+ - `bounds` (array): Longitude (possible range -180 to 180) and latitude (possible range -90 to 90) of top-left and bottom-right corners of the extent in the format of `[minX, maxY, maxX, minY]`
201
+ - `readyForDisplay` (bool): Preventing the layer from rendering when the layer is added to the map, if necessary (default: false)
202
+ - `particleCount` (number): Number of particles to render (default: 5000)
203
+ - `velocityFactor` (number): Speed multiplier for particle motion (default: 0.05)
204
+ - `updateInterval` (number): Minimum time between particle updates in ms (default: 50)
205
+ - `pointSize` (number): Size of particles in pixels (default: 5.0)
206
+ - `fadeOpacity` (number): Global opacity for particles (default: 0.9)
207
+ - `trailLength` (number): Number of trailing particles (default: 3)
208
+ - `trailFadeRate` (number): How quickly the trail fades (default: 0.7)
209
+ - `trailSizeDecay` (number): How quickly point size decreases for trail particles (default: 0.8)
210
+ - `ageThreshold` (number): Age threshold before particle position reset probability increases. This prevents particles from degenerating to some circular/looped pattern (default: 500)
211
+ - `maxAge` (number): Maximum age before particle position is forced to reset. This prevents particles from degenerating to some circular/looped pattern (default: 1000)
212
+ - `unit` (string): Unit of the wind velocity values in the EXIF data (needs to be consistent with the unit in color parameter). Can be one of:
213
+ - `'mph'` (default): Miles per hour
214
+ - `'kph'`: Kilometers per hour
215
+ - `'mps'`: Meters per second
216
+
217
+ #### Methods
218
+
219
+ - `setSource(source, percentParticleWhenSetSource = 0.5)` : Changes the URL of the EXIF-enabled wind image, and optionally the proportion of particles whose positions must be reset when the source is updated (default half of the particles). The layer will repaint automatically.
220
+
221
+ ### SmoothRaster
222
+
223
+ A raster visualization layer that provides a smooth display of the data.
224
+
225
+ #### Options
226
+
227
+ - `id` (string): Unique layer ID
228
+ - `source` (string): URL of the EXIF-enabled raster image
229
+ - `color` (array): Array of color stops `[value, [r, g, b]]`. Values do not have to be ordered since sorting is performed internally by the package. An optional A-band (opacity) value can also be specified, but interpolation will not be applied to A-band. A-band is useful for rendering precipitation by setting all zero or near-zero precipitation cells completely transparent (see Usage example).
230
+ - `bounds` (array): Longitude (possible range -180 to 180) and latitude (possible range -90 to 90) of top-left and bottom-right corners of the extent in the format of `[minX, maxY, maxX, minY]`
231
+ - `opacity` (number): Layer global opacity (default: 1.0)
232
+ - `readyForDisplay` (bool): Preventing the layer from rendering when the layer is added to the map, if necessary (default: false)
233
+
234
+ #### Methods
235
+
236
+ - `setSource(source, color=null)` : Changes the URL of the EXIF-enabled raster image, and optionally color array (default is to use the same color array as before). The layer will repaint automatically.
237
+
238
+ ## Acknowledgement
239
+
240
+ The shader utility code of this package is built upon the util.js of [mapbox/webgl-wind](https://github.com/mapbox/webgl-wind/blob/master/src/util.js). The idea of EXIF is credit to [sakitam-fdd/wind-layer](https://github.com/sakitam-fdd/wind-layer).
241
+
242
+ ## License
243
+
244
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var e=require("mapbox-gl"),t=require("exifreader");function r(e,t,r){const o=e.createProgram(),a=n(e,e.VERTEX_SHADER,t),i=n(e,e.FRAGMENT_SHADER,r);if(e.attachShader(o,a),e.attachShader(o,i),t.includes("out vec2 v_position")&&(t.includes("out float v_age")?e.transformFeedbackVaryings(o,["v_position","v_age"],e.SEPARATE_ATTRIBS):e.transformFeedbackVaryings(o,["v_position"],e.SEPARATE_ATTRIBS)),e.linkProgram(o),!e.getProgramParameter(o,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(o));const s={program:o},u=e.getProgramParameter(o,e.ACTIVE_ATTRIBUTES);for(let t=0;t<u;t++){const r=e.getActiveAttrib(o,t);s[r.name]=e.getAttribLocation(o,r.name)}const l=e.getProgramParameter(o,e.ACTIVE_UNIFORMS);for(let t=0;t<l;t++){const r=e.getActiveUniform(o,t);s[r.name]=e.getUniformLocation(o,r.name)}return s}function n(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function o(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):r instanceof Image?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,null),a}function a(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}function i(e){return.621371*e}function s(e){return 2.23694*e}class u extends e.Evented{constructor({id:e,source:t,color:r,bounds:n,particleCount:o=5e3,readyForDisplay:a=!1,ageThreshold:i=500,maxAge:s=1e3,velocityFactor:u=.05,fadeOpacity:l=.9,updateInterval:c=50,pointSize:f=5,trailLength:d=3,trailFadeRate:m=.7,trailSizeDecay:h=.8,unit:g="mph"}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.bounds=n,this.particleCount=o,this.sourceLoaded=!1,this.readyForDisplay=a,this.velocityFactor=u,this.fadeOpacity=l,this.updateInterval=c,this.pointSize=f,this.trailLength=d,this.trailFadeRate=m,this.trailSizeDecay=h,this.ageThreshold=i,this.maxAge=s,this.speedRange=[0,100],this.unit=g}onAdd(e,t){this.map=e,this.gl=t,this.updateProgram=r(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n","#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n"),this.renderProgram=r(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n","#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n");const n=new Float32Array(2*this.particleCount),o=new Float32Array(this.particleCount),i=Math.ceil(Math.sqrt(this.particleCount));for(let e=0;e<this.particleCount;e++){const t=e%i/(i-1),r=Math.floor(e/i)/(i-1),a=1/(i-1),s=.25*(Math.random()-.5)*a,u=.25*(Math.random()-.5)*a;n[2*e]=Math.max(0,Math.min(1,t+s)),n[2*e+1]=Math.max(0,Math.min(1,r+u)),o[e]=Math.floor(100*Math.random())}this.particleBufferA=a(t,n),this.particleBufferB=a(t,n),this.currentBuffer=this.particleBufferA,this.nextBuffer=this.particleBufferB,this.ageBufferA=a(t,o),this.ageBufferB=a(t,o),this.currentAgeBuffer=this.ageBufferA,this.nextAgeBuffer=this.ageBufferB;const s=new Float32Array(this.trailLength+1);for(let e=0;e<=this.trailLength;e++)s[e]=e;this.trailOffsetBuffer=a(t,s),this.transformFeedback=t.createTransformFeedback(),this.lastTime=0,this.setSource(this.source,0)}setSource(e,r=.5){this.source!=e&&(this.source=e);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,a])=>{const u=URL.createObjectURL(e);n.onload=()=>{URL.revokeObjectURL(u),this.sourceTexture=o(this.gl,this.gl.LINEAR,n),this.sourceLoaded=!0,r>0&&(this.percentParticleWhenSetSource=r,this.shouldResetParticles=!0),this.map.triggerRepaint()},n.onerror=e=>{console.warn("ParticleMotion: Error loading source image:",e),URL.revokeObjectURL(u)},(async()=>{try{const e=await t.load(a);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){let e=parseFloat(t[1]),r=parseFloat(t[2]),a=parseFloat(t[3]),l=parseFloat(t[4]),c=parseFloat(t[5]),f=parseFloat(t[6]);if("kph"===this.unit?(e=i(e),r=i(r),a=i(a),l=i(l),c=i(c),f=i(f)):"mps"===this.unit&&(e=s(e),r=s(r),a=s(a),l=s(l),c=s(c),f=s(f)),!(isNaN(e)||isNaN(r)||isNaN(a)||isNaN(l)||isNaN(c)||isNaN(f)))return this.valueRange_u=[e,r],this.valueRange_v=[a,l],this.speedRange=[c,f],this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[a,i]=r;for(let e=0;e<256;e++){const r=a+e/255*(i-a);let o=0;for(;o<t.length-1&&t[o+1][0]<r;)o++;const s=Math.min(o+1,t.length-1),u=t[o],l=t[s],c=s>o?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));n[f+3]=255}return o(e,e.LINEAR,n,256,1)}(this.gl,this.color,this.speedRange),void(n.src=u)}}console.warn("ParticleMotion: No valid value ranges found in EXIF data"),URL.revokeObjectURL(u)}catch(e){console.warn("ParticleMotion: Error reading EXIF data:",e),URL.revokeObjectURL(u)}})()})).catch((e=>{console.warn("ParticleMotion: Error fetching image:",e)}))}render(e,t){if(!this.sourceLoaded||!this.readyForDisplay)return;const r=performance.now();this.lastTime||(this.lastTime=r);r-this.lastTime>=this.updateInterval&&(this.lastTime=r,e.colorMask(!1,!1,!1,!1),e.disable(e.BLEND),e.useProgram(this.updateProgram.program),e.uniform1f(this.updateProgram.u_time,r/1e3),e.uniform1f(this.updateProgram.u_speed_factor,this.velocityFactor),e.uniform4fv(this.updateProgram.u_bounds,this.bounds),e.uniform2fv(this.updateProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.updateProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.updateProgram.u_speed_range,this.speedRange),e.uniform1f(this.updateProgram.u_age_threshold,this.ageThreshold),e.uniform1f(this.updateProgram.u_max_age,this.maxAge),e.uniform1f(this.updateProgram.u_percent_reset,this.percentParticleWhenSetSource||0),e.uniform1i(this.updateProgram.u_should_reset,this.shouldResetParticles?1:0),this.shouldResetParticles=!1,e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.updateProgram.u_velocity_texture,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.updateProgram.a_position),e.vertexAttribPointer(this.updateProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentAgeBuffer),e.enableVertexAttribArray(this.updateProgram.a_age),e.vertexAttribPointer(this.updateProgram.a_age,1,e.FLOAT,!1,0,0),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,this.transformFeedback),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,this.nextBuffer),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,this.nextAgeBuffer),e.beginTransformFeedback(e.POINTS),e.drawArrays(e.POINTS,0,this.particleCount),e.endTransformFeedback(),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,null),e.colorMask(!0,!0,!0,!0),[this.currentBuffer,this.nextBuffer]=[this.nextBuffer,this.currentBuffer],[this.currentAgeBuffer,this.nextAgeBuffer]=[this.nextAgeBuffer,this.currentAgeBuffer]),e.bindFramebuffer(e.FRAMEBUFFER,null),e.viewport(0,0,e.canvas.width,e.canvas.height),e.useProgram(this.renderProgram.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.renderProgram.u_matrix,!1,t),e.uniform4fv(this.renderProgram.u_bounds,this.bounds),e.uniform1f(this.renderProgram.u_point_size,this.pointSize),e.uniform1f(this.renderProgram.u_opacity,this.fadeOpacity),e.uniform1f(this.renderProgram.u_speed_factor,this.velocityFactor),e.uniform1f(this.renderProgram.u_trail_fade_rate,this.trailFadeRate),e.uniform1f(this.renderProgram.u_trail_size_decay,this.trailSizeDecay),e.uniform2fv(this.renderProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.renderProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.renderProgram.u_speed_range,this.speedRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.renderProgram.u_velocity_texture,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.renderProgram.u_wind_color,1),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.renderProgram.a_position),e.vertexAttribPointer(this.renderProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.trailOffsetBuffer),e.enableVertexAttribArray(this.renderProgram.a_trail_offset),e.vertexAttribPointer(this.renderProgram.a_trail_offset,1,e.FLOAT,!1,0,0),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,1),e.drawArraysInstanced(e.POINTS,0,this.particleCount,this.trailLength+1),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,0),e.disable(e.BLEND),this.map.triggerRepaint()}onRemove(e,t){if(this.updateProgram){const e=t.getAttachedShaders(this.updateProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.updateProgram.program)}if(this.renderProgram){const e=t.getAttachedShaders(this.renderProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.renderProgram.program)}this.particleBufferA&&t.deleteBuffer(this.particleBufferA),this.particleBufferB&&t.deleteBuffer(this.particleBufferB),this.ageBufferA&&t.deleteBuffer(this.ageBufferA),this.ageBufferB&&t.deleteBuffer(this.ageBufferB),this.trailOffsetBuffer&&t.deleteBuffer(this.trailOffsetBuffer),this.transformFeedback&&t.deleteTransformFeedback(this.transformFeedback),this.sourceTexture&&t.deleteTexture(this.sourceTexture),this.colormapTexture&&t.deleteTexture(this.colormapTexture),this.particleBufferA=null,this.particleBufferB=null,this.ageBufferA=null,this.ageBufferB=null,this.currentBuffer=null,this.nextBuffer=null,this.currentAgeBuffer=null,this.nextAgeBuffer=null,this.trailOffsetBuffer=null,this.transformFeedback=null,this.sourceTexture=null,this.colormapTexture=null,this.updateProgram=null,this.renderProgram=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}function l(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function c(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r),a}class f extends e.Evented{constructor({id:e,source:t,color:r,bounds:n,opacity:o=1,readyForDisplay:a=!1}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.opacity=o,this.bounds=n,this.sourceLoaded=!1,this.readyForDisplay=a}onAdd(e,t){this.map=e,this.gl=t,this.program=function(e,t,r){const n=e.createProgram(),o=l(e,e.VERTEX_SHADER,t),a=l(e,e.FRAGMENT_SHADER,r);if(e.attachShader(n,o),e.attachShader(n,a),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(n));const i={program:n},s=e.getProgramParameter(n,e.ACTIVE_ATTRIBUTES);for(let t=0;t<s;t++){const r=e.getActiveAttrib(n,t);i[r.name]=e.getAttribLocation(n,r.name)}const u=e.getProgramParameter(n,e.ACTIVE_UNIFORMS);for(let t=0;t<u;t++){const r=e.getActiveUniform(n,t);i[r.name]=e.getUniformLocation(n,r.name)}return i}(t,"\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n","\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n");const r=new Float32Array([0,0,1,0,0,1,0,1,1,0,1,1]);this.vertexBuffer=function(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}(t,r),this.setSource(this.source)}setSource(e,r=null){this.source!=e&&(this.source=e),null!=r&&(this.color=r);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,r])=>{const o=URL.createObjectURL(e);this.valueRange=[0,255],(async()=>{try{const e=await t.load(r);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){const e=parseFloat(t[1]),r=parseFloat(t[2]);isNaN(e)||isNaN(r)||(this.valueRange=[e,r])}}n.onload=()=>{URL.revokeObjectURL(o),this.gl&&(this.sourceTexture=c(this.gl,this.gl.LINEAR,n),this.valueRange&&(this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[o,a]=r;for(let e=0;e<256;e++){const r=o+e/255*(a-o);let i=0;for(;i<t.length-1&&t[i+1][0]<r;)i++;const s=Math.min(i+1,t.length-1),u=t[i],l=t[s],c=s>i?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));null!=u[1][3]&&255!=u[1][3]?n[f+3]=u[1][3]:n[f+3]=255}return c(e,e.NEAREST,n,256,1)}(this.gl,this.color,this.valueRange)),this.sourceLoaded=!0,this.map&&this.map.triggerRepaint())},n.onerror=e=>{URL.revokeObjectURL(o),console.error("Error loading source image:",e)},n.src=o}catch(e){console.warn("Error reading EXIF data:",e),n.src=o}})()})).catch((e=>{console.error("Error fetching image:",e)}))}onRemove(){const e=this.gl;if(e){if(this.sourceTexture&&e.deleteTexture(this.sourceTexture),this.colormapTexture&&e.deleteTexture(this.colormapTexture),this.vertexBuffer&&e.deleteBuffer(this.vertexBuffer),this.program){const t=e.getAttachedShaders(this.program.program);t&&t.forEach((t=>e.deleteShader(t))),e.deleteProgram(this.program.program)}this.sourceTexture=null,this.colormapTexture=null,this.vertexBuffer=null,this.program=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}render(e,t){this.sourceLoaded&&this.readyForDisplay&&(e.useProgram(this.program.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.program.u_matrix,!1,t),e.uniform4fv(this.program.u_bounds,this.bounds),e.uniform1f(this.program.u_opacity,this.opacity),e.uniform2fv(this.program.u_value_range,this.valueRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.program.u_image,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.program.u_colormap,1),e.bindBuffer(e.ARRAY_BUFFER,this.vertexBuffer),e.enableVertexAttribArray(this.program.a_pos),e.vertexAttribPointer(this.program.a_pos,2,e.FLOAT,!1,0,0),e.drawArrays(e.TRIANGLES,0,6),e.disable(e.BLEND))}}exports.ParticleMotion=u,exports.SmoothRaster=f;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/ParticleMotion.js","../src/SmoothRaster.js"],"sourcesContent":["import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n`;\n\nconst fragmentShader = \n `#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n`;\n\nconst renderVertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n`;\n\nconst updateFragmentShader = \n `#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n\n // Specify transform feedback varyings if this is the update program\n if (vertexSource.includes('out vec2 v_position')) {\n // Check if we also have age tracking\n if (vertexSource.includes('out float v_age')) {\n gl.transformFeedbackVaryings(program, ['v_position', 'v_age'], gl.SEPARATE_ATTRIBS);\n } else {\n gl.transformFeedbackVaryings(program, ['v_position'], gl.SEPARATE_ATTRIBS);\n }\n }\n\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else if (data instanceof Image) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n // For null data (empty texture)\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n data[idx + 3] = 255;\n }\n \n return createTexture(gl, gl.LINEAR, data, 256, 1);\n}\n\n// Add unit conversion functions before the class definition\nfunction kphToMph(kph) {\n return kph * 0.621371;\n}\n\nfunction mpsToMph(mps) {\n return mps * 2.23694;\n}\n\nexport default class ParticleMotion extends Evented {\n constructor({id, source, color, bounds, particleCount = 5000, readyForDisplay = false, ageThreshold = 500, maxAge = 1000,\n velocityFactor = 0.05, fadeOpacity = 0.9, updateInterval = 50, pointSize = 5.0, trailLength = 3, trailFadeRate = 0.7, trailSizeDecay = 0.8, unit = 'mph'}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.bounds = bounds;\n this.particleCount = particleCount;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n \n // Particle behavior settings\n this.velocityFactor = velocityFactor; // Speed multiplier for particle motion\n this.fadeOpacity = fadeOpacity; // Global opacity for particles\n this.updateInterval = updateInterval; // Minimum time (ms) between particle updates\n this.pointSize = pointSize; // Size of particles in pixels\n \n // Trail settings\n this.trailLength = trailLength; // Number of trailing particles per main particle\n this.trailFadeRate = trailFadeRate; // How quickly the trail fades (0-1)\n this.trailSizeDecay = trailSizeDecay; // How quickly the point size decreases for trail particles\n \n // Age-based reset settings\n this.ageThreshold = ageThreshold; // Age threshold before reset probability increases\n this.maxAge = maxAge; // Maximum age before forced reset\n \n // Default speed range in case we can't read from EXIF\n this.speedRange = [0, 100];\n \n this.unit = unit; // Store the unit\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create programs with appropriate fragment shaders\n this.updateProgram = createProgram(gl, vertexShader, updateFragmentShader);\n this.renderProgram = createProgram(gl, renderVertexShader, fragmentShader);\n\n // Initialize particle positions with a uniform grid distribution\n const positions = new Float32Array(this.particleCount * 2);\n const ages = new Float32Array(this.particleCount);\n const gridSize = Math.ceil(Math.sqrt(this.particleCount));\n \n for (let i = 0; i < this.particleCount; i++) {\n const x = i % gridSize;\n const y = Math.floor(i / gridSize);\n \n // Calculate base position in [0,1] range\n const baseX = x / (gridSize - 1);\n const baseY = y / (gridSize - 1);\n \n // Add very small jitter to prevent negative values\n const gridSpacing = 1.0 / (gridSize - 1);\n const jitterX = (Math.random() - 0.5) * 0.25 * gridSpacing;\n const jitterY = (Math.random() - 0.5) * 0.25 * gridSpacing;\n \n // Combine base position with jitter\n positions[i * 2] = Math.max(0, Math.min(1, baseX + jitterX));\n positions[i * 2 + 1] = Math.max(0, Math.min(1, baseY + jitterY));\n \n // Initialize ages to random values to prevent synchronized updates\n ages[i] = Math.floor(Math.random() * 100);\n }\n \n // Create double-buffered particle position buffers (for main particles only)\n this.particleBufferA = createBuffer(gl, positions);\n this.particleBufferB = createBuffer(gl, positions);\n this.currentBuffer = this.particleBufferA;\n this.nextBuffer = this.particleBufferB;\n \n // Create age buffers for tracking particle lifecycles\n this.ageBufferA = createBuffer(gl, ages);\n this.ageBufferB = createBuffer(gl, ages);\n this.currentAgeBuffer = this.ageBufferA;\n this.nextAgeBuffer = this.ageBufferB;\n \n // Create trail offset buffer (for trail rendering)\n // We'll use instanced rendering to draw main particle + trails\n const trailOffsets = new Float32Array(this.trailLength + 1);\n for (let i = 0; i <= this.trailLength; i++) {\n trailOffsets[i] = i; // 0 = main particle, 1,2,3... = trail segments\n }\n this.trailOffsetBuffer = createBuffer(gl, trailOffsets);\n \n // Create transform feedback object\n this.transformFeedback = gl.createTransformFeedback();\n \n // Initialize time for animation\n this.lastTime = 0;\n\n // Load source image\n this.setSource(this.source, 0.0);\n }\n\n setSource(source, percentParticleWhenSetSource = 0.5) {\n if (this.source != source) {\n this.source = source;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n this.sourceLoaded = true;\n \n // Only apply percentParticleWhenSetSource if this is not the first source set\n if (percentParticleWhenSetSource > 0.0) {\n this.percentParticleWhenSetSource = percentParticleWhenSetSource;\n this.shouldResetParticles = true;\n }\n \n this.map.triggerRepaint();\n };\n\n image.onerror = (err) => {\n console.warn('ParticleMotion: Error loading source image:', err);\n URL.revokeObjectURL(objectURL);\n };\n\n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n let min_u = parseFloat(matches[1]);\n let max_u = parseFloat(matches[2]);\n let min_v = parseFloat(matches[3]);\n let max_v = parseFloat(matches[4]);\n let min_speed = parseFloat(matches[5]);\n let max_speed = parseFloat(matches[6]);\n \n // Convert units if necessary\n if (this.unit === 'kph') {\n min_u = kphToMph(min_u);\n max_u = kphToMph(max_u);\n min_v = kphToMph(min_v);\n max_v = kphToMph(max_v);\n min_speed = kphToMph(min_speed);\n max_speed = kphToMph(max_speed);\n } else if (this.unit === 'mps') {\n min_u = mpsToMph(min_u);\n max_u = mpsToMph(max_u);\n min_v = mpsToMph(min_v);\n max_v = mpsToMph(max_v);\n min_speed = mpsToMph(min_speed);\n max_speed = mpsToMph(max_speed);\n }\n \n if (!isNaN(min_u) && !isNaN(max_u) && !isNaN(min_v) && !isNaN(max_v) && !isNaN(min_speed) && !isNaN(max_speed)) {\n this.valueRange_u = [min_u, max_u];\n this.valueRange_v = [min_v, max_v];\n this.speedRange = [min_speed, max_speed];\n \n this.colormapTexture = createColormap(this.gl, this.color, this.speedRange);\n \n image.src = objectURL;\n return;\n }\n }\n }\n \n console.warn('ParticleMotion: No valid value ranges found in EXIF data');\n URL.revokeObjectURL(objectURL);\n \n } catch (error) {\n console.warn('ParticleMotion: Error reading EXIF data:', error);\n URL.revokeObjectURL(objectURL);\n }\n })();\n })\n .catch(error => {\n console.warn('ParticleMotion: Error fetching image:', error);\n });\n }\n\n render(gl, matrix) {\n if (!this.sourceLoaded || !this.readyForDisplay) {\n return;\n }\n\n // Update particle positions with throttling\n const currentTime = performance.now();\n if (!this.lastTime) this.lastTime = currentTime;\n const deltaTime = currentTime - this.lastTime;\n \n // Only update particles if enough time has passed\n const shouldUpdate = deltaTime >= this.updateInterval;\n if (shouldUpdate) {\n this.lastTime = currentTime;\n\n // ---------- UPDATE STEP ----------\n // Prevent rendering during update step\n gl.colorMask(false, false, false, false);\n gl.disable(gl.BLEND);\n \n gl.useProgram(this.updateProgram.program);\n \n // Set uniforms for update\n gl.uniform1f(this.updateProgram.u_time, currentTime / 1000);\n gl.uniform1f(this.updateProgram.u_speed_factor, this.velocityFactor);\n gl.uniform4fv(this.updateProgram.u_bounds, this.bounds);\n gl.uniform2fv(this.updateProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.updateProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.updateProgram.u_speed_range, this.speedRange);\n gl.uniform1f(this.updateProgram.u_age_threshold, this.ageThreshold);\n gl.uniform1f(this.updateProgram.u_max_age, this.maxAge);\n \n // Set new uniforms for particle reset\n gl.uniform1f(this.updateProgram.u_percent_reset, this.percentParticleWhenSetSource || 0.0);\n gl.uniform1i(this.updateProgram.u_should_reset, this.shouldResetParticles ? 1 : 0);\n // Reset the flag after setting it\n this.shouldResetParticles = false;\n \n // Bind velocity texture\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.updateProgram.u_velocity_texture, 0);\n \n // Bind current particle positions buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_position);\n gl.vertexAttribPointer(this.updateProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Bind current age buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentAgeBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_age);\n gl.vertexAttribPointer(this.updateProgram.a_age, 1, gl.FLOAT, false, 0, 0);\n \n // Bind transform feedback and next buffers\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.transformFeedback);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.nextBuffer); // Position\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, this.nextAgeBuffer); // Age\n \n // Begin transform feedback\n gl.beginTransformFeedback(gl.POINTS);\n \n // Draw particles to update positions (but nothing will be rendered due to colorMask)\n gl.drawArrays(gl.POINTS, 0, this.particleCount);\n \n // End transform feedback\n gl.endTransformFeedback();\n \n // Clean up state\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);\n \n // Re-enable drawing to color buffer\n gl.colorMask(true, true, true, true);\n \n // Swap buffers\n [this.currentBuffer, this.nextBuffer] = [this.nextBuffer, this.currentBuffer];\n [this.currentAgeBuffer, this.nextAgeBuffer] = [this.nextAgeBuffer, this.currentAgeBuffer];\n }\n \n // ---------- RENDER STEP ----------\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n \n gl.useProgram(this.renderProgram.program);\n \n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n \n // Set uniforms for rendering\n gl.uniformMatrix4fv(this.renderProgram.u_matrix, false, matrix);\n gl.uniform4fv(this.renderProgram.u_bounds, this.bounds);\n gl.uniform1f(this.renderProgram.u_point_size, this.pointSize);\n gl.uniform1f(this.renderProgram.u_opacity, this.fadeOpacity);\n gl.uniform1f(this.renderProgram.u_speed_factor, this.velocityFactor);\n gl.uniform1f(this.renderProgram.u_trail_fade_rate, this.trailFadeRate);\n gl.uniform1f(this.renderProgram.u_trail_size_decay, this.trailSizeDecay);\n gl.uniform2fv(this.renderProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.renderProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.renderProgram.u_speed_range, this.speedRange);\n \n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.renderProgram.u_velocity_texture, 0);\n \n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.renderProgram.u_wind_color, 1);\n \n // Bind current particle positions\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_position);\n gl.vertexAttribPointer(this.renderProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Set up instanced rendering for trails\n gl.bindBuffer(gl.ARRAY_BUFFER, this.trailOffsetBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_trail_offset);\n gl.vertexAttribPointer(this.renderProgram.a_trail_offset, 1, gl.FLOAT, false, 0, 0);\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 1); // This makes it instanced\n \n // Draw trails using instanced rendering\n // Each main particle will be drawn (trailLength+1) times with different offsets\n gl.drawArraysInstanced(gl.POINTS, 0, this.particleCount, this.trailLength + 1);\n \n // Reset vertex attrib divisor\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 0);\n \n gl.disable(gl.BLEND);\n \n // Request next frame\n this.map.triggerRepaint();\n }\n\n onRemove(map, gl) {\n // Clean up WebGL resources\n if (this.updateProgram) {\n const shaders = gl.getAttachedShaders(this.updateProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.updateProgram.program);\n }\n if (this.renderProgram) {\n const shaders = gl.getAttachedShaders(this.renderProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.renderProgram.program);\n }\n\n // Delete buffers\n if (this.particleBufferA) gl.deleteBuffer(this.particleBufferA);\n if (this.particleBufferB) gl.deleteBuffer(this.particleBufferB);\n if (this.ageBufferA) gl.deleteBuffer(this.ageBufferA);\n if (this.ageBufferB) gl.deleteBuffer(this.ageBufferB);\n if (this.trailOffsetBuffer) gl.deleteBuffer(this.trailOffsetBuffer);\n\n // Delete transform feedback\n if (this.transformFeedback) gl.deleteTransformFeedback(this.transformFeedback);\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Clear references\n this.particleBufferA = null;\n this.particleBufferB = null;\n this.ageBufferA = null;\n this.ageBufferB = null;\n this.currentBuffer = null;\n this.nextBuffer = null;\n this.currentAgeBuffer = null;\n this.nextAgeBuffer = null;\n this.trailOffsetBuffer = null;\n this.transformFeedback = null;\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.updateProgram = null;\n this.renderProgram = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n} ","import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = `\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n`;\n\nconst fragmentShader = `\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n\n if (low[1][3] != null && low[1][3] != 255) {\n data[idx + 3] = low[1][3];\n }\n else {\n data[idx + 3] = 255;\n }\n }\n \n return createTexture(gl, gl.NEAREST, data, 256, 1);\n}\n\nexport default class SmoothRaster extends Evented {\n constructor({id, source, color, bounds, opacity = 1.0, readyForDisplay = false}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.opacity = opacity;\n this.bounds = bounds;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create program\n this.program = createProgram(gl, vertexShader, fragmentShader);\n\n // Create vertex buffer for a full-screen quad\n const vertices = new Float32Array([\n 0, 0,\n 1, 0,\n 0, 1,\n 0, 1,\n 1, 0,\n 1, 1\n ]);\n this.vertexBuffer = createBuffer(gl, vertices);\n\n // Load source image\n this.setSource(this.source);\n }\n\n setSource(source, color = null) {\n if (this.source != source) {\n this.source = source;\n }\n\n if (color != null) {\n this.color = color;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n // Set default value range if EXIF reading fails\n this.valueRange = [0, 255]; // Default range\n \n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n const min = parseFloat(matches[1]);\n const max = parseFloat(matches[2]);\n \n if (!isNaN(min) && !isNaN(max)) {\n this.valueRange = [min, max];\n }\n }\n }\n \n // Then define onload handler\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n if (this.gl) {\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n if (this.valueRange) {\n this.colormapTexture = createColormap(this.gl, this.color, this.valueRange);\n }\n this.sourceLoaded = true;\n if (this.map) {\n this.map.triggerRepaint();\n }\n }\n };\n\n image.onerror = (err) => {\n URL.revokeObjectURL(objectURL);\n console.error('Error loading source image:', err);\n };\n // Always proceed to load the image, even if EXIF parsing fails\n image.src = objectURL;\n } catch (error) {\n console.warn('Error reading EXIF data:', error);\n // Still proceed with loading the image\n image.src = objectURL;\n }\n })();\n })\n .catch(error => {\n console.error('Error fetching image:', error);\n });\n }\n\n onRemove() {\n // Clean up WebGL resources\n const gl = this.gl;\n if (!gl) return;\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Delete buffer\n if (this.vertexBuffer) gl.deleteBuffer(this.vertexBuffer);\n\n // Delete shaders and program\n if (this.program) {\n const shaders = gl.getAttachedShaders(this.program.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.program.program);\n }\n\n // Clear references\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.vertexBuffer = null;\n this.program = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n\n render(gl, matrix) {\n // Only render if source is loaded\n if (!this.sourceLoaded || !this.readyForDisplay) return;\n \n gl.useProgram(this.program.program);\n\n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n //gl.blendEquation(gl.FUNC_ADD);\n\n // Set uniforms\n gl.uniformMatrix4fv(this.program.u_matrix, false, matrix);\n gl.uniform4fv(this.program.u_bounds, this.bounds);\n gl.uniform1f(this.program.u_opacity, this.opacity);\n gl.uniform2fv(this.program.u_value_range, this.valueRange);\n\n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.program.u_image, 0);\n\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.program.u_colormap, 1);\n\n // Draw quad\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);\n gl.enableVertexAttribArray(this.program.a_pos);\n gl.vertexAttribPointer(this.program.a_pos, 2, gl.FLOAT, false, 0, 0);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n gl.disable(gl.BLEND);\n }\n} "],"names":["createProgram","gl","vertexSource","fragmentSource","program","vertexShader","createShader","VERTEX_SHADER","fragmentShader","FRAGMENT_SHADER","attachShader","includes","transformFeedbackVaryings","SEPARATE_ATTRIBS","linkProgram","getProgramParameter","LINK_STATUS","Error","getProgramInfoLog","wrapper","numAttributes","ACTIVE_ATTRIBUTES","i","attribute","getActiveAttrib","name","getAttribLocation","numUniforms","ACTIVE_UNIFORMS","uniform","getActiveUniform","getUniformLocation","type","source","shader","shaderSource","compileShader","getShaderParameter","COMPILE_STATUS","getShaderInfoLog","createTexture","filter","data","width","height","texture","bindTexture","TEXTURE_2D","texParameteri","TEXTURE_WRAP_S","CLAMP_TO_EDGE","TEXTURE_WRAP_T","TEXTURE_MIN_FILTER","TEXTURE_MAG_FILTER","Uint8Array","texImage2D","RGBA","UNSIGNED_BYTE","Image","createBuffer","buffer","bindBuffer","ARRAY_BUFFER","bufferData","STATIC_DRAW","kphToMph","kph","mpsToMph","mps","ParticleMotion","Evented","constructor","id","color","bounds","particleCount","readyForDisplay","ageThreshold","maxAge","velocityFactor","fadeOpacity","updateInterval","pointSize","trailLength","trailFadeRate","trailSizeDecay","unit","super","this","renderingMode","sourceLoaded","speedRange","onAdd","map","updateProgram","renderProgram","positions","Float32Array","ages","gridSize","Math","ceil","sqrt","baseX","baseY","floor","gridSpacing","jitterX","random","jitterY","max","min","particleBufferA","particleBufferB","currentBuffer","nextBuffer","ageBufferA","ageBufferB","currentAgeBuffer","nextAgeBuffer","trailOffsets","trailOffsetBuffer","transformFeedback","createTransformFeedback","lastTime","setSource","percentParticleWhenSetSource","image","crossOrigin","fetch","cache","then","response","Promise","all","clone","blob","arrayBuffer","objectURL","URL","createObjectURL","onload","revokeObjectURL","sourceTexture","LINEAR","shouldResetParticles","triggerRepaint","onerror","err","console","warn","tags","ExifReader","load","description","matches","match","min_u","parseFloat","max_u","min_v","max_v","min_speed","max_speed","isNaN","valueRange_u","valueRange_v","colormapTexture","colors","valueRange","sort","a","b","minVal","maxVal","value","lowIndex","length","highIndex","low","high","t","idx","j","round","createColormap","src","error","catch","render","matrix","currentTime","performance","now","colorMask","disable","BLEND","useProgram","uniform1f","u_time","u_speed_factor","uniform4fv","u_bounds","uniform2fv","u_value_range_u","u_value_range_v","u_speed_range","u_age_threshold","u_max_age","u_percent_reset","uniform1i","u_should_reset","activeTexture","TEXTURE0","u_velocity_texture","enableVertexAttribArray","a_position","vertexAttribPointer","FLOAT","a_age","bindTransformFeedback","TRANSFORM_FEEDBACK","bindBufferBase","TRANSFORM_FEEDBACK_BUFFER","beginTransformFeedback","POINTS","drawArrays","endTransformFeedback","bindFramebuffer","FRAMEBUFFER","viewport","canvas","enable","blendFunc","SRC_ALPHA","ONE_MINUS_SRC_ALPHA","uniformMatrix4fv","u_matrix","u_point_size","u_opacity","u_trail_fade_rate","u_trail_size_decay","TEXTURE1","u_wind_color","a_trail_offset","vertexAttribDivisor","drawArraysInstanced","onRemove","shaders","getAttachedShaders","forEach","deleteShader","deleteProgram","deleteBuffer","deleteTransformFeedback","deleteTexture","SmoothRaster","opacity","vertices","vertexBuffer","NEAREST","u_value_range","u_image","u_colormap","a_pos","TRIANGLES"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{Evented as e}from"mapbox-gl";import t from"exifreader";function r(e,t,r){const o=e.createProgram(),a=n(e,e.VERTEX_SHADER,t),i=n(e,e.FRAGMENT_SHADER,r);if(e.attachShader(o,a),e.attachShader(o,i),t.includes("out vec2 v_position")&&(t.includes("out float v_age")?e.transformFeedbackVaryings(o,["v_position","v_age"],e.SEPARATE_ATTRIBS):e.transformFeedbackVaryings(o,["v_position"],e.SEPARATE_ATTRIBS)),e.linkProgram(o),!e.getProgramParameter(o,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(o));const s={program:o},u=e.getProgramParameter(o,e.ACTIVE_ATTRIBUTES);for(let t=0;t<u;t++){const r=e.getActiveAttrib(o,t);s[r.name]=e.getAttribLocation(o,r.name)}const l=e.getProgramParameter(o,e.ACTIVE_UNIFORMS);for(let t=0;t<l;t++){const r=e.getActiveUniform(o,t);s[r.name]=e.getUniformLocation(o,r.name)}return s}function n(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function o(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):r instanceof Image?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,null),a}function a(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}function i(e){return.621371*e}function s(e){return 2.23694*e}class u extends e{constructor({id:e,source:t,color:r,bounds:n,particleCount:o=5e3,readyForDisplay:a=!1,ageThreshold:i=500,maxAge:s=1e3,velocityFactor:u=.05,fadeOpacity:l=.9,updateInterval:c=50,pointSize:f=5,trailLength:m=3,trailFadeRate:d=.7,trailSizeDecay:h=.8,unit:g="mph"}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.bounds=n,this.particleCount=o,this.sourceLoaded=!1,this.readyForDisplay=a,this.velocityFactor=u,this.fadeOpacity=l,this.updateInterval=c,this.pointSize=f,this.trailLength=m,this.trailFadeRate=d,this.trailSizeDecay=h,this.ageThreshold=i,this.maxAge=s,this.speedRange=[0,100],this.unit=g}onAdd(e,t){this.map=e,this.gl=t,this.updateProgram=r(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n","#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n"),this.renderProgram=r(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n","#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n");const n=new Float32Array(2*this.particleCount),o=new Float32Array(this.particleCount),i=Math.ceil(Math.sqrt(this.particleCount));for(let e=0;e<this.particleCount;e++){const t=e%i/(i-1),r=Math.floor(e/i)/(i-1),a=1/(i-1),s=.25*(Math.random()-.5)*a,u=.25*(Math.random()-.5)*a;n[2*e]=Math.max(0,Math.min(1,t+s)),n[2*e+1]=Math.max(0,Math.min(1,r+u)),o[e]=Math.floor(100*Math.random())}this.particleBufferA=a(t,n),this.particleBufferB=a(t,n),this.currentBuffer=this.particleBufferA,this.nextBuffer=this.particleBufferB,this.ageBufferA=a(t,o),this.ageBufferB=a(t,o),this.currentAgeBuffer=this.ageBufferA,this.nextAgeBuffer=this.ageBufferB;const s=new Float32Array(this.trailLength+1);for(let e=0;e<=this.trailLength;e++)s[e]=e;this.trailOffsetBuffer=a(t,s),this.transformFeedback=t.createTransformFeedback(),this.lastTime=0,this.setSource(this.source,0)}setSource(e,r=.5){this.source!=e&&(this.source=e);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,a])=>{const u=URL.createObjectURL(e);n.onload=()=>{URL.revokeObjectURL(u),this.sourceTexture=o(this.gl,this.gl.LINEAR,n),this.sourceLoaded=!0,r>0&&(this.percentParticleWhenSetSource=r,this.shouldResetParticles=!0),this.map.triggerRepaint()},n.onerror=e=>{console.warn("ParticleMotion: Error loading source image:",e),URL.revokeObjectURL(u)},(async()=>{try{const e=await t.load(a);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){let e=parseFloat(t[1]),r=parseFloat(t[2]),a=parseFloat(t[3]),l=parseFloat(t[4]),c=parseFloat(t[5]),f=parseFloat(t[6]);if("kph"===this.unit?(e=i(e),r=i(r),a=i(a),l=i(l),c=i(c),f=i(f)):"mps"===this.unit&&(e=s(e),r=s(r),a=s(a),l=s(l),c=s(c),f=s(f)),!(isNaN(e)||isNaN(r)||isNaN(a)||isNaN(l)||isNaN(c)||isNaN(f)))return this.valueRange_u=[e,r],this.valueRange_v=[a,l],this.speedRange=[c,f],this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[a,i]=r;for(let e=0;e<256;e++){const r=a+e/255*(i-a);let o=0;for(;o<t.length-1&&t[o+1][0]<r;)o++;const s=Math.min(o+1,t.length-1),u=t[o],l=t[s],c=s>o?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));n[f+3]=255}return o(e,e.LINEAR,n,256,1)}(this.gl,this.color,this.speedRange),void(n.src=u)}}console.warn("ParticleMotion: No valid value ranges found in EXIF data"),URL.revokeObjectURL(u)}catch(e){console.warn("ParticleMotion: Error reading EXIF data:",e),URL.revokeObjectURL(u)}})()})).catch((e=>{console.warn("ParticleMotion: Error fetching image:",e)}))}render(e,t){if(!this.sourceLoaded||!this.readyForDisplay)return;const r=performance.now();this.lastTime||(this.lastTime=r);r-this.lastTime>=this.updateInterval&&(this.lastTime=r,e.colorMask(!1,!1,!1,!1),e.disable(e.BLEND),e.useProgram(this.updateProgram.program),e.uniform1f(this.updateProgram.u_time,r/1e3),e.uniform1f(this.updateProgram.u_speed_factor,this.velocityFactor),e.uniform4fv(this.updateProgram.u_bounds,this.bounds),e.uniform2fv(this.updateProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.updateProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.updateProgram.u_speed_range,this.speedRange),e.uniform1f(this.updateProgram.u_age_threshold,this.ageThreshold),e.uniform1f(this.updateProgram.u_max_age,this.maxAge),e.uniform1f(this.updateProgram.u_percent_reset,this.percentParticleWhenSetSource||0),e.uniform1i(this.updateProgram.u_should_reset,this.shouldResetParticles?1:0),this.shouldResetParticles=!1,e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.updateProgram.u_velocity_texture,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.updateProgram.a_position),e.vertexAttribPointer(this.updateProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentAgeBuffer),e.enableVertexAttribArray(this.updateProgram.a_age),e.vertexAttribPointer(this.updateProgram.a_age,1,e.FLOAT,!1,0,0),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,this.transformFeedback),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,this.nextBuffer),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,this.nextAgeBuffer),e.beginTransformFeedback(e.POINTS),e.drawArrays(e.POINTS,0,this.particleCount),e.endTransformFeedback(),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,null),e.colorMask(!0,!0,!0,!0),[this.currentBuffer,this.nextBuffer]=[this.nextBuffer,this.currentBuffer],[this.currentAgeBuffer,this.nextAgeBuffer]=[this.nextAgeBuffer,this.currentAgeBuffer]),e.bindFramebuffer(e.FRAMEBUFFER,null),e.viewport(0,0,e.canvas.width,e.canvas.height),e.useProgram(this.renderProgram.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.renderProgram.u_matrix,!1,t),e.uniform4fv(this.renderProgram.u_bounds,this.bounds),e.uniform1f(this.renderProgram.u_point_size,this.pointSize),e.uniform1f(this.renderProgram.u_opacity,this.fadeOpacity),e.uniform1f(this.renderProgram.u_speed_factor,this.velocityFactor),e.uniform1f(this.renderProgram.u_trail_fade_rate,this.trailFadeRate),e.uniform1f(this.renderProgram.u_trail_size_decay,this.trailSizeDecay),e.uniform2fv(this.renderProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.renderProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.renderProgram.u_speed_range,this.speedRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.renderProgram.u_velocity_texture,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.renderProgram.u_wind_color,1),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.renderProgram.a_position),e.vertexAttribPointer(this.renderProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.trailOffsetBuffer),e.enableVertexAttribArray(this.renderProgram.a_trail_offset),e.vertexAttribPointer(this.renderProgram.a_trail_offset,1,e.FLOAT,!1,0,0),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,1),e.drawArraysInstanced(e.POINTS,0,this.particleCount,this.trailLength+1),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,0),e.disable(e.BLEND),this.map.triggerRepaint()}onRemove(e,t){if(this.updateProgram){const e=t.getAttachedShaders(this.updateProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.updateProgram.program)}if(this.renderProgram){const e=t.getAttachedShaders(this.renderProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.renderProgram.program)}this.particleBufferA&&t.deleteBuffer(this.particleBufferA),this.particleBufferB&&t.deleteBuffer(this.particleBufferB),this.ageBufferA&&t.deleteBuffer(this.ageBufferA),this.ageBufferB&&t.deleteBuffer(this.ageBufferB),this.trailOffsetBuffer&&t.deleteBuffer(this.trailOffsetBuffer),this.transformFeedback&&t.deleteTransformFeedback(this.transformFeedback),this.sourceTexture&&t.deleteTexture(this.sourceTexture),this.colormapTexture&&t.deleteTexture(this.colormapTexture),this.particleBufferA=null,this.particleBufferB=null,this.ageBufferA=null,this.ageBufferB=null,this.currentBuffer=null,this.nextBuffer=null,this.currentAgeBuffer=null,this.nextAgeBuffer=null,this.trailOffsetBuffer=null,this.transformFeedback=null,this.sourceTexture=null,this.colormapTexture=null,this.updateProgram=null,this.renderProgram=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}function l(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function c(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r),a}class f extends e{constructor({id:e,source:t,color:r,bounds:n,opacity:o=1,readyForDisplay:a=!1}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.opacity=o,this.bounds=n,this.sourceLoaded=!1,this.readyForDisplay=a}onAdd(e,t){this.map=e,this.gl=t,this.program=function(e,t,r){const n=e.createProgram(),o=l(e,e.VERTEX_SHADER,t),a=l(e,e.FRAGMENT_SHADER,r);if(e.attachShader(n,o),e.attachShader(n,a),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(n));const i={program:n},s=e.getProgramParameter(n,e.ACTIVE_ATTRIBUTES);for(let t=0;t<s;t++){const r=e.getActiveAttrib(n,t);i[r.name]=e.getAttribLocation(n,r.name)}const u=e.getProgramParameter(n,e.ACTIVE_UNIFORMS);for(let t=0;t<u;t++){const r=e.getActiveUniform(n,t);i[r.name]=e.getUniformLocation(n,r.name)}return i}(t,"\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n","\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n");const r=new Float32Array([0,0,1,0,0,1,0,1,1,0,1,1]);this.vertexBuffer=function(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}(t,r),this.setSource(this.source)}setSource(e,r=null){this.source!=e&&(this.source=e),null!=r&&(this.color=r);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,r])=>{const o=URL.createObjectURL(e);this.valueRange=[0,255],(async()=>{try{const e=await t.load(r);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){const e=parseFloat(t[1]),r=parseFloat(t[2]);isNaN(e)||isNaN(r)||(this.valueRange=[e,r])}}n.onload=()=>{URL.revokeObjectURL(o),this.gl&&(this.sourceTexture=c(this.gl,this.gl.LINEAR,n),this.valueRange&&(this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[o,a]=r;for(let e=0;e<256;e++){const r=o+e/255*(a-o);let i=0;for(;i<t.length-1&&t[i+1][0]<r;)i++;const s=Math.min(i+1,t.length-1),u=t[i],l=t[s],c=s>i?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));null!=u[1][3]&&255!=u[1][3]?n[f+3]=u[1][3]:n[f+3]=255}return c(e,e.NEAREST,n,256,1)}(this.gl,this.color,this.valueRange)),this.sourceLoaded=!0,this.map&&this.map.triggerRepaint())},n.onerror=e=>{URL.revokeObjectURL(o),console.error("Error loading source image:",e)},n.src=o}catch(e){console.warn("Error reading EXIF data:",e),n.src=o}})()})).catch((e=>{console.error("Error fetching image:",e)}))}onRemove(){const e=this.gl;if(e){if(this.sourceTexture&&e.deleteTexture(this.sourceTexture),this.colormapTexture&&e.deleteTexture(this.colormapTexture),this.vertexBuffer&&e.deleteBuffer(this.vertexBuffer),this.program){const t=e.getAttachedShaders(this.program.program);t&&t.forEach((t=>e.deleteShader(t))),e.deleteProgram(this.program.program)}this.sourceTexture=null,this.colormapTexture=null,this.vertexBuffer=null,this.program=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}render(e,t){this.sourceLoaded&&this.readyForDisplay&&(e.useProgram(this.program.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.program.u_matrix,!1,t),e.uniform4fv(this.program.u_bounds,this.bounds),e.uniform1f(this.program.u_opacity,this.opacity),e.uniform2fv(this.program.u_value_range,this.valueRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.program.u_image,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.program.u_colormap,1),e.bindBuffer(e.ARRAY_BUFFER,this.vertexBuffer),e.enableVertexAttribArray(this.program.a_pos),e.vertexAttribPointer(this.program.a_pos,2,e.FLOAT,!1,0,0),e.drawArrays(e.TRIANGLES,0,6),e.disable(e.BLEND))}}export{u as ParticleMotion,f as SmoothRaster};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/ParticleMotion.js","../src/SmoothRaster.js"],"sourcesContent":["import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n`;\n\nconst fragmentShader = \n `#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n`;\n\nconst renderVertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n`;\n\nconst updateFragmentShader = \n `#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n\n // Specify transform feedback varyings if this is the update program\n if (vertexSource.includes('out vec2 v_position')) {\n // Check if we also have age tracking\n if (vertexSource.includes('out float v_age')) {\n gl.transformFeedbackVaryings(program, ['v_position', 'v_age'], gl.SEPARATE_ATTRIBS);\n } else {\n gl.transformFeedbackVaryings(program, ['v_position'], gl.SEPARATE_ATTRIBS);\n }\n }\n\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else if (data instanceof Image) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n // For null data (empty texture)\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n data[idx + 3] = 255;\n }\n \n return createTexture(gl, gl.LINEAR, data, 256, 1);\n}\n\n// Add unit conversion functions before the class definition\nfunction kphToMph(kph) {\n return kph * 0.621371;\n}\n\nfunction mpsToMph(mps) {\n return mps * 2.23694;\n}\n\nexport default class ParticleMotion extends Evented {\n constructor({id, source, color, bounds, particleCount = 5000, readyForDisplay = false, ageThreshold = 500, maxAge = 1000,\n velocityFactor = 0.05, fadeOpacity = 0.9, updateInterval = 50, pointSize = 5.0, trailLength = 3, trailFadeRate = 0.7, trailSizeDecay = 0.8, unit = 'mph'}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.bounds = bounds;\n this.particleCount = particleCount;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n \n // Particle behavior settings\n this.velocityFactor = velocityFactor; // Speed multiplier for particle motion\n this.fadeOpacity = fadeOpacity; // Global opacity for particles\n this.updateInterval = updateInterval; // Minimum time (ms) between particle updates\n this.pointSize = pointSize; // Size of particles in pixels\n \n // Trail settings\n this.trailLength = trailLength; // Number of trailing particles per main particle\n this.trailFadeRate = trailFadeRate; // How quickly the trail fades (0-1)\n this.trailSizeDecay = trailSizeDecay; // How quickly the point size decreases for trail particles\n \n // Age-based reset settings\n this.ageThreshold = ageThreshold; // Age threshold before reset probability increases\n this.maxAge = maxAge; // Maximum age before forced reset\n \n // Default speed range in case we can't read from EXIF\n this.speedRange = [0, 100];\n \n this.unit = unit; // Store the unit\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create programs with appropriate fragment shaders\n this.updateProgram = createProgram(gl, vertexShader, updateFragmentShader);\n this.renderProgram = createProgram(gl, renderVertexShader, fragmentShader);\n\n // Initialize particle positions with a uniform grid distribution\n const positions = new Float32Array(this.particleCount * 2);\n const ages = new Float32Array(this.particleCount);\n const gridSize = Math.ceil(Math.sqrt(this.particleCount));\n \n for (let i = 0; i < this.particleCount; i++) {\n const x = i % gridSize;\n const y = Math.floor(i / gridSize);\n \n // Calculate base position in [0,1] range\n const baseX = x / (gridSize - 1);\n const baseY = y / (gridSize - 1);\n \n // Add very small jitter to prevent negative values\n const gridSpacing = 1.0 / (gridSize - 1);\n const jitterX = (Math.random() - 0.5) * 0.25 * gridSpacing;\n const jitterY = (Math.random() - 0.5) * 0.25 * gridSpacing;\n \n // Combine base position with jitter\n positions[i * 2] = Math.max(0, Math.min(1, baseX + jitterX));\n positions[i * 2 + 1] = Math.max(0, Math.min(1, baseY + jitterY));\n \n // Initialize ages to random values to prevent synchronized updates\n ages[i] = Math.floor(Math.random() * 100);\n }\n \n // Create double-buffered particle position buffers (for main particles only)\n this.particleBufferA = createBuffer(gl, positions);\n this.particleBufferB = createBuffer(gl, positions);\n this.currentBuffer = this.particleBufferA;\n this.nextBuffer = this.particleBufferB;\n \n // Create age buffers for tracking particle lifecycles\n this.ageBufferA = createBuffer(gl, ages);\n this.ageBufferB = createBuffer(gl, ages);\n this.currentAgeBuffer = this.ageBufferA;\n this.nextAgeBuffer = this.ageBufferB;\n \n // Create trail offset buffer (for trail rendering)\n // We'll use instanced rendering to draw main particle + trails\n const trailOffsets = new Float32Array(this.trailLength + 1);\n for (let i = 0; i <= this.trailLength; i++) {\n trailOffsets[i] = i; // 0 = main particle, 1,2,3... = trail segments\n }\n this.trailOffsetBuffer = createBuffer(gl, trailOffsets);\n \n // Create transform feedback object\n this.transformFeedback = gl.createTransformFeedback();\n \n // Initialize time for animation\n this.lastTime = 0;\n\n // Load source image\n this.setSource(this.source, 0.0);\n }\n\n setSource(source, percentParticleWhenSetSource = 0.5) {\n if (this.source != source) {\n this.source = source;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n this.sourceLoaded = true;\n \n // Only apply percentParticleWhenSetSource if this is not the first source set\n if (percentParticleWhenSetSource > 0.0) {\n this.percentParticleWhenSetSource = percentParticleWhenSetSource;\n this.shouldResetParticles = true;\n }\n \n this.map.triggerRepaint();\n };\n\n image.onerror = (err) => {\n console.warn('ParticleMotion: Error loading source image:', err);\n URL.revokeObjectURL(objectURL);\n };\n\n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n let min_u = parseFloat(matches[1]);\n let max_u = parseFloat(matches[2]);\n let min_v = parseFloat(matches[3]);\n let max_v = parseFloat(matches[4]);\n let min_speed = parseFloat(matches[5]);\n let max_speed = parseFloat(matches[6]);\n \n // Convert units if necessary\n if (this.unit === 'kph') {\n min_u = kphToMph(min_u);\n max_u = kphToMph(max_u);\n min_v = kphToMph(min_v);\n max_v = kphToMph(max_v);\n min_speed = kphToMph(min_speed);\n max_speed = kphToMph(max_speed);\n } else if (this.unit === 'mps') {\n min_u = mpsToMph(min_u);\n max_u = mpsToMph(max_u);\n min_v = mpsToMph(min_v);\n max_v = mpsToMph(max_v);\n min_speed = mpsToMph(min_speed);\n max_speed = mpsToMph(max_speed);\n }\n \n if (!isNaN(min_u) && !isNaN(max_u) && !isNaN(min_v) && !isNaN(max_v) && !isNaN(min_speed) && !isNaN(max_speed)) {\n this.valueRange_u = [min_u, max_u];\n this.valueRange_v = [min_v, max_v];\n this.speedRange = [min_speed, max_speed];\n \n this.colormapTexture = createColormap(this.gl, this.color, this.speedRange);\n \n image.src = objectURL;\n return;\n }\n }\n }\n \n console.warn('ParticleMotion: No valid value ranges found in EXIF data');\n URL.revokeObjectURL(objectURL);\n \n } catch (error) {\n console.warn('ParticleMotion: Error reading EXIF data:', error);\n URL.revokeObjectURL(objectURL);\n }\n })();\n })\n .catch(error => {\n console.warn('ParticleMotion: Error fetching image:', error);\n });\n }\n\n render(gl, matrix) {\n if (!this.sourceLoaded || !this.readyForDisplay) {\n return;\n }\n\n // Update particle positions with throttling\n const currentTime = performance.now();\n if (!this.lastTime) this.lastTime = currentTime;\n const deltaTime = currentTime - this.lastTime;\n \n // Only update particles if enough time has passed\n const shouldUpdate = deltaTime >= this.updateInterval;\n if (shouldUpdate) {\n this.lastTime = currentTime;\n\n // ---------- UPDATE STEP ----------\n // Prevent rendering during update step\n gl.colorMask(false, false, false, false);\n gl.disable(gl.BLEND);\n \n gl.useProgram(this.updateProgram.program);\n \n // Set uniforms for update\n gl.uniform1f(this.updateProgram.u_time, currentTime / 1000);\n gl.uniform1f(this.updateProgram.u_speed_factor, this.velocityFactor);\n gl.uniform4fv(this.updateProgram.u_bounds, this.bounds);\n gl.uniform2fv(this.updateProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.updateProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.updateProgram.u_speed_range, this.speedRange);\n gl.uniform1f(this.updateProgram.u_age_threshold, this.ageThreshold);\n gl.uniform1f(this.updateProgram.u_max_age, this.maxAge);\n \n // Set new uniforms for particle reset\n gl.uniform1f(this.updateProgram.u_percent_reset, this.percentParticleWhenSetSource || 0.0);\n gl.uniform1i(this.updateProgram.u_should_reset, this.shouldResetParticles ? 1 : 0);\n // Reset the flag after setting it\n this.shouldResetParticles = false;\n \n // Bind velocity texture\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.updateProgram.u_velocity_texture, 0);\n \n // Bind current particle positions buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_position);\n gl.vertexAttribPointer(this.updateProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Bind current age buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentAgeBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_age);\n gl.vertexAttribPointer(this.updateProgram.a_age, 1, gl.FLOAT, false, 0, 0);\n \n // Bind transform feedback and next buffers\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.transformFeedback);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.nextBuffer); // Position\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, this.nextAgeBuffer); // Age\n \n // Begin transform feedback\n gl.beginTransformFeedback(gl.POINTS);\n \n // Draw particles to update positions (but nothing will be rendered due to colorMask)\n gl.drawArrays(gl.POINTS, 0, this.particleCount);\n \n // End transform feedback\n gl.endTransformFeedback();\n \n // Clean up state\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);\n \n // Re-enable drawing to color buffer\n gl.colorMask(true, true, true, true);\n \n // Swap buffers\n [this.currentBuffer, this.nextBuffer] = [this.nextBuffer, this.currentBuffer];\n [this.currentAgeBuffer, this.nextAgeBuffer] = [this.nextAgeBuffer, this.currentAgeBuffer];\n }\n \n // ---------- RENDER STEP ----------\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n \n gl.useProgram(this.renderProgram.program);\n \n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n \n // Set uniforms for rendering\n gl.uniformMatrix4fv(this.renderProgram.u_matrix, false, matrix);\n gl.uniform4fv(this.renderProgram.u_bounds, this.bounds);\n gl.uniform1f(this.renderProgram.u_point_size, this.pointSize);\n gl.uniform1f(this.renderProgram.u_opacity, this.fadeOpacity);\n gl.uniform1f(this.renderProgram.u_speed_factor, this.velocityFactor);\n gl.uniform1f(this.renderProgram.u_trail_fade_rate, this.trailFadeRate);\n gl.uniform1f(this.renderProgram.u_trail_size_decay, this.trailSizeDecay);\n gl.uniform2fv(this.renderProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.renderProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.renderProgram.u_speed_range, this.speedRange);\n \n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.renderProgram.u_velocity_texture, 0);\n \n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.renderProgram.u_wind_color, 1);\n \n // Bind current particle positions\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_position);\n gl.vertexAttribPointer(this.renderProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Set up instanced rendering for trails\n gl.bindBuffer(gl.ARRAY_BUFFER, this.trailOffsetBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_trail_offset);\n gl.vertexAttribPointer(this.renderProgram.a_trail_offset, 1, gl.FLOAT, false, 0, 0);\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 1); // This makes it instanced\n \n // Draw trails using instanced rendering\n // Each main particle will be drawn (trailLength+1) times with different offsets\n gl.drawArraysInstanced(gl.POINTS, 0, this.particleCount, this.trailLength + 1);\n \n // Reset vertex attrib divisor\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 0);\n \n gl.disable(gl.BLEND);\n \n // Request next frame\n this.map.triggerRepaint();\n }\n\n onRemove(map, gl) {\n // Clean up WebGL resources\n if (this.updateProgram) {\n const shaders = gl.getAttachedShaders(this.updateProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.updateProgram.program);\n }\n if (this.renderProgram) {\n const shaders = gl.getAttachedShaders(this.renderProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.renderProgram.program);\n }\n\n // Delete buffers\n if (this.particleBufferA) gl.deleteBuffer(this.particleBufferA);\n if (this.particleBufferB) gl.deleteBuffer(this.particleBufferB);\n if (this.ageBufferA) gl.deleteBuffer(this.ageBufferA);\n if (this.ageBufferB) gl.deleteBuffer(this.ageBufferB);\n if (this.trailOffsetBuffer) gl.deleteBuffer(this.trailOffsetBuffer);\n\n // Delete transform feedback\n if (this.transformFeedback) gl.deleteTransformFeedback(this.transformFeedback);\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Clear references\n this.particleBufferA = null;\n this.particleBufferB = null;\n this.ageBufferA = null;\n this.ageBufferB = null;\n this.currentBuffer = null;\n this.nextBuffer = null;\n this.currentAgeBuffer = null;\n this.nextAgeBuffer = null;\n this.trailOffsetBuffer = null;\n this.transformFeedback = null;\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.updateProgram = null;\n this.renderProgram = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n} ","import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = `\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n`;\n\nconst fragmentShader = `\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n\n if (low[1][3] != null && low[1][3] != 255) {\n data[idx + 3] = low[1][3];\n }\n else {\n data[idx + 3] = 255;\n }\n }\n \n return createTexture(gl, gl.NEAREST, data, 256, 1);\n}\n\nexport default class SmoothRaster extends Evented {\n constructor({id, source, color, bounds, opacity = 1.0, readyForDisplay = false}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.opacity = opacity;\n this.bounds = bounds;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create program\n this.program = createProgram(gl, vertexShader, fragmentShader);\n\n // Create vertex buffer for a full-screen quad\n const vertices = new Float32Array([\n 0, 0,\n 1, 0,\n 0, 1,\n 0, 1,\n 1, 0,\n 1, 1\n ]);\n this.vertexBuffer = createBuffer(gl, vertices);\n\n // Load source image\n this.setSource(this.source);\n }\n\n setSource(source, color = null) {\n if (this.source != source) {\n this.source = source;\n }\n\n if (color != null) {\n this.color = color;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n // Set default value range if EXIF reading fails\n this.valueRange = [0, 255]; // Default range\n \n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n const min = parseFloat(matches[1]);\n const max = parseFloat(matches[2]);\n \n if (!isNaN(min) && !isNaN(max)) {\n this.valueRange = [min, max];\n }\n }\n }\n \n // Then define onload handler\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n if (this.gl) {\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n if (this.valueRange) {\n this.colormapTexture = createColormap(this.gl, this.color, this.valueRange);\n }\n this.sourceLoaded = true;\n if (this.map) {\n this.map.triggerRepaint();\n }\n }\n };\n\n image.onerror = (err) => {\n URL.revokeObjectURL(objectURL);\n console.error('Error loading source image:', err);\n };\n // Always proceed to load the image, even if EXIF parsing fails\n image.src = objectURL;\n } catch (error) {\n console.warn('Error reading EXIF data:', error);\n // Still proceed with loading the image\n image.src = objectURL;\n }\n })();\n })\n .catch(error => {\n console.error('Error fetching image:', error);\n });\n }\n\n onRemove() {\n // Clean up WebGL resources\n const gl = this.gl;\n if (!gl) return;\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Delete buffer\n if (this.vertexBuffer) gl.deleteBuffer(this.vertexBuffer);\n\n // Delete shaders and program\n if (this.program) {\n const shaders = gl.getAttachedShaders(this.program.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.program.program);\n }\n\n // Clear references\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.vertexBuffer = null;\n this.program = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n\n render(gl, matrix) {\n // Only render if source is loaded\n if (!this.sourceLoaded || !this.readyForDisplay) return;\n \n gl.useProgram(this.program.program);\n\n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n //gl.blendEquation(gl.FUNC_ADD);\n\n // Set uniforms\n gl.uniformMatrix4fv(this.program.u_matrix, false, matrix);\n gl.uniform4fv(this.program.u_bounds, this.bounds);\n gl.uniform1f(this.program.u_opacity, this.opacity);\n gl.uniform2fv(this.program.u_value_range, this.valueRange);\n\n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.program.u_image, 0);\n\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.program.u_colormap, 1);\n\n // Draw quad\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);\n gl.enableVertexAttribArray(this.program.a_pos);\n gl.vertexAttribPointer(this.program.a_pos, 2, gl.FLOAT, false, 0, 0);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n gl.disable(gl.BLEND);\n }\n} "],"names":["createProgram","gl","vertexSource","fragmentSource","program","vertexShader","createShader","VERTEX_SHADER","fragmentShader","FRAGMENT_SHADER","attachShader","includes","transformFeedbackVaryings","SEPARATE_ATTRIBS","linkProgram","getProgramParameter","LINK_STATUS","Error","getProgramInfoLog","wrapper","numAttributes","ACTIVE_ATTRIBUTES","i","attribute","getActiveAttrib","name","getAttribLocation","numUniforms","ACTIVE_UNIFORMS","uniform","getActiveUniform","getUniformLocation","type","source","shader","shaderSource","compileShader","getShaderParameter","COMPILE_STATUS","getShaderInfoLog","createTexture","filter","data","width","height","texture","bindTexture","TEXTURE_2D","texParameteri","TEXTURE_WRAP_S","CLAMP_TO_EDGE","TEXTURE_WRAP_T","TEXTURE_MIN_FILTER","TEXTURE_MAG_FILTER","Uint8Array","texImage2D","RGBA","UNSIGNED_BYTE","Image","createBuffer","buffer","bindBuffer","ARRAY_BUFFER","bufferData","STATIC_DRAW","kphToMph","kph","mpsToMph","mps","ParticleMotion","Evented","constructor","id","color","bounds","particleCount","readyForDisplay","ageThreshold","maxAge","velocityFactor","fadeOpacity","updateInterval","pointSize","trailLength","trailFadeRate","trailSizeDecay","unit","super","this","renderingMode","sourceLoaded","speedRange","onAdd","map","updateProgram","renderProgram","positions","Float32Array","ages","gridSize","Math","ceil","sqrt","baseX","baseY","floor","gridSpacing","jitterX","random","jitterY","max","min","particleBufferA","particleBufferB","currentBuffer","nextBuffer","ageBufferA","ageBufferB","currentAgeBuffer","nextAgeBuffer","trailOffsets","trailOffsetBuffer","transformFeedback","createTransformFeedback","lastTime","setSource","percentParticleWhenSetSource","image","crossOrigin","fetch","cache","then","response","Promise","all","clone","blob","arrayBuffer","objectURL","URL","createObjectURL","onload","revokeObjectURL","sourceTexture","LINEAR","shouldResetParticles","triggerRepaint","onerror","err","console","warn","tags","ExifReader","load","description","matches","match","min_u","parseFloat","max_u","min_v","max_v","min_speed","max_speed","isNaN","valueRange_u","valueRange_v","colormapTexture","colors","valueRange","sort","a","b","minVal","maxVal","value","lowIndex","length","highIndex","low","high","t","idx","j","round","createColormap","src","error","catch","render","matrix","currentTime","performance","now","colorMask","disable","BLEND","useProgram","uniform1f","u_time","u_speed_factor","uniform4fv","u_bounds","uniform2fv","u_value_range_u","u_value_range_v","u_speed_range","u_age_threshold","u_max_age","u_percent_reset","uniform1i","u_should_reset","activeTexture","TEXTURE0","u_velocity_texture","enableVertexAttribArray","a_position","vertexAttribPointer","FLOAT","a_age","bindTransformFeedback","TRANSFORM_FEEDBACK","bindBufferBase","TRANSFORM_FEEDBACK_BUFFER","beginTransformFeedback","POINTS","drawArrays","endTransformFeedback","bindFramebuffer","FRAMEBUFFER","viewport","canvas","enable","blendFunc","SRC_ALPHA","ONE_MINUS_SRC_ALPHA","uniformMatrix4fv","u_matrix","u_point_size","u_opacity","u_trail_fade_rate","u_trail_size_decay","TEXTURE1","u_wind_color","a_trail_offset","vertexAttribDivisor","drawArraysInstanced","onRemove","shaders","getAttachedShaders","forEach","deleteShader","deleteProgram","deleteBuffer","deleteTransformFeedback","deleteTexture","SmoothRaster","opacity","vertices","vertexBuffer","NEAREST","u_value_range","u_image","u_colormap","a_pos","TRIANGLES"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("mapbox-gl"),require("exifreader")):"function"==typeof define&&define.amd?define(["exports","mapbox-gl","exifreader"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).MapboxExifLayer={},e.mapboxgl,e.ExifReader)}(this,(function(e,t,r){"use strict";function n(e,t,r){const n=e.createProgram(),a=o(e,e.VERTEX_SHADER,t),i=o(e,e.FRAGMENT_SHADER,r);if(e.attachShader(n,a),e.attachShader(n,i),t.includes("out vec2 v_position")&&(t.includes("out float v_age")?e.transformFeedbackVaryings(n,["v_position","v_age"],e.SEPARATE_ATTRIBS):e.transformFeedbackVaryings(n,["v_position"],e.SEPARATE_ATTRIBS)),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(n));const s={program:n},u=e.getProgramParameter(n,e.ACTIVE_ATTRIBUTES);for(let t=0;t<u;t++){const r=e.getActiveAttrib(n,t);s[r.name]=e.getAttribLocation(n,r.name)}const l=e.getProgramParameter(n,e.ACTIVE_UNIFORMS);for(let t=0;t<l;t++){const r=e.getActiveUniform(n,t);s[r.name]=e.getUniformLocation(n,r.name)}return s}function o(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function a(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):r instanceof Image?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,null),a}function i(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}function s(e){return.621371*e}function u(e){return 2.23694*e}class l extends t.Evented{constructor({id:e,source:t,color:r,bounds:n,particleCount:o=5e3,readyForDisplay:a=!1,ageThreshold:i=500,maxAge:s=1e3,velocityFactor:u=.05,fadeOpacity:l=.9,updateInterval:c=50,pointSize:f=5,trailLength:d=3,trailFadeRate:m=.7,trailSizeDecay:h=.8,unit:g="mph"}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.bounds=n,this.particleCount=o,this.sourceLoaded=!1,this.readyForDisplay=a,this.velocityFactor=u,this.fadeOpacity=l,this.updateInterval=c,this.pointSize=f,this.trailLength=d,this.trailFadeRate=m,this.trailSizeDecay=h,this.ageThreshold=i,this.maxAge=s,this.speedRange=[0,100],this.unit=g}onAdd(e,t){this.map=e,this.gl=t,this.updateProgram=n(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n","#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n"),this.renderProgram=n(t,"#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n","#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n");const r=new Float32Array(2*this.particleCount),o=new Float32Array(this.particleCount),a=Math.ceil(Math.sqrt(this.particleCount));for(let e=0;e<this.particleCount;e++){const t=e%a/(a-1),n=Math.floor(e/a)/(a-1),i=1/(a-1),s=.25*(Math.random()-.5)*i,u=.25*(Math.random()-.5)*i;r[2*e]=Math.max(0,Math.min(1,t+s)),r[2*e+1]=Math.max(0,Math.min(1,n+u)),o[e]=Math.floor(100*Math.random())}this.particleBufferA=i(t,r),this.particleBufferB=i(t,r),this.currentBuffer=this.particleBufferA,this.nextBuffer=this.particleBufferB,this.ageBufferA=i(t,o),this.ageBufferB=i(t,o),this.currentAgeBuffer=this.ageBufferA,this.nextAgeBuffer=this.ageBufferB;const s=new Float32Array(this.trailLength+1);for(let e=0;e<=this.trailLength;e++)s[e]=e;this.trailOffsetBuffer=i(t,s),this.transformFeedback=t.createTransformFeedback(),this.lastTime=0,this.setSource(this.source,0)}setSource(e,t=.5){this.source!=e&&(this.source=e);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,o])=>{const i=URL.createObjectURL(e);n.onload=()=>{URL.revokeObjectURL(i),this.sourceTexture=a(this.gl,this.gl.LINEAR,n),this.sourceLoaded=!0,t>0&&(this.percentParticleWhenSetSource=t,this.shouldResetParticles=!0),this.map.triggerRepaint()},n.onerror=e=>{console.warn("ParticleMotion: Error loading source image:",e),URL.revokeObjectURL(i)},(async()=>{try{const e=await r.load(o);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*);(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){let e=parseFloat(t[1]),r=parseFloat(t[2]),o=parseFloat(t[3]),l=parseFloat(t[4]),c=parseFloat(t[5]),f=parseFloat(t[6]);if("kph"===this.unit?(e=s(e),r=s(r),o=s(o),l=s(l),c=s(c),f=s(f)):"mps"===this.unit&&(e=u(e),r=u(r),o=u(o),l=u(l),c=u(c),f=u(f)),!(isNaN(e)||isNaN(r)||isNaN(o)||isNaN(l)||isNaN(c)||isNaN(f)))return this.valueRange_u=[e,r],this.valueRange_v=[o,l],this.speedRange=[c,f],this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[o,i]=r;for(let e=0;e<256;e++){const r=o+e/255*(i-o);let a=0;for(;a<t.length-1&&t[a+1][0]<r;)a++;const s=Math.min(a+1,t.length-1),u=t[a],l=t[s],c=s>a?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));n[f+3]=255}return a(e,e.LINEAR,n,256,1)}(this.gl,this.color,this.speedRange),void(n.src=i)}}console.warn("ParticleMotion: No valid value ranges found in EXIF data"),URL.revokeObjectURL(i)}catch(e){console.warn("ParticleMotion: Error reading EXIF data:",e),URL.revokeObjectURL(i)}})()})).catch((e=>{console.warn("ParticleMotion: Error fetching image:",e)}))}render(e,t){if(!this.sourceLoaded||!this.readyForDisplay)return;const r=performance.now();this.lastTime||(this.lastTime=r);r-this.lastTime>=this.updateInterval&&(this.lastTime=r,e.colorMask(!1,!1,!1,!1),e.disable(e.BLEND),e.useProgram(this.updateProgram.program),e.uniform1f(this.updateProgram.u_time,r/1e3),e.uniform1f(this.updateProgram.u_speed_factor,this.velocityFactor),e.uniform4fv(this.updateProgram.u_bounds,this.bounds),e.uniform2fv(this.updateProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.updateProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.updateProgram.u_speed_range,this.speedRange),e.uniform1f(this.updateProgram.u_age_threshold,this.ageThreshold),e.uniform1f(this.updateProgram.u_max_age,this.maxAge),e.uniform1f(this.updateProgram.u_percent_reset,this.percentParticleWhenSetSource||0),e.uniform1i(this.updateProgram.u_should_reset,this.shouldResetParticles?1:0),this.shouldResetParticles=!1,e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.updateProgram.u_velocity_texture,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.updateProgram.a_position),e.vertexAttribPointer(this.updateProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.currentAgeBuffer),e.enableVertexAttribArray(this.updateProgram.a_age),e.vertexAttribPointer(this.updateProgram.a_age,1,e.FLOAT,!1,0,0),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,this.transformFeedback),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,this.nextBuffer),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,this.nextAgeBuffer),e.beginTransformFeedback(e.POINTS),e.drawArrays(e.POINTS,0,this.particleCount),e.endTransformFeedback(),e.bindTransformFeedback(e.TRANSFORM_FEEDBACK,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,0,null),e.bindBufferBase(e.TRANSFORM_FEEDBACK_BUFFER,1,null),e.colorMask(!0,!0,!0,!0),[this.currentBuffer,this.nextBuffer]=[this.nextBuffer,this.currentBuffer],[this.currentAgeBuffer,this.nextAgeBuffer]=[this.nextAgeBuffer,this.currentAgeBuffer]),e.bindFramebuffer(e.FRAMEBUFFER,null),e.viewport(0,0,e.canvas.width,e.canvas.height),e.useProgram(this.renderProgram.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.renderProgram.u_matrix,!1,t),e.uniform4fv(this.renderProgram.u_bounds,this.bounds),e.uniform1f(this.renderProgram.u_point_size,this.pointSize),e.uniform1f(this.renderProgram.u_opacity,this.fadeOpacity),e.uniform1f(this.renderProgram.u_speed_factor,this.velocityFactor),e.uniform1f(this.renderProgram.u_trail_fade_rate,this.trailFadeRate),e.uniform1f(this.renderProgram.u_trail_size_decay,this.trailSizeDecay),e.uniform2fv(this.renderProgram.u_value_range_u,this.valueRange_u),e.uniform2fv(this.renderProgram.u_value_range_v,this.valueRange_v),e.uniform2fv(this.renderProgram.u_speed_range,this.speedRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.renderProgram.u_velocity_texture,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.renderProgram.u_wind_color,1),e.bindBuffer(e.ARRAY_BUFFER,this.currentBuffer),e.enableVertexAttribArray(this.renderProgram.a_position),e.vertexAttribPointer(this.renderProgram.a_position,2,e.FLOAT,!1,0,0),e.bindBuffer(e.ARRAY_BUFFER,this.trailOffsetBuffer),e.enableVertexAttribArray(this.renderProgram.a_trail_offset),e.vertexAttribPointer(this.renderProgram.a_trail_offset,1,e.FLOAT,!1,0,0),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,1),e.drawArraysInstanced(e.POINTS,0,this.particleCount,this.trailLength+1),e.vertexAttribDivisor(this.renderProgram.a_trail_offset,0),e.disable(e.BLEND),this.map.triggerRepaint()}onRemove(e,t){if(this.updateProgram){const e=t.getAttachedShaders(this.updateProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.updateProgram.program)}if(this.renderProgram){const e=t.getAttachedShaders(this.renderProgram.program);e&&e.forEach((e=>t.deleteShader(e))),t.deleteProgram(this.renderProgram.program)}this.particleBufferA&&t.deleteBuffer(this.particleBufferA),this.particleBufferB&&t.deleteBuffer(this.particleBufferB),this.ageBufferA&&t.deleteBuffer(this.ageBufferA),this.ageBufferB&&t.deleteBuffer(this.ageBufferB),this.trailOffsetBuffer&&t.deleteBuffer(this.trailOffsetBuffer),this.transformFeedback&&t.deleteTransformFeedback(this.transformFeedback),this.sourceTexture&&t.deleteTexture(this.sourceTexture),this.colormapTexture&&t.deleteTexture(this.colormapTexture),this.particleBufferA=null,this.particleBufferB=null,this.ageBufferA=null,this.ageBufferB=null,this.currentBuffer=null,this.nextBuffer=null,this.currentAgeBuffer=null,this.nextAgeBuffer=null,this.trailOffsetBuffer=null,this.transformFeedback=null,this.sourceTexture=null,this.colormapTexture=null,this.updateProgram=null,this.renderProgram=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}function c(e,t,r){const n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function f(e,t,r,n,o){const a=e.createTexture();return e.bindTexture(e.TEXTURE_2D,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),r instanceof Uint8Array?e.texImage2D(e.TEXTURE_2D,0,e.RGBA,n,o,0,e.RGBA,e.UNSIGNED_BYTE,r):e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,r),a}class d extends t.Evented{constructor({id:e,source:t,color:r,bounds:n,opacity:o=1,readyForDisplay:a=!1}){super(),this.id=e,this.type="custom",this.renderingMode="2d",this.source=t,this.color=r,this.opacity=o,this.bounds=n,this.sourceLoaded=!1,this.readyForDisplay=a}onAdd(e,t){this.map=e,this.gl=t,this.program=function(e,t,r){const n=e.createProgram(),o=c(e,e.VERTEX_SHADER,t),a=c(e,e.FRAGMENT_SHADER,r);if(e.attachShader(n,o),e.attachShader(n,a),e.linkProgram(n),!e.getProgramParameter(n,e.LINK_STATUS))throw new Error(e.getProgramInfoLog(n));const i={program:n},s=e.getProgramParameter(n,e.ACTIVE_ATTRIBUTES);for(let t=0;t<s;t++){const r=e.getActiveAttrib(n,t);i[r.name]=e.getAttribLocation(n,r.name)}const u=e.getProgramParameter(n,e.ACTIVE_UNIFORMS);for(let t=0;t<u;t++){const r=e.getActiveUniform(n,t);i[r.name]=e.getUniformLocation(n,r.name)}return i}(t,"\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n","\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n");const r=new Float32Array([0,0,1,0,0,1,0,1,1,0,1,1]);this.vertexBuffer=function(e,t){const r=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,r),e.bufferData(e.ARRAY_BUFFER,t,e.STATIC_DRAW),r}(t,r),this.setSource(this.source)}setSource(e,t=null){this.source!=e&&(this.source=e),null!=t&&(this.color=t);const n=new Image;n.crossOrigin="anonymous",fetch(e,{cache:"no-store"}).then((e=>Promise.all([e.clone().blob(),e.arrayBuffer().then((e=>new Uint8Array(e).buffer))]))).then((([e,t])=>{const o=URL.createObjectURL(e);this.valueRange=[0,255],(async()=>{try{const e=await r.load(t);if(e.ImageDescription&&e.ImageDescription.description){const t=e.ImageDescription.description.match(/(-?\d+\.?\d*),(-?\d+\.?\d*)/);if(t){const e=parseFloat(t[1]),r=parseFloat(t[2]);isNaN(e)||isNaN(r)||(this.valueRange=[e,r])}}n.onload=()=>{URL.revokeObjectURL(o),this.gl&&(this.sourceTexture=f(this.gl,this.gl.LINEAR,n),this.valueRange&&(this.colormapTexture=function(e,t,r){t.sort(((e,t)=>e[0]-t[0]));const n=new Uint8Array(1024),[o,a]=r;for(let e=0;e<256;e++){const r=o+e/255*(a-o);let i=0;for(;i<t.length-1&&t[i+1][0]<r;)i++;const s=Math.min(i+1,t.length-1),u=t[i],l=t[s],c=s>i?(r-u[0])/(l[0]-u[0]):0,f=4*e;for(let e=0;e<3;e++)n[f+e]=Math.round(u[1][e]+c*(l[1][e]-u[1][e]));null!=u[1][3]&&255!=u[1][3]?n[f+3]=u[1][3]:n[f+3]=255}return f(e,e.NEAREST,n,256,1)}(this.gl,this.color,this.valueRange)),this.sourceLoaded=!0,this.map&&this.map.triggerRepaint())},n.onerror=e=>{URL.revokeObjectURL(o),console.error("Error loading source image:",e)},n.src=o}catch(e){console.warn("Error reading EXIF data:",e),n.src=o}})()})).catch((e=>{console.error("Error fetching image:",e)}))}onRemove(){const e=this.gl;if(e){if(this.sourceTexture&&e.deleteTexture(this.sourceTexture),this.colormapTexture&&e.deleteTexture(this.colormapTexture),this.vertexBuffer&&e.deleteBuffer(this.vertexBuffer),this.program){const t=e.getAttachedShaders(this.program.program);t&&t.forEach((t=>e.deleteShader(t))),e.deleteProgram(this.program.program)}this.sourceTexture=null,this.colormapTexture=null,this.vertexBuffer=null,this.program=null,this.gl=null,this.map=null,this.sourceLoaded=!1}}render(e,t){this.sourceLoaded&&this.readyForDisplay&&(e.useProgram(this.program.program),e.enable(e.BLEND),e.blendFunc(e.SRC_ALPHA,e.ONE_MINUS_SRC_ALPHA),e.uniformMatrix4fv(this.program.u_matrix,!1,t),e.uniform4fv(this.program.u_bounds,this.bounds),e.uniform1f(this.program.u_opacity,this.opacity),e.uniform2fv(this.program.u_value_range,this.valueRange),e.activeTexture(e.TEXTURE0),e.bindTexture(e.TEXTURE_2D,this.sourceTexture),e.uniform1i(this.program.u_image,0),e.activeTexture(e.TEXTURE1),e.bindTexture(e.TEXTURE_2D,this.colormapTexture),e.uniform1i(this.program.u_colormap,1),e.bindBuffer(e.ARRAY_BUFFER,this.vertexBuffer),e.enableVertexAttribArray(this.program.a_pos),e.vertexAttribPointer(this.program.a_pos,2,e.FLOAT,!1,0,0),e.drawArrays(e.TRIANGLES,0,6),e.disable(e.BLEND))}}e.ParticleMotion=l,e.SmoothRaster=d}));
2
+ //# sourceMappingURL=index.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.umd.js","sources":["../src/ParticleMotion.js","../src/SmoothRaster.js"],"sourcesContent":["import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Current position [0,1]\n in float a_age; // Particle age for tracking circular patterns\n out vec2 v_position; // Updated position for transform feedback\n out float v_age; // Updated age for transform feedback\n \n uniform sampler2D u_velocity_texture; // Texture with normalized velocities [0,1]\n uniform mediump vec4 u_bounds;\n uniform mediump float u_speed_factor;\n uniform mediump float u_time;\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump float u_age_threshold; // Age threshold for reset probability\n uniform mediump float u_max_age; // Maximum age before forced reset\n uniform mediump float u_percent_reset; // Percentage of particles to reset on source update\n uniform bool u_should_reset; // Flag to indicate if we should apply the percentage reset\n \n // Random function based on time and position\n float random(vec2 co) {\n float a = 12.9898;\n float b = 78.233;\n float c = 43758.5453;\n float dt = dot(co, vec2(a,b));\n float sn = mod(dt, 3.14);\n return fract(sin(sn) * c + u_time);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n // Function to generate random position within extent\n vec2 generateRandomPosition(vec2 seed) {\n return vec2(\n random(seed + vec2(1.23, 4.56)),\n random(seed + vec2(7.89, 0.12))\n );\n }\n \n // Function to generate a position on one of the boundaries\n vec2 generateBoundaryPosition(vec2 seed) {\n // Choose which boundary (0=left, 1=top, 2=right, 3=bottom)\n float boundary = floor(random(seed) * 4.0);\n \n // Position along the boundary\n float pos = random(seed + vec2(boundary));\n \n // Small offset to prevent immediate out-of-bounds\n float offset = 0.001;\n \n if (boundary < 1.0) { // Left\n return vec2(offset, pos);\n } else if (boundary < 2.0) { // Top\n return vec2(pos, offset);\n } else if (boundary < 3.0) { // Right\n return vec2(1.0 - offset, pos);\n } else { // Bottom\n return vec2(pos, 1.0 - offset);\n }\n }\n \n void main() {\n // Sample normalized velocity from texture [0,1]\n vec4 velocity = texture(u_velocity_texture, a_position);\n \n // Denormalize velocities to actual mph values\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n \n // Calculate wind speed\n float windSpeed = length(vec2(u, v));\n \n // Convert position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], a_position.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - a_position.y);\n \n // Convert wind velocity to degree offsets using actual mph values\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), lat);\n \n // Convert degree offsets back to normalized coordinates\n vec2 normalizedVelocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n // Update position with velocity\n vec2 newPos = a_position + normalizedVelocity * u_speed_factor;\n \n // Reset rules\n bool shouldReset = false;\n \n // Get current age of particle and increment\n float age = a_age + 1.0;\n \n // Reset if out of bounds or very low wind speed\n if (newPos.x < 0.0 || newPos.x > 1.0 || newPos.y < 0.0 || newPos.y > 1.0 || windSpeed < 1.5) {\n shouldReset = true;\n }\n \n // Reset based on age with increasing probability\n if (age > u_age_threshold) {\n float resetProbability = (age - u_age_threshold) / (u_max_age - u_age_threshold);\n if (random(a_position + vec2(u_time * 0.1, age * 0.01)) < resetProbability) {\n shouldReset = true;\n }\n }\n \n // Force reset of very old particles\n if (age > u_max_age) {\n shouldReset = true;\n }\n \n // Additional reset based on percentParticleWhenSetSource if flag is set\n if (!shouldReset && u_should_reset) {\n if (random(a_position + vec2(u_time)) < u_percent_reset) {\n shouldReset = true;\n }\n }\n \n // Handle reset by generating a new position\n if (shouldReset) {\n newPos = generateRandomPosition(a_position + vec2(u_time));\n\n age = 0.0; // Reset age\n }\n \n v_position = newPos;\n v_age = age;\n }\n`;\n\nconst fragmentShader = \n `#version 300 es\n precision mediump float;\n \n uniform sampler2D u_wind_color; // Colormap texture\n uniform sampler2D u_velocity_texture; // Wind velocity texture\n uniform mediump float u_opacity; // Global opacity control\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n uniform mediump vec2 u_speed_range; // Speed range for normalization\n \n in vec2 v_position; // Current position\n out vec4 fragColor; // Output color\n \n void main() {\n // Calculate distance from center for circular particles\n vec2 center = gl_PointCoord - vec2(0.5);\n float dist = length(center);\n \n // Discard pixels outside the circle\n if (dist > 0.5) {\n discard;\n }\n \n // Create soft-edged circular particles\n float edgeFactor = 1.0 - smoothstep(0.45, 0.5, dist);\n \n // Sample wind velocity for coloring\n vec4 velocity = texture(u_velocity_texture, v_position);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocity.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocity.g);\n float speed = length(vec2(u, v));\n float normalizedSpeed = (speed - u_speed_range[0]) / (u_speed_range[1] - u_speed_range[0]);\n \n // Sample color from colormap using normalized speed\n vec4 color = texture(u_wind_color, vec2(normalizedSpeed, 0.5));\n \n // Ensure we have some minimum color intensity\n color.rgb = max(color.rgb, vec3(0.2));\n \n // Combine edge fade with opacity\n float finalAlpha = edgeFactor * u_opacity;\n \n // Output final color with alpha\n fragColor = vec4(color.rgb, finalAlpha);\n }\n`;\n\nconst renderVertexShader = \n `#version 300 es\n precision mediump float;\n \n in vec2 a_position; // Position in [0,1] range\n in float a_trail_offset; // Trail offset (0=main particle, 1,2,3=trail segments)\n \n uniform mediump mat4 u_matrix;\n uniform mediump vec4 u_bounds; // [minX, maxY, maxX, minY]\n uniform mediump float u_point_size; // Base point size\n uniform mediump float u_speed_factor; // Speed multiplier\n uniform mediump float u_trail_size_decay; // Size decay rate for trail particles\n uniform mediump float u_trail_fade_rate; // Opacity decay rate for trail\n uniform sampler2D u_velocity_texture; // Velocity texture\n uniform mediump vec2 u_value_range_u; // Wind U component range\n uniform mediump vec2 u_value_range_v; // Wind V component range\n \n out vec2 v_position; // Pass position to fragment shader\n out float v_opacity; // Varying opacity for trail\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n return vec2(x, y);\n }\n \n // Convert position to geographic coordinates\n vec2 positionToGeo(vec2 pos) {\n float lng = mix(u_bounds[0], u_bounds[2], pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - pos.y); // Invert y for correct mapping\n return vec2(lng, lat);\n }\n \n // Convert mph to degrees per frame\n vec2 mphToDegreesPerFrame(vec2 mph, float lat) {\n float kmh_to_degrees = 1.60934 / 111.32; // Convert mph to km/h, then to degrees\n float latScale = cos(lat * 3.14159 / 180.0); // Latitude scaling for longitude\n return vec2(mph.x * kmh_to_degrees / latScale, -mph.y * kmh_to_degrees);\n }\n \n void main() {\n // Trail offset (0 = main particle, >0 = trail segment)\n float trailOffset = a_trail_offset;\n \n // Main particle position from buffer\n vec2 mainPos = a_position;\n \n // Current position is main position for main particle (offset=0)\n vec2 currentPos = mainPos;\n \n // Sample velocity at the main particle's position\n vec4 velocityData = texture(u_velocity_texture, mainPos);\n float u = mix(u_value_range_u[0], u_value_range_u[1], velocityData.r);\n float v = mix(u_value_range_v[0], u_value_range_v[1], velocityData.g);\n \n // Calculate velocity in normalized coordinates\n vec2 geo = positionToGeo(mainPos);\n vec2 degrees = mphToDegreesPerFrame(vec2(u, v), geo.y);\n vec2 velocity = vec2(\n degrees.x / (u_bounds[2] - u_bounds[0]),\n degrees.y / (u_bounds[1] - u_bounds[3])\n );\n \n if (trailOffset > 0.0) {\n // Compute trail position by moving backwards from main particle along its velocity path\n // The strength of the offset is based on trail segment position\n currentPos = mainPos - velocity * u_speed_factor * trailOffset * 1.5;\n }\n \n // Convert normalized position to geographic coordinates\n float lng = mix(u_bounds[0], u_bounds[2], currentPos.x);\n float lat = mix(u_bounds[3], u_bounds[1], 1.0 - currentPos.y);\n \n // Project to Web Mercator\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n \n // Pass position to fragment shader\n v_position = currentPos;\n \n // Compute opacity for trail particles (1.0 for main particle, decreasing for trail)\n v_opacity = trailOffset == 0.0 ? 1.0 : pow(u_trail_fade_rate, trailOffset);\n \n // Compute point size (decreasing for trail particles)\n float size = trailOffset == 0.0 ? u_point_size : u_point_size * pow(u_trail_size_decay, trailOffset);\n gl_PointSize = size;\n }\n`;\n\nconst updateFragmentShader = \n `#version 300 es\n precision mediump float;\n \n out vec4 fragColor;\n \n void main() {\n // For update step, we don't need to output anything visual\n // We're just using transform feedback to capture the new positions\n fragColor = vec4(0.0, 0.0, 0.0, 0.0);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n\n // Specify transform feedback varyings if this is the update program\n if (vertexSource.includes('out vec2 v_position')) {\n // Check if we also have age tracking\n if (vertexSource.includes('out float v_age')) {\n gl.transformFeedbackVaryings(program, ['v_position', 'v_age'], gl.SEPARATE_ATTRIBS);\n } else {\n gl.transformFeedbackVaryings(program, ['v_position'], gl.SEPARATE_ATTRIBS);\n }\n }\n\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else if (data instanceof Image) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n // For null data (empty texture)\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n data[idx + 3] = 255;\n }\n \n return createTexture(gl, gl.LINEAR, data, 256, 1);\n}\n\n// Add unit conversion functions before the class definition\nfunction kphToMph(kph) {\n return kph * 0.621371;\n}\n\nfunction mpsToMph(mps) {\n return mps * 2.23694;\n}\n\nexport default class ParticleMotion extends Evented {\n constructor({id, source, color, bounds, particleCount = 5000, readyForDisplay = false, ageThreshold = 500, maxAge = 1000,\n velocityFactor = 0.05, fadeOpacity = 0.9, updateInterval = 50, pointSize = 5.0, trailLength = 3, trailFadeRate = 0.7, trailSizeDecay = 0.8, unit = 'mph'}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.bounds = bounds;\n this.particleCount = particleCount;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n \n // Particle behavior settings\n this.velocityFactor = velocityFactor; // Speed multiplier for particle motion\n this.fadeOpacity = fadeOpacity; // Global opacity for particles\n this.updateInterval = updateInterval; // Minimum time (ms) between particle updates\n this.pointSize = pointSize; // Size of particles in pixels\n \n // Trail settings\n this.trailLength = trailLength; // Number of trailing particles per main particle\n this.trailFadeRate = trailFadeRate; // How quickly the trail fades (0-1)\n this.trailSizeDecay = trailSizeDecay; // How quickly the point size decreases for trail particles\n \n // Age-based reset settings\n this.ageThreshold = ageThreshold; // Age threshold before reset probability increases\n this.maxAge = maxAge; // Maximum age before forced reset\n \n // Default speed range in case we can't read from EXIF\n this.speedRange = [0, 100];\n \n this.unit = unit; // Store the unit\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create programs with appropriate fragment shaders\n this.updateProgram = createProgram(gl, vertexShader, updateFragmentShader);\n this.renderProgram = createProgram(gl, renderVertexShader, fragmentShader);\n\n // Initialize particle positions with a uniform grid distribution\n const positions = new Float32Array(this.particleCount * 2);\n const ages = new Float32Array(this.particleCount);\n const gridSize = Math.ceil(Math.sqrt(this.particleCount));\n \n for (let i = 0; i < this.particleCount; i++) {\n const x = i % gridSize;\n const y = Math.floor(i / gridSize);\n \n // Calculate base position in [0,1] range\n const baseX = x / (gridSize - 1);\n const baseY = y / (gridSize - 1);\n \n // Add very small jitter to prevent negative values\n const gridSpacing = 1.0 / (gridSize - 1);\n const jitterX = (Math.random() - 0.5) * 0.25 * gridSpacing;\n const jitterY = (Math.random() - 0.5) * 0.25 * gridSpacing;\n \n // Combine base position with jitter\n positions[i * 2] = Math.max(0, Math.min(1, baseX + jitterX));\n positions[i * 2 + 1] = Math.max(0, Math.min(1, baseY + jitterY));\n \n // Initialize ages to random values to prevent synchronized updates\n ages[i] = Math.floor(Math.random() * 100);\n }\n \n // Create double-buffered particle position buffers (for main particles only)\n this.particleBufferA = createBuffer(gl, positions);\n this.particleBufferB = createBuffer(gl, positions);\n this.currentBuffer = this.particleBufferA;\n this.nextBuffer = this.particleBufferB;\n \n // Create age buffers for tracking particle lifecycles\n this.ageBufferA = createBuffer(gl, ages);\n this.ageBufferB = createBuffer(gl, ages);\n this.currentAgeBuffer = this.ageBufferA;\n this.nextAgeBuffer = this.ageBufferB;\n \n // Create trail offset buffer (for trail rendering)\n // We'll use instanced rendering to draw main particle + trails\n const trailOffsets = new Float32Array(this.trailLength + 1);\n for (let i = 0; i <= this.trailLength; i++) {\n trailOffsets[i] = i; // 0 = main particle, 1,2,3... = trail segments\n }\n this.trailOffsetBuffer = createBuffer(gl, trailOffsets);\n \n // Create transform feedback object\n this.transformFeedback = gl.createTransformFeedback();\n \n // Initialize time for animation\n this.lastTime = 0;\n\n // Load source image\n this.setSource(this.source, 0.0);\n }\n\n setSource(source, percentParticleWhenSetSource = 0.5) {\n if (this.source != source) {\n this.source = source;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n this.sourceLoaded = true;\n \n // Only apply percentParticleWhenSetSource if this is not the first source set\n if (percentParticleWhenSetSource > 0.0) {\n this.percentParticleWhenSetSource = percentParticleWhenSetSource;\n this.shouldResetParticles = true;\n }\n \n this.map.triggerRepaint();\n };\n\n image.onerror = (err) => {\n console.warn('ParticleMotion: Error loading source image:', err);\n URL.revokeObjectURL(objectURL);\n };\n\n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*);(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n let min_u = parseFloat(matches[1]);\n let max_u = parseFloat(matches[2]);\n let min_v = parseFloat(matches[3]);\n let max_v = parseFloat(matches[4]);\n let min_speed = parseFloat(matches[5]);\n let max_speed = parseFloat(matches[6]);\n \n // Convert units if necessary\n if (this.unit === 'kph') {\n min_u = kphToMph(min_u);\n max_u = kphToMph(max_u);\n min_v = kphToMph(min_v);\n max_v = kphToMph(max_v);\n min_speed = kphToMph(min_speed);\n max_speed = kphToMph(max_speed);\n } else if (this.unit === 'mps') {\n min_u = mpsToMph(min_u);\n max_u = mpsToMph(max_u);\n min_v = mpsToMph(min_v);\n max_v = mpsToMph(max_v);\n min_speed = mpsToMph(min_speed);\n max_speed = mpsToMph(max_speed);\n }\n \n if (!isNaN(min_u) && !isNaN(max_u) && !isNaN(min_v) && !isNaN(max_v) && !isNaN(min_speed) && !isNaN(max_speed)) {\n this.valueRange_u = [min_u, max_u];\n this.valueRange_v = [min_v, max_v];\n this.speedRange = [min_speed, max_speed];\n \n this.colormapTexture = createColormap(this.gl, this.color, this.speedRange);\n \n image.src = objectURL;\n return;\n }\n }\n }\n \n console.warn('ParticleMotion: No valid value ranges found in EXIF data');\n URL.revokeObjectURL(objectURL);\n \n } catch (error) {\n console.warn('ParticleMotion: Error reading EXIF data:', error);\n URL.revokeObjectURL(objectURL);\n }\n })();\n })\n .catch(error => {\n console.warn('ParticleMotion: Error fetching image:', error);\n });\n }\n\n render(gl, matrix) {\n if (!this.sourceLoaded || !this.readyForDisplay) {\n return;\n }\n\n // Update particle positions with throttling\n const currentTime = performance.now();\n if (!this.lastTime) this.lastTime = currentTime;\n const deltaTime = currentTime - this.lastTime;\n \n // Only update particles if enough time has passed\n const shouldUpdate = deltaTime >= this.updateInterval;\n if (shouldUpdate) {\n this.lastTime = currentTime;\n\n // ---------- UPDATE STEP ----------\n // Prevent rendering during update step\n gl.colorMask(false, false, false, false);\n gl.disable(gl.BLEND);\n \n gl.useProgram(this.updateProgram.program);\n \n // Set uniforms for update\n gl.uniform1f(this.updateProgram.u_time, currentTime / 1000);\n gl.uniform1f(this.updateProgram.u_speed_factor, this.velocityFactor);\n gl.uniform4fv(this.updateProgram.u_bounds, this.bounds);\n gl.uniform2fv(this.updateProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.updateProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.updateProgram.u_speed_range, this.speedRange);\n gl.uniform1f(this.updateProgram.u_age_threshold, this.ageThreshold);\n gl.uniform1f(this.updateProgram.u_max_age, this.maxAge);\n \n // Set new uniforms for particle reset\n gl.uniform1f(this.updateProgram.u_percent_reset, this.percentParticleWhenSetSource || 0.0);\n gl.uniform1i(this.updateProgram.u_should_reset, this.shouldResetParticles ? 1 : 0);\n // Reset the flag after setting it\n this.shouldResetParticles = false;\n \n // Bind velocity texture\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.updateProgram.u_velocity_texture, 0);\n \n // Bind current particle positions buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_position);\n gl.vertexAttribPointer(this.updateProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Bind current age buffer as input\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentAgeBuffer);\n gl.enableVertexAttribArray(this.updateProgram.a_age);\n gl.vertexAttribPointer(this.updateProgram.a_age, 1, gl.FLOAT, false, 0, 0);\n \n // Bind transform feedback and next buffers\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.transformFeedback);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, this.nextBuffer); // Position\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, this.nextAgeBuffer); // Age\n \n // Begin transform feedback\n gl.beginTransformFeedback(gl.POINTS);\n \n // Draw particles to update positions (but nothing will be rendered due to colorMask)\n gl.drawArrays(gl.POINTS, 0, this.particleCount);\n \n // End transform feedback\n gl.endTransformFeedback();\n \n // Clean up state\n gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);\n gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);\n \n // Re-enable drawing to color buffer\n gl.colorMask(true, true, true, true);\n \n // Swap buffers\n [this.currentBuffer, this.nextBuffer] = [this.nextBuffer, this.currentBuffer];\n [this.currentAgeBuffer, this.nextAgeBuffer] = [this.nextAgeBuffer, this.currentAgeBuffer];\n }\n \n // ---------- RENDER STEP ----------\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n \n gl.useProgram(this.renderProgram.program);\n \n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n \n // Set uniforms for rendering\n gl.uniformMatrix4fv(this.renderProgram.u_matrix, false, matrix);\n gl.uniform4fv(this.renderProgram.u_bounds, this.bounds);\n gl.uniform1f(this.renderProgram.u_point_size, this.pointSize);\n gl.uniform1f(this.renderProgram.u_opacity, this.fadeOpacity);\n gl.uniform1f(this.renderProgram.u_speed_factor, this.velocityFactor);\n gl.uniform1f(this.renderProgram.u_trail_fade_rate, this.trailFadeRate);\n gl.uniform1f(this.renderProgram.u_trail_size_decay, this.trailSizeDecay);\n gl.uniform2fv(this.renderProgram.u_value_range_u, this.valueRange_u);\n gl.uniform2fv(this.renderProgram.u_value_range_v, this.valueRange_v);\n gl.uniform2fv(this.renderProgram.u_speed_range, this.speedRange);\n \n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.renderProgram.u_velocity_texture, 0);\n \n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.renderProgram.u_wind_color, 1);\n \n // Bind current particle positions\n gl.bindBuffer(gl.ARRAY_BUFFER, this.currentBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_position);\n gl.vertexAttribPointer(this.renderProgram.a_position, 2, gl.FLOAT, false, 0, 0);\n \n // Set up instanced rendering for trails\n gl.bindBuffer(gl.ARRAY_BUFFER, this.trailOffsetBuffer);\n gl.enableVertexAttribArray(this.renderProgram.a_trail_offset);\n gl.vertexAttribPointer(this.renderProgram.a_trail_offset, 1, gl.FLOAT, false, 0, 0);\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 1); // This makes it instanced\n \n // Draw trails using instanced rendering\n // Each main particle will be drawn (trailLength+1) times with different offsets\n gl.drawArraysInstanced(gl.POINTS, 0, this.particleCount, this.trailLength + 1);\n \n // Reset vertex attrib divisor\n gl.vertexAttribDivisor(this.renderProgram.a_trail_offset, 0);\n \n gl.disable(gl.BLEND);\n \n // Request next frame\n this.map.triggerRepaint();\n }\n\n onRemove(map, gl) {\n // Clean up WebGL resources\n if (this.updateProgram) {\n const shaders = gl.getAttachedShaders(this.updateProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.updateProgram.program);\n }\n if (this.renderProgram) {\n const shaders = gl.getAttachedShaders(this.renderProgram.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.renderProgram.program);\n }\n\n // Delete buffers\n if (this.particleBufferA) gl.deleteBuffer(this.particleBufferA);\n if (this.particleBufferB) gl.deleteBuffer(this.particleBufferB);\n if (this.ageBufferA) gl.deleteBuffer(this.ageBufferA);\n if (this.ageBufferB) gl.deleteBuffer(this.ageBufferB);\n if (this.trailOffsetBuffer) gl.deleteBuffer(this.trailOffsetBuffer);\n\n // Delete transform feedback\n if (this.transformFeedback) gl.deleteTransformFeedback(this.transformFeedback);\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Clear references\n this.particleBufferA = null;\n this.particleBufferB = null;\n this.ageBufferA = null;\n this.ageBufferB = null;\n this.currentBuffer = null;\n this.nextBuffer = null;\n this.currentAgeBuffer = null;\n this.nextAgeBuffer = null;\n this.trailOffsetBuffer = null;\n this.transformFeedback = null;\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.updateProgram = null;\n this.renderProgram = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n} ","import {Evented} from 'mapbox-gl';\nimport ExifReader from 'exifreader';\n\nconst vertexShader = `\n precision mediump float;\n \n attribute vec2 a_pos;\n uniform mat4 u_matrix;\n uniform vec4 u_bounds; // [minX, maxY, maxX, minY]\n \n varying vec2 v_tex_pos;\n \n const float PI = 3.141592653589793;\n \n vec2 latLngToMercator(vec2 lnglat) {\n // Convert lng/lat to Web Mercator coordinates in [0, 1] range\n float x = (lnglat.x + 180.0) / 360.0; // Convert longitude to [0,1]\n \n // Convert latitude to y coordinate using Web Mercator projection\n float latRad = lnglat.y * PI / 180.0;\n float y = 0.5 - (log(tan(PI / 4.0 + latRad / 2.0)) / (2.0 * PI));\n \n // y is already in [0,1] range\n return vec2(x, y);\n }\n \n void main() {\n // Map input position [0,1] to geographic bounds\n float lng = mix(u_bounds[0], u_bounds[2], a_pos.x);\n float lat = mix(u_bounds[3], u_bounds[1], a_pos.y);\n \n // Convert to Web Mercator coordinates in [0,1] range\n vec2 mercator = latLngToMercator(vec2(lng, lat));\n \n // Pass texture coordinates\n v_tex_pos = vec2(a_pos.x, 1.0 - a_pos.y);\n \n // Apply matrix transformation\n gl_Position = u_matrix * vec4(mercator, 0, 1);\n }\n`;\n\nconst fragmentShader = `\n precision mediump float;\n \n uniform sampler2D u_image;\n uniform sampler2D u_colormap;\n uniform float u_opacity;\n uniform vec2 u_value_range; // [min, max] of original values\n \n varying vec2 v_tex_pos;\n \n void main() {\n // Get the R value from the source image\n vec4 pixel = texture2D(u_image, v_tex_pos);\n float normalized = pixel.r;\n \n // Use value as index into colormap\n vec4 color = texture2D(u_colormap, vec2(normalized, 0.5));\n \n // Apply global opacity\n gl_FragColor = vec4(color.rgb, color.a * u_opacity);\n }\n`;\n\nfunction createProgram(gl, vertexSource, fragmentSource) {\n const program = gl.createProgram();\n\n const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);\n const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);\n\n gl.attachShader(program, vertexShader);\n gl.attachShader(program, fragmentShader);\n gl.linkProgram(program);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n throw new Error(gl.getProgramInfoLog(program));\n }\n\n const wrapper = {program: program};\n\n const numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);\n for (let i = 0; i < numAttributes; i++) {\n const attribute = gl.getActiveAttrib(program, i);\n wrapper[attribute.name] = gl.getAttribLocation(program, attribute.name);\n }\n const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);\n for (let i = 0; i < numUniforms; i++) {\n const uniform = gl.getActiveUniform(program, i);\n wrapper[uniform.name] = gl.getUniformLocation(program, uniform.name);\n }\n\n return wrapper;\n}\n\nfunction createShader(gl, type, source) {\n const shader = gl.createShader(type);\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n throw new Error(gl.getShaderInfoLog(shader));\n }\n return shader;\n}\n\nfunction createTexture(gl, filter, data, width, height) {\n const texture = gl.createTexture();\n gl.bindTexture(gl.TEXTURE_2D, texture);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);\n \n if (data instanceof Uint8Array) {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);\n } else {\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);\n }\n return texture;\n}\n\nfunction createBuffer(gl, data) {\n const buffer = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);\n return buffer;\n}\n\nfunction createColormap(gl, colors, valueRange) {\n // Sort colors by value\n colors.sort((a, b) => a[0] - b[0]);\n \n // Create a 256x1 texture for the colormap\n const data = new Uint8Array(256 * 4);\n const [minVal, maxVal] = valueRange;\n \n // Fill the colormap texture\n for (let i = 0; i < 256; i++) {\n // Convert texture position [0,255] to actual data value\n const value = minVal + (maxVal - minVal) * (i / 255);\n \n // Find the color stops that bracket this value\n let lowIndex = 0;\n while (lowIndex < colors.length - 1 && colors[lowIndex + 1][0] < value) {\n lowIndex++;\n }\n const highIndex = Math.min(lowIndex + 1, colors.length - 1);\n \n // Interpolate between the two colors\n const low = colors[lowIndex];\n const high = colors[highIndex];\n const t = highIndex > lowIndex ? \n (value - low[0]) / (high[0] - low[0]) : 0;\n \n const idx = i * 4;\n for (let j = 0; j < 3; j++) {\n data[idx + j] = Math.round(low[1][j] + t * (high[1][j] - low[1][j]));\n }\n\n if (low[1][3] != null && low[1][3] != 255) {\n data[idx + 3] = low[1][3];\n }\n else {\n data[idx + 3] = 255;\n }\n }\n \n return createTexture(gl, gl.NEAREST, data, 256, 1);\n}\n\nexport default class SmoothRaster extends Evented {\n constructor({id, source, color, bounds, opacity = 1.0, readyForDisplay = false}) {\n super();\n \n this.id = id;\n this.type = 'custom';\n this.renderingMode = '2d';\n \n this.source = source;\n this.color = color;\n this.opacity = opacity;\n this.bounds = bounds;\n \n this.sourceLoaded = false;\n this.readyForDisplay = readyForDisplay;\n }\n\n onAdd(map, gl) {\n this.map = map;\n this.gl = gl;\n\n // Create program\n this.program = createProgram(gl, vertexShader, fragmentShader);\n\n // Create vertex buffer for a full-screen quad\n const vertices = new Float32Array([\n 0, 0,\n 1, 0,\n 0, 1,\n 0, 1,\n 1, 0,\n 1, 1\n ]);\n this.vertexBuffer = createBuffer(gl, vertices);\n\n // Load source image\n this.setSource(this.source);\n }\n\n setSource(source, color = null) {\n if (this.source != source) {\n this.source = source;\n }\n\n if (color != null) {\n this.color = color;\n }\n\n const image = new Image();\n image.crossOrigin = \"anonymous\";\n \n fetch(source, {\n cache: 'no-store'\n })\n .then(response => \n Promise.all([\n response.clone().blob(),\n response.arrayBuffer().then(buffer => new Uint8Array(buffer).buffer)\n ])\n )\n .then(([blob, arrayBuffer]) => {\n const objectURL = URL.createObjectURL(blob);\n\n // Set default value range if EXIF reading fails\n this.valueRange = [0, 255]; // Default range\n \n (async () => {\n try {\n const tags = await ExifReader.load(arrayBuffer);\n \n if (tags[\"ImageDescription\"] && tags[\"ImageDescription\"].description) {\n const description = tags[\"ImageDescription\"].description;\n \n const matches = description.match(/(-?\\d+\\.?\\d*),(-?\\d+\\.?\\d*)/);\n if (matches) {\n const min = parseFloat(matches[1]);\n const max = parseFloat(matches[2]);\n \n if (!isNaN(min) && !isNaN(max)) {\n this.valueRange = [min, max];\n }\n }\n }\n \n // Then define onload handler\n image.onload = () => {\n URL.revokeObjectURL(objectURL);\n if (this.gl) {\n this.sourceTexture = createTexture(this.gl, this.gl.LINEAR, image);\n if (this.valueRange) {\n this.colormapTexture = createColormap(this.gl, this.color, this.valueRange);\n }\n this.sourceLoaded = true;\n if (this.map) {\n this.map.triggerRepaint();\n }\n }\n };\n\n image.onerror = (err) => {\n URL.revokeObjectURL(objectURL);\n console.error('Error loading source image:', err);\n };\n // Always proceed to load the image, even if EXIF parsing fails\n image.src = objectURL;\n } catch (error) {\n console.warn('Error reading EXIF data:', error);\n // Still proceed with loading the image\n image.src = objectURL;\n }\n })();\n })\n .catch(error => {\n console.error('Error fetching image:', error);\n });\n }\n\n onRemove() {\n // Clean up WebGL resources\n const gl = this.gl;\n if (!gl) return;\n\n // Delete textures\n if (this.sourceTexture) gl.deleteTexture(this.sourceTexture);\n if (this.colormapTexture) gl.deleteTexture(this.colormapTexture);\n\n // Delete buffer\n if (this.vertexBuffer) gl.deleteBuffer(this.vertexBuffer);\n\n // Delete shaders and program\n if (this.program) {\n const shaders = gl.getAttachedShaders(this.program.program);\n if (shaders) {\n shaders.forEach(shader => gl.deleteShader(shader));\n }\n gl.deleteProgram(this.program.program);\n }\n\n // Clear references\n this.sourceTexture = null;\n this.colormapTexture = null;\n this.vertexBuffer = null;\n this.program = null;\n this.gl = null;\n this.map = null;\n this.sourceLoaded = false;\n }\n\n render(gl, matrix) {\n // Only render if source is loaded\n if (!this.sourceLoaded || !this.readyForDisplay) return;\n \n gl.useProgram(this.program.program);\n\n // Set up blending for transparency\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n //gl.blendEquation(gl.FUNC_ADD);\n\n // Set uniforms\n gl.uniformMatrix4fv(this.program.u_matrix, false, matrix);\n gl.uniform4fv(this.program.u_bounds, this.bounds);\n gl.uniform1f(this.program.u_opacity, this.opacity);\n gl.uniform2fv(this.program.u_value_range, this.valueRange);\n\n // Bind textures\n gl.activeTexture(gl.TEXTURE0);\n gl.bindTexture(gl.TEXTURE_2D, this.sourceTexture);\n gl.uniform1i(this.program.u_image, 0);\n\n gl.activeTexture(gl.TEXTURE1);\n gl.bindTexture(gl.TEXTURE_2D, this.colormapTexture);\n gl.uniform1i(this.program.u_colormap, 1);\n\n // Draw quad\n gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);\n gl.enableVertexAttribArray(this.program.a_pos);\n gl.vertexAttribPointer(this.program.a_pos, 2, gl.FLOAT, false, 0, 0);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n\n gl.disable(gl.BLEND);\n }\n} "],"names":["createProgram","gl","vertexSource","fragmentSource","program","vertexShader","createShader","VERTEX_SHADER","fragmentShader","FRAGMENT_SHADER","attachShader","includes","transformFeedbackVaryings","SEPARATE_ATTRIBS","linkProgram","getProgramParameter","LINK_STATUS","Error","getProgramInfoLog","wrapper","numAttributes","ACTIVE_ATTRIBUTES","i","attribute","getActiveAttrib","name","getAttribLocation","numUniforms","ACTIVE_UNIFORMS","uniform","getActiveUniform","getUniformLocation","type","source","shader","shaderSource","compileShader","getShaderParameter","COMPILE_STATUS","getShaderInfoLog","createTexture","filter","data","width","height","texture","bindTexture","TEXTURE_2D","texParameteri","TEXTURE_WRAP_S","CLAMP_TO_EDGE","TEXTURE_WRAP_T","TEXTURE_MIN_FILTER","TEXTURE_MAG_FILTER","Uint8Array","texImage2D","RGBA","UNSIGNED_BYTE","Image","createBuffer","buffer","bindBuffer","ARRAY_BUFFER","bufferData","STATIC_DRAW","kphToMph","kph","mpsToMph","mps","ParticleMotion","Evented","constructor","id","color","bounds","particleCount","readyForDisplay","ageThreshold","maxAge","velocityFactor","fadeOpacity","updateInterval","pointSize","trailLength","trailFadeRate","trailSizeDecay","unit","super","this","renderingMode","sourceLoaded","speedRange","onAdd","map","updateProgram","renderProgram","positions","Float32Array","ages","gridSize","Math","ceil","sqrt","baseX","baseY","floor","gridSpacing","jitterX","random","jitterY","max","min","particleBufferA","particleBufferB","currentBuffer","nextBuffer","ageBufferA","ageBufferB","currentAgeBuffer","nextAgeBuffer","trailOffsets","trailOffsetBuffer","transformFeedback","createTransformFeedback","lastTime","setSource","percentParticleWhenSetSource","image","crossOrigin","fetch","cache","then","response","Promise","all","clone","blob","arrayBuffer","objectURL","URL","createObjectURL","onload","revokeObjectURL","sourceTexture","LINEAR","shouldResetParticles","triggerRepaint","onerror","err","console","warn","tags","ExifReader","load","description","matches","match","min_u","parseFloat","max_u","min_v","max_v","min_speed","max_speed","isNaN","valueRange_u","valueRange_v","colormapTexture","colors","valueRange","sort","a","b","minVal","maxVal","value","lowIndex","length","highIndex","low","high","t","idx","j","round","createColormap","src","error","catch","render","matrix","currentTime","performance","now","colorMask","disable","BLEND","useProgram","uniform1f","u_time","u_speed_factor","uniform4fv","u_bounds","uniform2fv","u_value_range_u","u_value_range_v","u_speed_range","u_age_threshold","u_max_age","u_percent_reset","uniform1i","u_should_reset","activeTexture","TEXTURE0","u_velocity_texture","enableVertexAttribArray","a_position","vertexAttribPointer","FLOAT","a_age","bindTransformFeedback","TRANSFORM_FEEDBACK","bindBufferBase","TRANSFORM_FEEDBACK_BUFFER","beginTransformFeedback","POINTS","drawArrays","endTransformFeedback","bindFramebuffer","FRAMEBUFFER","viewport","canvas","enable","blendFunc","SRC_ALPHA","ONE_MINUS_SRC_ALPHA","uniformMatrix4fv","u_matrix","u_point_size","u_opacity","u_trail_fade_rate","u_trail_size_decay","TEXTURE1","u_wind_color","a_trail_offset","vertexAttribDivisor","drawArraysInstanced","onRemove","shaders","getAttachedShaders","forEach","deleteShader","deleteProgram","deleteBuffer","deleteTransformFeedback","deleteTexture","SmoothRaster","opacity","vertices","vertexBuffer","NEAREST","u_value_range","u_image","u_colormap","a_pos","TRIANGLES"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "mapbox-exif-layer",
3
+ "version": "1.0.1",
4
+ "description": "Custom Mapbox GL JS layers for rendering particle motion (e.g., wind) or smooth raster (e.g., temperature) based on EXIF-enabled JPEG images",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "rollup -c",
13
+ "dev": "rollup -c -w",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mapbox-gl-js",
18
+ "raster",
19
+ "wind",
20
+ "velocity",
21
+ "weather",
22
+ "particle motion"
23
+ ],
24
+ "author": "Zifan Wang",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "mapbox-gl": "^3.8.0"
28
+ },
29
+ "devDependencies": {
30
+ "@rollup/plugin-commonjs": "^25.0.0",
31
+ "@rollup/plugin-node-resolve": "^15.0.0",
32
+ "rollup": "^4.0.0",
33
+ "@rollup/plugin-terser": "^0.4.4"
34
+ },
35
+ "dependencies": {
36
+ "exifreader": "^4.26.2"
37
+ }
38
+ }