Setting the file. One moment.
Prep Geo · Remotion Best Practices · remotion-dev/skills · Skills Docs
ContentsBack to the top of the page remotion-maps/techniques/maptiler/scripts/ prep-geo.mjs
JavaScript · 202 lines · 7 KB
13
14 import {readFileSync, writeFileSync, mkdirSync} from 'fs' ;
15 import {dirname, resolve} from 'path' ;
16 import {fileURLToPath} from 'url' ;
17
18 if (process.argv. includes ( '--help' )) {
19 console. log (
20 'Configure COUNTRIES, RIVER, BORDER, label bounds, and output paths in this script, then run: bun scripts/prep-geo.mjs' ,
21 );
22 process. exit ( 0 );
23 }
24
25 const turf = await import ( '@turf/turf' );
26
27 const __dir = dirname ( fileURLToPath ( import . meta .url));
28 const root = resolve (__dir, '..' );
29 const geo = resolve (root, '../geodata' ); // ADAPT: where your input GeoJSON lives
30 const read = ( p ) => JSON . parse ( readFileSync (p, 'utf8' ));
31
32 // ===== CONFIG — edit for your river + countries =====
33 const COUNTRIES = [ 'china' , 'india' , 'bangladesh' ]; // ORDERED headwaters → mouth; first = source (stop 0)
34 const RIVER = resolve (geo, 'focus-rivers/yarlung-brahmaputra-full-osm.geojson' ); // a single clean source→mouth LineString
35 const BORDER = ( name ) => resolve (geo, `project-borders/${ name }.geojson` ); // one polygon file per country, named <country>.geojson
36 const FRAME_BBOX = [ 76 , 14 , 104 , 33.5 ]; // [W,S,E,N] visible extent — fallback for label anchoring only
37 const ANCHOR_BBOX = {
38 china: [ 82 , 27 , 96 , 32 ],
39 india: [ 76 , 14 , 99 , 31 ],
40 bangladesh: [ 86 , 20 , 93 , 27 ],
41 }; // [W,S,E,N] per-country label "story region"
42 const NUDGE = {china: [ 0 , 0.6 ], india: [ - 1.0 , 0 ], bangladesh: [ 0 , - 0.6 ]}; // [lng,lat] label nudge
43 const RIVER_SIMPLIFY_TOL = 0.006 ; // degrees — smooths the draw-on (bigger = simpler)
44 const OUT_RIVER = resolve (root, 'out/river-flow.json' );
45 const OUT_META = resolve (root, 'out/country-meta.json' );
46 const OUT_BORDERS = resolve (root, 'out/borders.geojson' );
47 // =====================================================
48
49 const havKm = ( a , b ) => {
50 const R = 6371 ,
51 r = Math. PI / 180 ;
52 const dLat = (b[ 1 ] - a[ 1 ]) * r,
53 dLng = (b[ 0 ] - a[ 0 ]) * r;
54 const h =
55 Math. sin (dLat / 2 ) ** 2 +
56 Math. cos (a[ 1 ] * r) * Math. cos (b[ 1 ] * r) * Math. sin (dLng / 2 ) ** 2 ;
57 return 2 * R * Math. asin (Math. sqrt (h));
58 };
59
60 // --- River draw-on line: take the clean routed LineString, strip a dangling final hop, simplify so the
61 // wide-zoom draw-on reads as one smooth thread (no bezier — meander overshoot on a long river). ---
62 const routed = read ( RIVER ).features[ 0 ].geometry.coordinates;
63 let end = routed. length ;
64 while (end > 2 && havKm (routed[end - 2 ], routed[end - 1 ]) > 15 ) end -- ; // drop a final cross-braid jump if present
65 const flow = turf. simplify (turf. lineString (routed. slice ( 0 , end)), {
66 tolerance: RIVER_SIMPLIFY_TOL ,
67 highQuality: true ,
68 }).geometry.coordinates;
69
70 // --- Borders + country fills (one source, filtered per country in the component) ---
71 const borders = {type: 'FeatureCollection' , features: []};
72 const polys = {};
73 for ( const name of COUNTRIES ) {
74 const fc = read ( BORDER (name));
75 polys[name] = fc;
76 for ( const f of fc.features) {
77 f.properties = { ... (f.properties || {}), country: name};
78 borders.features. push (f);
79 }
80 }
81
82 // --- Reveal stops: arc-length fraction of `flow` where the river first ENTERS each country. The first
83 // country (headwaters) is the source, so its stop is 0; the rest are computed. ---
84 const flowKm = turf. length (turf. lineString (flow));
85 const insideCountry = ( pt , name ) =>
86 polys[name].features. some (( f ) => turf. booleanPointInPolygon (pt, f));
87 const stops = {};
88 COUNTRIES . forEach (( c , i ) => {
89 stops[c] = i === 0 ? 0 : 1 ;
90 }); // 0 = headwaters; 1 = sentinel until entered
91 let acc = 0 ;
92 for ( let i = 0 ; i < flow. length ; i ++ ) {
93 if (i > 0 ) acc += havKm (flow[i - 1 ], flow[i]);
94 const frac = acc / (flowKm || 1 );
95 const pt = turf. point (flow[i]);
96 for ( const c of COUNTRIES )
97 if (stops[c] === 1 && insideCountry (pt, c)) stops[c] = frac;
98 }
99
100 // --- Per-country meta: anchor = pole of inaccessibility of the visible landmass (centred, away from
101 // borders/edges) within the country's story region + a NUDGE; border = every exterior ring from the
102 // complete named source. Never crop a country or bilateral boundary to the viewport. ---
103 const biggestPoly = ( geom ) => {
104 const rings =
105 geom.type === 'MultiPolygon' ? geom.coordinates : [geom.coordinates];
106 let best = null ,
107 bestA = - 1 ;
108 for ( const c of rings) {
109 const p = turf. polygon (c);
110 const a = turf. area (p);
111 if (a > bestA) {
112 bestA = a;
113 best = p;
114 }
115 }
116 return best;
117 };
118 const largestPolygon = ( fc ) => {
119 let best = null ,
120 bestA = - 1 ;
121 for ( const f of fc.features) {
122 const p = biggestPoly (f.geometry),
123 a = turf. area (p);
124 if (a > bestA) {
125 bestA = a;
126 best = p;
127 }
128 }
129 return best;
130 };
131 const completeExteriorSegments = ( fc ) => {
132 const segments = [];
133 for ( const feature of fc.features) {
134 const polygons =
135 feature.geometry.type === 'MultiPolygon'
136 ? feature.geometry.coordinates
137 : [feature.geometry.coordinates];
138 for ( const polygon of polygons)
139 if (polygon[ 0 ]?. length > 1 ) segments. push (polygon[ 0 ]);
140 }
141 return segments;
142 };
143 const poleOfInaccessibility = ( poly ) => {
144 const bb = turf. bbox (poly),
145 boundary = turf. polygonToLine (poly),
146 N = 46 ;
147 let best = null ,
148 bestD = - 1 ;
149 for ( let i = 0 ; i <= N ; i ++ )
150 for ( let j = 0 ; j <= N ; j ++ ) {
151 const lng = bb[ 0 ] + ((bb[ 2 ] - bb[ 0 ]) * i) / N ,
152 lat = bb[ 1 ] + ((bb[ 3 ] - bb[ 1 ]) * j) / N ;
153 const pt = turf. point ([lng, lat]);
154 if ( ! turf. booleanPointInPolygon (pt, poly)) continue ;
155 const d = turf. pointToLineDistance (pt, boundary);
156 if (d > bestD) {
157 bestD = d;
158 best = [lng, lat];
159 }
160 }
161 return best;
162 };
163 const countryMeta = {};
164 for ( const name of COUNTRIES ) {
165 const poly = largestPolygon (polys[name]);
166 const storyRegion = biggestPoly (
167 turf. bboxClip (poly, ANCHOR_BBOX [name] || FRAME_BBOX ).geometry,
168 );
169 const pole = poleOfInaccessibility (storyRegion);
170 const segs = completeExteriorSegments (polys[name]);
171 const nudge = NUDGE [name] || [ 0 , 0 ];
172 countryMeta[name] = {
173 stop: stops[name],
174 anchor: [pole[ 0 ] + nudge[ 0 ], pole[ 1 ] + nudge[ 1 ]],
175 border: segs,
176 };
177 }
178
179 mkdirSync ( dirname ( OUT_RIVER ), {recursive: true });
180 writeFileSync ( OUT_RIVER , JSON . stringify (flow));
181 writeFileSync ( OUT_META , JSON . stringify (countryMeta));
182 writeFileSync ( OUT_BORDERS , JSON . stringify (borders));
183 console. log (
184 'river:' ,
185 flow. length ,
186 'pts ·' ,
187 flowKm. toFixed ( 0 ),
188 'km · entry stops' ,
189 JSON . stringify (stops),
190 );
191 for ( const n of COUNTRIES ) {
192 const km = countryMeta[n].border. reduce (
193 ( s , seg ) => s + turf. length (turf. lineString (seg)),
194 0 ,
195 );
196 console. log (
197 ` ${ n }: anchor ${ countryMeta [ n ]. anchor . map (( v ) => v . toFixed ( 2 )). join ( ',' ) } · border ${ km . toFixed ( 0 ) } km` ,
198 );
199 }
200 console. log (
201 " \n Next: copy out/river-flow.json + out/country-meta.json → your project's src/geo/ ; out/borders.geojson → public/geo/" ,
202 );