1/** 2 * @file Gets an isochrone from the Mapbox Isochrone API and updates the map sources to reflect the new isochrone 3 */ 4 5import { fetchWithCache, getDistance } from './utilities.js'; 6 7const empty = { type: 'FeatureCollection', features: [] }; 8 9/** 10 * Get the isochrone for a given center and add it to the map 11 * @param {Object} map - Mapbox map 12 * @param {Array} selectedPlace - Selected place 13 * @param {string} type - Type of isochrone 14 */ 15export async function updateIsochrone(map, selectedPlace, type = 'time') { 16 const isochrone = await getIsochrone({ 17 location: selectedPlace, 18 type 19 }); 20 21 const alternateType = type === 'distance' ? 'time' : 'distance'; 22 23 map.getSource(`${type}_isochrone`).setData(isochrone); 24 map.getSource(`${alternateType}_isochrone`).setData(empty); 25} 26 27/** 28 * Get the extended isochrone for a given center and add it to the map 29 * @param {Object} map - Mapbox map 30 * @param {Array} selectedPlace - Selected place 31 * @param {string} type - Type of isochrone 32 */ 33export async function updateExtendedIsochrone(map, selectedPlace, type = 'time') { 34 const isochrone = await getExtendedIsochrone(selectedPlace, type); 35 36 const alternateType = type === 'distance' ? 'time' : 'distance'; 37 38 map.getSource(`extended_${type}_isochrone`).setData(isochrone); 39 map.getSource(`extended_${alternateType}_isochrone`).setData(empty); 40} 41 42/** 43 * Get the extended isochrone for a given center and add it to the map 44 * @param {Object} map - Mapbox map 45 * @returns {Promise} Mapbox Isochrone API response 46 */ 47export async function getIsochrone(options = {}) { 48 const defaultOptions = { 49 location: [-74.0, 40.75] 50 }; 51 52 options = Object.assign(defaultOptions, options); 53 54 const type = options.type || 'time'; 55 56 const queries = { 57 distance: { 58 access_token: window.mapboxgl.accessToken, 59 contours_meters: `80467`, 60 generalize: 250, 61 polygons: true 62 }, 63 time: { 64 access_token: window.mapboxgl.accessToken, 65 contours_minutes: `60`, 66 generalize: 250, 67 polygons: true 68 } 69 }; 70 71 const query = queries[type]; 72 73 /* 74 const query = { 75 access_token: window.mapboxgl.accessToken, 76 contours_minutes: `60`, 77 generalize: 250, 78 polygons: true 79 }; 80 */ 81 82 const [lat, lng] = options.location; 83 84 const base = `https://api.mapbox.com/isochrone/v1/mapbox/driving`; 85 86 const url = `${base}/${lat},${lng}?${optionsToQueryString(query)}`; 87 const encoded = encodeURI(url); 88 89 return fetchWithCache(encoded); 90} 91 92/** 93 * Get the extended isochrone for a given center 94 * @param {Array} center - Center of the isochrone 95 * @returns {Object} - Feature collection 96 */ 97export async function getExtendedIsochrone(center, type) { 98 let keyPoints = await getSamples(center); 99 keyPoints.push(center); 100 101 const extendedBuffers = await Promise.all( 102 keyPoints.map((point) => getIsochrone({ location: point, type })) 103 ); 104 105 const features = extendedBuffers.reduce((collected, buffer) => { 106 return [...collected, ...buffer.features]; 107 }, []); 108 109 let combined = combine(features); 110 combined.geometry.coordinates = combined.geometry.coordinates.slice(0, 1); 111 112 return combined; 113} 114 115/** 116 * Get the extended isochrone for a given center 117 * @param {Array} center - Center of the isochrone 118 * @param {string} type - Type of isochrone 119 * @returns {Array} - Array of max points 120 */ 121export async function getSamples(center, type) { 122 const samplesToCalculate = 8; 123 124 const firstRing = await getIsochrone({ location: center, type }); 125 126 const points = firstRing.features[0].geometry.coordinates[0]; 127 // const interval = Math.floor(points.length / samplesToCalculate); 128 129 const samples = chunk(points, samplesToCalculate).map(getMax(center)); 130 return samples; 131} 132 133/** 134 * Get the extended isochrone for a given center 135 * @param {Array} center - Center of the isochrone 136 * @param {string} type - Type of isochrone 137 * @returns {Array} - Array of max points 138 */ 139function getMax(center) { 140 return (points) => { 141 let maxPoint = null; 142 let maxDistance = 0; 143 144 points.forEach((point, index) => { 145 const distance = getDistance(center, point); 146 147 if (index === 0) { 148 maxPoint = point; 149 maxDistance = distance; 150 151 return; 152 } 153 154 if (distance > maxDistance) { 155 maxPoint = point; 156 maxDistance = distance; 157 } 158 }); 159 160 return maxPoint; 161 }; 162} 163 164/** 165 * Splits an array into chunks of a given size 166 * @param {Array} points 167 * @param {number} segments 168 * @returns {Array} - An array of arrays 169 */ 170function chunk(points, segments) {
171 const interval = Math.floor(points.length / segments); 172 173 let chunks = []; 174 175 for (let i = 0; i < segments; i++) { 176 chunks.push(points.slice(i * interval, i * interval + interval)); 177 } 178 179 return chunks; 180} 181 182/** 183 * Combines an array of features into a single unionized feature collection 184 * @param {Array} features - array of features 185 * @returns {Object} - feature collection 186 */ 187function combine(features) { 188 let union = features[0]; 189 190 for (let i = 1; i < features.length; i++) { 191 union = turf.union(union, features[i]); 192 } 193 194 return union; 195} 196 197/** 198 * Converts an object of key/value pairs into a query string 199 * @param {Object} options - key/value pairs 200 * @returns {string} - query string 201 */ 202function optionsToQueryString(options) { 203 return Object.entries(options) 204 .map(([key, value]) => { 205 return `${key}=${value}`; 206 }) 207 .join('&'); 208}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.