1/** 2 * BEREINIGTER MAPBOX TREE GENERATOR 3 * Vereinfacht ohne Hide/Show Logik 4 */ 5 6class MapboxTreeGenerator { 7 constructor(mapInstance) { 8 this.map = mapInstance; 9 this.treeData = []; 10 this.isInitialized = false; 11 } 12 13 async createTreesInPolygon(polygon, season = 'summer', options = {}) { 14 try { 15 const geoJsonPolygon = this.polygonToGeoJSON(polygon); 16 if (!geoJsonPolygon) { 17 console.error(' Could not convert polygon'); 18 return false; 19 } 20 21 const config = { 22 treeDensity: this.getTreeDensityForPolygon(polygon), 23 minDistance: Math.max(3, parseFloat(polygon.height) * 0.4), 24 treeTypes: ['oak', 'pine', 'birch'], 25 polygonHeight: parseFloat(polygon.height) || 0, 26 randomSeed: polygon.id, 27 ...options 28 }; 29 30 this.removeTreesForPolygon(polygon.id); 31 32 // SCHRITT 2: Neue Trees für DIESES Polygon erstellen 33 const trees = await this.generateTreesInPolygon(geoJsonPolygon, config); 34 await this.createRealisticTrees(trees, season, polygon); 35 36 this.updateTreeLayer(options.callSetupMap !== false); 37 38 return true; 39 } catch (error) { 40 console.error(' Error creating trees:', error); 41 return false; 42 } 43 } 44 45 46 // FIXED: Gleiche ID-Logik wie Buildings 47 removeTreesForPolygon(polygonId) { 48 this.treeData = this.treeData.filter(treeFeature => { 49 const treeId = treeFeature.properties?.treeId || ''; 50 const isFromThisPolygon = treeId.includes(`tree_${polygonId}_`); 51 return !isFromThisPolygon; // â Gleiche Logik wie Buildings 52 }); 53 } 54 55 polygonToGeoJSON(polygon) { 56 try { 57 let coordinates = polygon.coordinates; 58 59 if (typeof coordinates === 'string') { 60 coordinates = JSON.parse(coordinates); 61 } 62 63 if (typeof coordinates[0][0] === 'number') { 64 coordinates = [coordinates]; 65 } 66 67 // NEW: Vollständige Properties für Building-Features 68 return { 69 type: 'Feature', 70 geometry: { 71 type: 'Polygon', 72 coordinates: coordinates 73 }, 74 properties: { 75 // Original Properties (wie vorher) 76 polygonId: polygon.id, 77 id: polygon.id, // Additionally as 'id' 78 79 // NEW: All popup properties hinzufügen 80 name: polygon.name || `Gebäude ${polygon.id}`, 81 height: parseFloat(polygon.height) || 3, // Als Zahl, mit Fallback 82 size: parseFloat(polygon.size) || this.calculatePolygonSize(polygon), 83 color: polygon.color || '#8B4513', 84 type: polygon.type || 'buildings', 85 86 // NEW: Zusätzliche nützliche Properties 87 created: polygon.created, 88 width: polygon.width, // Falls rechteckiges Polygon 89 length: polygon.length // Falls rechteckiges Polygon 90 } 91 }; 92 } catch (error) { 93 console.error(' Error in polygon conversion:', error); 94 return null; 95 } 96 } 97 98 generateTreesInPolygon(geoJsonPolygon, config) { 99 try { 100 const area = turf.area(geoJsonPolygon); 101 const numTrees = Math.max(2, Math.floor(area / config.treeDensity)); 102 const bbox = turf.bbox(geoJsonPolygon); 103 const candidatePoints = turf.randomPoint(numTrees * 2, { bbox: bbox }); 104 105 const treesInside = candidatePoints.features.filter(point => 106 turf.booleanPointInPolygon(point, geoJsonPolygon) 107 ).slice(0, numTrees); 108 109 const filteredTrees = this.filterByDistance(treesInside, config.minDistance); 110 111 // EXAKT WIE BUILDINGS: ID-Extraktion 112 const rawPolygonId = geoJsonPolygon.properties.polygonId || 113 geoJsonPolygon.properties.id; 114 115 // EXAKT WIE BUILDINGS: Original-Polygon für ECHTE ID finden 116 const originalPolygon = projectData?.variations?.[variation]?.polygons?.find(p => 117 p.id === rawPolygonId || p.id == rawPolygonId 118 ); 119 120 // ECHTE DB-ID verwenden (falls Original-Polygon gefunden) 121 const polygonId = originalPolygon ? originalPolygon.id : rawPolygonId; 122 123 return filteredTrees.map((tree, index) => { 124 const treeId = `tree_${polygonId}_${index}`; 125 126 return { 127 ...tree, 128 properties: { 129 treeId: treeId, 130 type: config.treeTypes[index % config.treeTypes.length], 131 height: this.getRandomTreeHeight(), 132 radius: this.getRandomTreeRadius(), 133 polygonId: polygonId, // Echte DB-ID
134 polygonHeight: config.polygonHeight || 0 135 } 136 }; 137 }); 138 139 } catch (error) { 140 console.error(' Error in tree generation:', error); 141 return []; 142 } 143 } 144 145 filterByDistance(trees, minDistanceMeters) { 146 const filtered = []; 147 const minDistanceKm = minDistanceMeters / 1000; 148 149 trees.forEach(tree => { 150 const tooClose = filtered.some(existing => 151 turf.distance(tree, existing, { units: 'kilometers' }) < minDistanceKm 152 ); 153 154 if (!tooClose) { 155 filtered.push(tree); 156 } 157 }); 158 159 return filtered; 160 } 161 162 async createRealisticTrees(trees, season, polygon) { 163 const colors = this.getSeasonColors(season); 164 165 // SEQUENTIELL mit Pausen â verhindert Main Thread Blocking 166 // Batches von 3 Baeumen, dann Main Thread atmen lassen 167 const batchSize = 5; 168 for (let i = 0; i < trees.length; i += batchSize) { 169 const batch = trees.slice(i, i + batchSize); 170 // Innerhalb des Batches: sequentiell statt Promise.all 171 for (let j = 0; j < batch.length; j++) { 172 await this.generateSingleTree(batch[j], colors, polygon); 173 } 174 175 // Kurze Pause zwischen Batches â Main Thread frei geben 176 if (i + batchSize < trees.length) { 177 await new Promise(resolve => setTimeout(resolve, 20)); 178 } 179 } 180 } 181 182 async generateSingleTree(tree, colors, polygon) { 183 try { 184 const coords = tree.geometry.coordinates; 185 const treeProps = tree.properties; 186 187 // EXAKT WIE BUILDINGS: Original-Polygon finden für echte DB-ID 188 const originalPolygon = projectData?.variations?.[variation]?.polygons?.find(p => 189 p.id === polygon.id || p.id == polygon.id 190 ); 191 192 // Echte DB-ID verwenden 193 const realPolygonId = originalPolygon ? originalPolygon.id : polygon.id; 194 195 // Polygon-Daten (ECHTE ID verwenden) 196 const polygonData = { 197 polygonId: realPolygonId, // ECHTE DB-ID! 198 name: (originalPolygon || polygon).name || 'Wald', 199 size: parseFloat((originalPolygon || polygon).size) || 0.1, 200 polygonColor: (originalPolygon || polygon).color || '#228B22', 201 type: (originalPolygon || polygon).type === 'obstacle' ? 'trees' : (originalPolygon || polygon).type || 'trees' 202 }; 203 204 // Rest der Tree-Generierung bleibt gleich... 205 let treeHeight = treeProps.height || 5; 206 const polygonHeight = Math.max(0.5, treeProps.polygonHeight || parseFloat((originalPolygon || polygon).height) || 0.5); 207 208 if (polygonHeight >= 0.5) { 209 const heightFactor = 0.7 + Math.random() * 0.5; 210 treeHeight = Math.max(0.5, polygonHeight * heightFactor + Math.random() * 2); 211 } 212 213 const trunkHeight = treeHeight * 0.3; 214 const trunkRadius = 0.2 + Math.random() * 0.3; 215 216 // TRUNK mit echter DB-ID 217 const trunk = turf.circle(coords, trunkRadius, { 218 steps: 8, 219 units: 'meters', 220 properties: { 221 polygonId: polygonData.polygonId, // ECHTE DB-ID 222 name: polygonData.name, 223 height: trunkHeight, 224 size: polygonData.size, 225 color: '#8B4513', 226 type: polygonData.type, 227 base_height: 0, 228 treeId: treeProps.treeId, 229 part: 'trunk' 230 } 231 }); 232 233 this.treeData.push(trunk); 234 235 const crownHeight = treeHeight - trunkHeight; 236 const numLayers = Math.max(8, Math.floor(treeHeight * 1.5)); 237 const maxRadius = Math.max(1, treeHeight * 0.3); 238 239 // Crown mit echter DB-ID 240 await this.generateTreeCrown(coords, numLayers, trunkHeight, crownHeight, maxRadius, colors, treeProps, polygonData); 241 242 } catch (error) { 243 console.error(' Error in tree generation:', error); 244 } 245 } 246 247// ============================================================================ 248// ERWEITERTE generateTreeCrown - FEATURE-HÃHE + POLYGON-DATEN 249// ============================================================================ 250 251 async generateTreeCrown(center, numLayers, baseHeight, crownHeight, maxRadius, colors, treeProps, polygonData) { 252 try { 253 const layerHeight = crownHeight / numLayers; 254 255 for (let i = 0; i <= numLayers; i++) { 256 const currentHeight = baseHeight + (i * layerHeight); 257 const progress = i / numLayers; 258 259 let radius; 260 if (progress <= 0.6) { 261 radius = maxRadius * (progress / 0.6); 262 } else { 263 radius = maxRadius * (1 - (progress - 0.6) / 0.4); 264 } 265 266 radius = Math.max(0.2, radius + (Math.random() - 0.5) * 0.5); 267 268 if (radius > 0.2) { 269 const crownColor = this.getColorBetween(colors.light, colors.dark, progress); 270 const crownColorStr = `rgb(${crownColor[0]}, ${crownColor[1]}, ${crownColor[2]})`; 271 const crownLayer = this.createOrganicCrownLayer(center, radius, treeProps.treeId); 272 273 if (crownLayer) { 274 crownLayer.properties = { 275 // POPUP-PROPERTIES: 276 polygonId: polygonData.polygonId, 277 name: polygonData.name, // â VOM POLYGON (gleich für alle Features) 278 height: currentHeight + layerHeight, // â FEATURE-SPEZIFISCH (Crown-Layer-Höhe) 279 size: polygonData.size, // â VOM POLYGON (gleich für alle Features) 280 color: polygonData.polygonColor || crownColorStr, // â FEATURE-SPEZIFISCH (Crown-Farbe) 281 type: polygonData.type, 282 283 // Rendering-Properties 284 base_height: currentHeight, 285 286 // Tree-spezifische Properties 287 treeId: treeProps.treeId, 288 part: 'crown', 289 layer: i 290 }; 291 292 this.treeData.push(crownLayer); 293 } 294 } 295 } 296 } catch (error) { 297 console.error(' Error in crown generation:', error); 298 } 299 } 300 301 302 createOrganicCrownLayer(center, radius, treeId) { 303 try { 304 // OPTIMIERT: Weniger Kreise für bessere Performance 305 const numCircles = Math.max(2, Math.floor(radius * 2)); 306 const circles = []; 307 308 for (let j = 0; j < numCircles; j++) { 309 const angle = (j / numCircles) * 2 * Math.PI; 310 const distance = radius * 0.4 * Math.random(); 311 const circleCenter = [
312 center[0] + Math.cos(angle) * distance / 111320, 313 center[1] + Math.sin(angle) * distance / 111320 314 ]; 315 316 const circleRadius = radius * (0.3 + Math.random() * 0.4); 317 const circle = turf.circle(circleCenter, circleRadius, { 318 steps: 8, // OPTIMIERT: Weniger Steps 319 units: 'meters' 320 }); 321 circles.push(circle); 322 } 323 324 const mainCircle = turf.circle(center, radius * 0.7, { 325 steps: 12, 326 units: 'meters' 327 }); 328 circles.push(mainCircle); 329 330 const collection = turf.featureCollection(circles); 331 const dissolved = turf.dissolve(collection); 332 333 return dissolved.features.length > 0 ? dissolved.features[0] : mainCircle; 334 335 } catch (error) { 336 return turf.circle(center, radius, { steps: 8, units: 'meters' }); 337 } 338 } 339 340 async updateTreeLayer(callSetupMap = true) { 341 // Alle Trees loeschen und neu erstellen 342 this.treeData = []; 343 344 // Warte auf Style bevor Layer erstellt werden 345 if (this.map && !this.map.isStyleLoaded()) { 346 await new Promise((resolve) => { 347 this.map.on('style.load', resolve); 348 // Safety timeout nach 10s 349 setTimeout(resolve, 10000); 350 }); 351 } 352 353 // Alle Tree-Polygone finden â mit Guard fuer polygonManager 354 const allPolygons = projectData?.variations?.[variation]?.polygons || []; 355 356 var pm = (typeof polygonManager !== 'undefined' && polygonManager) 357 ? polygonManager 358 : (window.polygonManager || null); 359 360 const treePolygons = allPolygons.filter(polygon => { 361 if (pm && typeof pm.isTreePolygon === 'function') { 362 return pm.isTreePolygon(polygon); 363 } 364 // Fallback: eigene Detection 365 if (polygon.type === 'obstacle' || polygon.type === 'trees') return true; 366 if (!polygon.name) return false; 367 var name = polygon.name.toLowerCase(); 368 return ['wald', 'baum', 'tree', 'forest', 'gehoelz', 'hecke'].some(function(kw) { return name.indexOf(kw) !== -1; }); 369 }); 370 371 // Trees fuer alle Tree-Polygone erstellen 372 for (const polygon of treePolygons) { 373 const config = { 374 treeDensity: this.getTreeDensityForPolygon(polygon), 375 minDistance: Math.max(3, parseFloat(polygon.height) * 0.4), 376 treeTypes: ['oak', 'pine', 'birch'], 377 polygonHeight: parseFloat(polygon.height) || 0, 378 randomSeed: polygon.id 379 }; 380 381 const trees = this.generateTreesInPolygon(this.polygonToGeoJSON(polygon), config); 382 await this.createRealisticTrees(trees, 'summer', polygon); 383 } 384 385 // Layer aktualisieren mit Retry 386 const collection = turf.featureCollection(this.treeData); 387 388 var maxRetries = 3; 389 for (var attempt = 1; attempt <= maxRetries; attempt++) { 390 try { 391 const treesLayer = layers.find(l => l.id === 'trees-3d'); 392 if (treesLayer) { 393 treesLayer.features = collection; 394 } else { 395 layers.push({ 396 id: 'trees-3d', 397 type: '3d-tree', 398 fillId: 'trees-3d-layer', 399 features: collection 400 }); 401 } 402 403 if (callSetupMap) { 404 setupMap(0, 'trees-3d'); 405 } 406 407 // Erfolgreich â Schleife verlassen 408 break; 409 410 } catch (error) { 411 console.warn('[Trees] Layer update error attempt ' + attempt + '/' + maxRetries + ': ' + error.message); 412 if (attempt < maxRetries) { 413 await new Promise(resolve => setTimeout(resolve, 500)); 414 } else { 415 console.error('[Trees] Layer update after ' + maxRetries + ' attempts failed:', error); 416 } 417 } 418 } 419 } 420 421 async rebuildAllTrees(callSetupMap = true) { 422 423 // Alle Trees löschen 424 this.treeData = []; 425 426 // Alle Tree-Polygone finden 427 const allPolygons = projectData?.variations?.[variation]?.polygons || []; 428 const treePolygons = allPolygons.filter(polygon => 429 this.isTreePolygon(polygon) // â Eigene Methode verwenden 430 ); 431 432 // Trees für alle Tree-Polygone erstellen 433 for (const polygon of treePolygons) { 434 const config = { 435 treeDensity: this.getTreeDensityForPolygon(polygon), 436 minDistance: Math.max(3, parseFloat(polygon.height) * 0.4), 437 treeTypes: ['oak', 'pine', 'birch'], 438 polygonHeight: parseFloat(polygon.height) || 0, 439 randomSeed: polygon.id 440 }; 441 442 const geoJsonPolygon = this.polygonToGeoJSON(polygon); 443 if (geoJsonPolygon) { 444 const trees = this.generateTreesInPolygon(geoJsonPolygon, config); 445 await this.createRealisticTrees(trees, 'summer', polygon); 446 } 447 } 448 449 // Layer aktualisieren 450 this.updateTreeLayer(callSetupMap); 451 } 452 453 // NEW: getTreeDensityForPolygon ins TreeGenerator 454 getTreeDensityForPolygon(polygon) { 455 const area = parseFloat(polygon.size) * 10000; // ha zu m² 456 const height = parseFloat(polygon.height) || 0; 457 458 // Baumdichte basierend auf Höhe anpassen 459 let baseDensity = 40; 460 if (height > 15) baseDensity = 50; 461 else if (height > 8) baseDensity = 35; 462 else baseDensity = 30; 463 464 // Flächenbasierte Anpassung 465 if (area < 1000) return baseDensity * 0.8; 466 if (area < 5000) return baseDensity; 467 return baseDensity * 1.2; 468 } 469 470 clearTrees() { 471 this.treeData = []; 472 if (this.map.getSource('trees-3d')) { 473 this.map.getSource('trees-3d').setData(turf.featureCollection([])); 474 } 475 } 476 477 reloadTreeLayer() { 478 try { 479 if (this.map.getLayer('trees-3d-layer')) { 480 this.map.removeLayer('trees-3d-layer'); 481 } 482 if (this.map.getSource('trees-3d')) { 483 this.map.removeSource('trees-3d'); 484 } 485 486 this.updateTreeLayer(true); 487 } catch (error) { 488 console.error(' Error in layer reload:', error); 489 } 490 } 491 492 getSeasonColors(season) { 493 const seasonColors = { 494 summer: { light: [116, 196, 118], dark: [0, 68, 27] }, 495 autumn: { light: [254, 224, 139], dark: [215, 48, 39] },
496 winter: { light: [255, 255, 255], dark: [0, 68, 27] }, 497 spring: { light: [144, 238, 144], dark: [34, 139, 34] } 498 }; 499 return seasonColors[season] || seasonColors.summer; 500 } 501 502 getColorBetween(color1, color2, weight) { 503 const w1 = weight; 504 const w2 = 1 - w1; 505 return [ 506 Math.round(color1[0] * w1 + color2[0] * w2), 507 Math.round(color1[1] * w1 + color2[1] * w2), 508 Math.round(color1[2] * w1 + color2[2] * w2) 509 ]; 510 } 511 512 getRandomTreeHeight() { 513 return 6 + Math.random() * 12; 514 } 515 516 getRandomTreeRadius() { 517 return 2 + Math.random() * 3; 518 } 519} 520 521// ============================================================================ 522// VEREINFACHTE GLOBAL FUNCTIONS 523// ============================================================================ 524 525let treeGenerator = null; 526 527async function initializeTreeGenerator(mapInstance) { 528 treeGenerator = new MapboxTreeGenerator(mapInstance); 529 530 // Warte auf Style-Load bevor Baeume geladen werden 531 if (mapInstance && !mapInstance.isStyleLoaded()) { 532 await new Promise(function(resolve) { 533 mapInstance.on('style.load', resolve); 534 setTimeout(resolve, 10000); 535 }); 536 } 537
538 // Bestehende Baeume werden vom PolygonManager geladen (sequentiell mit Pausen). 539 // NICHT hier laden â sonst Double-Loading und Main Thread Freeze. 540 // loadExistingTreePolygons(); // Deaktiviert: PolygonManager uebernimmt 541} 542 543async function updateAllTrees() { 544 if (treeGenerator) { 545 await treeGenerator.updateTreeLayer(false); 546 setupMap(0, 'trees-3d'); 547 } 548} 549 550async function loadExistingTreePolygons() { 551 try { 552 if (typeof projectData === 'undefined' || typeof variation === 'undefined') { 553 console.warn('[Trees] projectData or variation not available â skipping batch load'); 554 return; 555 } 556 557 // Warte bis treeGenerator bereit ist (max 10s) 558 var maxWait = 20; 559 var waited = 0; 560 while (waited < maxWait && !treeGenerator) { 561 await new Promise(resolve => setTimeout(resolve, 500)); 562 waited++; 563 } 564 565 if (!treeGenerator) { 566 console.error('[Trees] treeGenerator not available after 10s â skipping'); 567 return; 568 } 569 570 // Warte auf map style loaded 571 if (treeGenerator.map && !treeGenerator.map.isStyleLoaded()) { 572 await new Promise(function(resolve) { 573 treeGenerator.map.on('style.load', resolve); 574 setTimeout(resolve, 10000); 575 }); 576 } 577 578 const allPolygons = projectData?.variations?.[variation]?.polygons || []; 579 580 // polygonManager mit Fallback 581 var pm = (typeof polygonManager !== 'undefined' && polygonManager) 582 ? polygonManager 583 : (window.polygonManager || null); 584 585 const treePolygons = allPolygons.filter(function(polygon) { 586 if (pm && typeof pm.isTreePolygon === 'function') { 587 return pm.isTreePolygon(polygon); 588 } 589 // Fallback-Detection ohne polygonManager 590 if (polygon.type === 'obstacle' || polygon.type === 'trees') return true; 591 if (!polygon.name) return false; 592 var name = polygon.name.toLowerCase(); 593 return ['wald', 'baum', 'tree', 'forest', 'gehoelz', 'hecke'].some(function(kw) { return name.indexOf(kw) !== -1; }); 594 }); 595 596 if (treePolygons.length > 0) { 597 for (var i = 0; i < treePolygons.length; i++) { 598 var polygon = treePolygons[i]; 599 try { 600 await createTreesInPolygonSilent(polygon, 'summer', { 601 polygonHeight: parseFloat(polygon.height) || 0.5, 602 randomSeed: polygon.id 603 }); 604 } catch (err) { 605 console.warn('[Trees] Error with polygon ' + polygon.id + ', skipping: ' + err.message); 606 } 607 await new Promise(resolve => setTimeout(resolve, 200)); 608 } 609 610 // setupMap nur EINMAL am Ende â mit Retry 611 var setupRetries = 3; 612 for (var r = 1; r <= setupRetries; r++) { 613 try { 614 setupMap(0, 'trees-3d'); 615 break; 616 } catch (err) { 617 console.warn('[Trees] setupMap error attempt ' + r + '/' + setupRetries + ': ' + err.message); 618 if (r < setupRetries) { 619 await new Promise(resolve => setTimeout(resolve, 500)); 620 } 621 } 622 } 623 } 624 625 } catch (error) { 626 console.error('[Trees] Error loading existing tree polygons:', error); 627 } 628} 629 630// New silent version for batch loading 631async function createTreesInPolygonSilent(polygon, season = 'summer', options = {}) { 632 if (!treeGenerator) { 633 console.error(' Tree generator not initialized!'); 634 return false; 635 } 636 637 const polygonHeight = Math.max(0.5, parseFloat(polygon.height) || 0.5); 638 const extendedOptions = { 639 polygonHeight: polygonHeight, 640 randomSeed: polygon.id, 641 callSetupMap: false, // No setupMap bei Batch-Loading 642 ...options 643 }; 644 645 return await treeGenerator.createTreesInPolygon(polygon, season, extendedOptions); 646} 647 648 649async function createTreesInPolygon(polygon, season = 'summer', options = {}) { 650 if (!treeGenerator) { 651 console.error(' Tree generator not initialized!'); 652 return false;
653 } 654 655 const polygonHeight = Math.max(0.5, parseFloat(polygon.height) || 0.5); 656 657 // FIXED: Konsistente Options mit randomSeed 658 const extendedOptions = { 659 polygonHeight: polygonHeight, 660 randomSeed: polygon.id, // WICHTIG: Immer die Polygon-ID als Seed verwenden 661 ...options 662 }; 663 664 return await treeGenerator.createTreesInPolygon(polygon, season, extendedOptions); 665} 666 667// VEREINFACHT: Nur noch diese Funktionen 668function clearAllTrees() { 669 if (treeGenerator) { 670 treeGenerator.clearTrees(); 671 } 672} 673 674function reloadTreeLayer() { 675 if (treeGenerator) { 676 treeGenerator.reloadTreeLayer(); 677 } 678} 679 680// NEW: Nur Bäume für ein bestimmtes Polygon löschen 681function clearTreesForPolygon(polygonId) { 682 if (treeGenerator) { 683 treeGenerator.removeTreesForPolygon(polygonId); 684 685 // NUR Layer-Update 686 const collection = turf.featureCollection(treeGenerator.treeData); 687 const treesLayer = layers.find(l => l.id === 'trees-3d'); 688 if (treesLayer) { 689 treesLayer.features = collection; 690 } 691 setupMap(0, 'trees-3d'); 692 } 693}
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.