@@ -15,17 +15,23 @@ import Feature from 'ol/Feature.js';
1515import Point from 'ol/geom/Point.js' ;
1616import Style from 'ol/style/Style.js' ;
1717import Icon from 'ol/style/Icon.js' ;
18+ import CircleStyle from 'ol/style/Circle.js' ;
19+ import Stroke from 'ol/style/Stroke.js' ;
20+ import Fill from 'ol/style/Fill.js' ;
1821
1922/** Default Panoramax instance used when `panoramaxUrl` is not set in the config */
2023const DEFAULT_PANORAMAX_URL = 'https://panoramax.openstreetmap.fr/api' ;
2124
25+ /** Panoramax brand color used for the coverage layer */
26+ const PNX_COLOR = '#e2007a' ;
27+
2228/**
2329 * Arrow icon pointing to the North (heading 0). Rotated clockwise to match the
2430 * heading (in radians) reported by the Panoramax photo viewer.
2531 */
2632const ARROW_SVG = 'data:image/svg+xml,'
2733 + "%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48' width='48' height='48'%3E"
28- + "%3Cpath d='M24 3 41 43 24 33 7 43Z' fill='%23e2007a ' stroke='%23ffffff' stroke-width='2.5' stroke-linejoin='round'/%3E"
34+ + "%3Cpath d='M24 3 41 43 24 33 7 43Z' fill='%231700e2 ' stroke='%23ffffff' stroke-width='2.5' stroke-linejoin='round'/%3E"
2935 + '%3C/svg%3E' ;
3036
3137/**
@@ -53,10 +59,31 @@ export default class Panoramax {
5359 this . _lizmap3 = lizmap3 ;
5460 this . _active = false ;
5561
62+ // Date-range filter state (null = no filter).
63+ // - sequences layer uses the `date` field ("YYYY-MM-DD")
64+ // - pictures layer uses the `ts` field ("YYYY-MM-DDTHH:mm:ss")
65+ this . _filterStart = null ;
66+ this . _filterEnd = null ;
67+ // `ts` includes time, so the max comparison uses end+1 day to include
68+ // all pictures taken on the end day (mirrors Panoramax's own logic).
69+ this . _filterEndPlusOne = null ;
70+
5671 // STAC API base URL and the derived MVT tiles URL
5772 this . _url = ( options . panoramaxUrl || DEFAULT_PANORAMAX_URL ) . replace ( / \/ + $ / , '' ) ;
5873 const tilesUrl = this . _url + '/map/{z}/{x}/{y}.mvt' ;
5974
75+ // Cached styles for the coverage layer (created once, reused every frame).
76+ this . _pointStyle = new Style ( {
77+ image : new CircleStyle ( {
78+ radius : 4 ,
79+ fill : new Fill ( { color : PNX_COLOR } ) ,
80+ stroke : new Stroke ( { color : '#ffffff' , width : 1 } ) ,
81+ } ) ,
82+ } ) ;
83+ this . _lineStyle = new Style ( {
84+ stroke : new Stroke ( { color : PNX_COLOR , width : 3 } ) ,
85+ } ) ;
86+
6087 // Panoramax coverage layer (MVT). The tiles are served in EPSG:3857 and
6188 // reprojected by OpenLayers to the current map view projection if needed.
6289 this . _olLayer = new VectorTileLayer ( {
@@ -65,7 +92,27 @@ export default class Panoramax {
6592 format : new MVT ( ) ,
6693 projection : 'EPSG:3857' ,
6794 url : tilesUrl ,
68- } )
95+ } ) ,
96+ style : ( feature ) => {
97+ const isPoint = feature . getType ( ) === 'Point' || feature . getType ( ) === 'MultiPoint' ;
98+ // Apply date filter when at least one bound is set.
99+ if ( this . _filterStart || this . _filterEnd ) {
100+ // sequences → `date` ("YYYY-MM-DD"), pictures → `ts` ("YYYY-MM-DDTHH:mm:ss")
101+ const dateStr = isPoint ? feature . get ( 'ts' ) : feature . get ( 'date' ) ;
102+ if ( dateStr ) {
103+ if ( this . _filterStart && dateStr < this . _filterStart ) {
104+ return null ;
105+ }
106+ if ( this . _filterEnd ) {
107+ const maxComp = isPoint ? this . _filterEndPlusOne : this . _filterEnd ;
108+ if ( dateStr > maxComp ) {
109+ return null ;
110+ }
111+ }
112+ }
113+ }
114+ return isPoint ? this . _pointStyle : this . _lineStyle ;
115+ } ,
69116 } ) ;
70117
71118 // Add the layer to the layer tree through an external group. This must be
@@ -217,9 +264,14 @@ export default class Panoramax {
217264 picId = props . id ?? props . picture_id ?? props . pic_id ?? fid ?? null ;
218265 seqId = props . sequence_id ?? props . seq_id ?? null ;
219266 if ( ! seqId && props . sequences ) {
220- seqId = Array . isArray ( props . sequences )
221- ? props . sequences [ 0 ]
222- : String ( props . sequences ) . split ( ',' ) [ 0 ] ;
267+ let sequences = props . sequences ;
268+ // MVT RenderFeature serializes JS arrays as JSON strings (e.g. '["uuid1"]').
269+ if ( typeof sequences === 'string' && sequences . trimStart ( ) . startsWith ( '[' ) ) {
270+ try { sequences = JSON . parse ( sequences ) ; } catch { sequences = [ ] ; }
271+ }
272+ seqId = Array . isArray ( sequences )
273+ ? ( sequences [ 0 ] ?? null )
274+ : ( String ( sequences ) || null ) ;
223275 }
224276 }
225277 // A sequence line (no precise picture id) falls through to the
@@ -242,4 +294,29 @@ export default class Panoramax {
242294 } ) ;
243295 }
244296 }
297+
298+ /**
299+ * Filter the coverage layer to only show pictures/sequences in a date range.
300+ * Either bound can be null to leave that side open.
301+ * @param {string|null } startDate - ISO date string "YYYY-MM-DD", or null
302+ * @param {string|null } endDate - ISO date string "YYYY-MM-DD", or null
303+ */
304+ setDateFilter ( startDate , endDate ) {
305+ this . _filterStart = startDate || null ;
306+ this . _filterEnd = endDate || null ;
307+
308+ if ( endDate ) {
309+ // Advance by one day so that pictures with a timestamp on `endDate`
310+ // (e.g. "2024-03-15T14:30:00") are included in the result.
311+ const d = new Date ( endDate ) ;
312+ d . setDate ( d . getDate ( ) + 1 ) ;
313+ this . _filterEndPlusOne = d . toISOString ( ) . split ( 'T' ) [ 0 ] ;
314+ } else {
315+ this . _filterEndPlusOne = null ;
316+ }
317+
318+ // Mark the layer dirty so the style function is re-evaluated on the next
319+ // render frame. The tiles themselves remain cached (no network requests).
320+ this . _olLayer . changed ( ) ;
321+ }
245322}
0 commit comments