1/* 2 Developed by Nick Bowman ([email protected]) 3 Copyright 2015 Hodes 4 */ 5var CWS = CWS || {}; 6 7var previous_map = previous_map || null; 8var previous_router = previous_router|| null; 9var previous_map_router = previous_map_router || true; 10 11CWS.map = (function (w, $) { 12 var options = { 13 high_volume: false, 14 marker_clustering: true, 15 display_loading_bar: true, //CWS-4574/CWS-4576 added for showing and hiding loader 16 cluster_radius: 40, 17 custom_marker: false, 18 starting_coords: [39.2037532, -96.6201883], 19 starting_zoom: 3, 20 max_zoom: 13, 21 fit_bounds: true, 22 tilemap: 'http://{s}.tile.osm.org/{z}/{x}/{y}.png', 23 api_url: 'http://jobsapi-internal.cert.m-cloud.io/api/map', 24 jobdetail_path: '/job', 25 org_id: 1927, 26 wait_for_searchform: false, 27 include_geolocation: true, 28 include_directions: true, 29 custom_popup: false 30 }, 31 filters = {}, 32 map, 33 markers, 34 geojson, 35 router, 36 router_on_map = false, 37 jobs = {}, 38 stores = {}, 39 stores_exist = false, 40 arrow_icon = '<span class="job-arrow avia-font-entypo-fontello"></span>', 41 popup_loader, 42 current_criteria = {}; 43 44 45 var search_jobs = function (criteria, loader) { 46 // Criteria should always be set, but just in case... 47 criteria = criteria || {}; 48 criteria.Organization = options.org_id; 49 50 if (filters.length > 0) { 51 if (criteria['facet']) { 52 // Underscore's union() removes duplicate keys... probably not necessary but it's cleaner 53 criteria['facet'] = _.union(criteria['facet'], filters); 54 } 55 else { 56 // No existing filters from the search form exist 57 criteria['facet'] = filters; 58 } 59 } 60 61 if(CWS.jobs && CWS.jobs.get_option('filter_od')){ 62 criteria.openeddate = CWS.jobs.get_option('filter_od'); 63 } 64 65 // Maxing out at 500, enable high_volume setting if 66 criteria.Limit = 500; 67 delete criteria.offset; 68 if(criteria.hasOwnProperty('facetlist')){ 69 delete criteria.facetlist; 70 } 71 72 if (options.high_volume) { 73 criteria.Limit = 0; 74 if(!criteria.hasOwnProperty('facetlist')){ 75 criteria['facetlist'] = []; 76 } 77 criteria['facetlist'].push('coordinates'); 78 } 79 80 if(CWS.jobs && CWS.jobs.get_option('use_boolean_search')){ 81 criteria.useBooleanKeywordSearch = 'true'; 82 } 83 84 current_criteria = criteria; 85 86 $.ajax({ 87 url: CWS.apply_filter('map_api_url', options.api_url), 88 data: CWS.apply_filter('map_criteria', criteria), 89 dataType: 'jsonp', 90 crossDomain: true, 91 cache: true, 92 jsonpCallback: 'CWS.map.mapCallback', 93 //success: display_jobs, 94 complete: function () { 95 if (loader) { 96 loader.end(); 97 $('.widget-jobsearch-results').find('.search-results-table').attr("aria-busy","false"); 98 } 99 } 100 }); 101 }; 102 103 var display_jobs = function (data) { 104 data = CWS.apply_filter('job_map_search_response', data); 105 if (markers) { 106 map.removeLayer(markers); 107 jobs = {}; 108 109 if (router_on_map) { 110 map.removeControl(router); 111 } 112 } 113 if (options.marker_clustering) { 114 var cluster_options = {maxClusterRadius: options.cluster_radius}; 115 cluster_options = CWS.apply_filter('marker_cluster_options', cluster_options); 116 markers = new Leaflet.MarkerClusterGroup(cluster_options); 117 } 118 119 stores_exist = false; 120 121 var dataset = options.high_volume ? ('markers' in data ? data.markers : data.aggregations.coordinates.buckets) : data.queryResult; 122 123 for (var i = 0, len = dataset.length; i < len; i++) { 124 var marker_opts = {}; 125 if (options.custom_marker !== false) { 126 // I have it set so that the icon options format comes from the widget settings correctly 127 marker_opts.icon = new Leaflet.icon(CWS.apply_filter('marker_icon_options', options.custom_marker, dataset[i])); 128 } 129 130 // For now, only some clients can use the new coordinates-by-aggregation, so there's going to be some duplicating code 131 if (options.high_volume) { 132 CWS.apply_filter('map_marker_data', dataset[i]); 133 var job = dataset[i].key; 134 var coords = job.split(',');
135 var marker = new Leaflet.Marker([coords[1].trim(), coords[0]], CWS.apply_filter('marker_options', marker_opts, dataset[i])) 136 .bindPopup('') 137 .on('popupopen', get_jobs_at_coords); 138 139 marker.orig_coords = job; 140 141 marker = CWS.apply_filter('marker_init', marker, dataset[i]); 142 143 markers.addLayer(marker); 144 } 145 146 // Original endpoint 147 else { 148 var job = dataset[i]; 149 if (job) { 150 var coord_text = job.primary_location[1] + ',' + job.primary_location[0]; 151 // We're going to group jobs into an array if they have the same coordinates 152 if (typeof jobs[coord_text] === 'undefined') { 153 var marker = new Leaflet.Marker([job.primary_location[1], job.primary_location[0]], CWS.apply_filter('marker_options', marker_opts, dataset[i])) 154 // We'll be setting popup content on-click 155 .bindPopup('') 156 .on('popupopen', popup_open); 157 158 markers.addLayer(marker); 159 160 jobs[coord_text] = []; 161 jobs[coord_text].push(job); 162 163 if (job.store_id) { 164 stores_exist = true; 165 } 166 } 167 else { 168 jobs[coord_text].push(job); 169 } 170 } 171 } 172 } 173 174 geojson = CWS.apply_filter('map_geojson', geojson, map, data); 175 176 // Unfortunately another loop is needed for stores 177 // But at least we check if there are any store ids in the first loop, so this'll be ignored for most clients 178 if (stores_exist) { 179 for (var coords in jobs) { 180 // Nifty underscore function 181 stores[coords] = _.groupBy(jobs[coords], function (jb) {
182 return jb.store_id; 183 }); 184 } 185 } 186 187 map.addLayer(markers); 188 if (markers.getLayers().length > 0 && options.fit_bounds) { 189 map.fitBounds(markers.getBounds(), {maxZoom: 12}); 190 } 191 if (dataset.length == 0) { 192 map.setView(options.starting_coords, options.starting_zoom, {animate: true}); 193 } 194 195 map = CWS.apply_filter('map_layers_complete', map, markers, geojson); 196 /*if (!initial==) { 197 198 initial.center = map.getCenter(); 199 initial.zoom = map.getZoom(); 200 map.setView(initial.center, initial.zoom) 201 }*/ 202 203 204 }; 205 206 var popup_open = function (popup, data) { 207 // to clear previous layer 208 if (previous_router != null && previous_map_router) { 209 previour_map.removeControl(previous_router); 210 } else { 211 previous_map_router = true; 212 } 213 214 if(CWS.buttons.theme() === 'zephyr'){ 215 arrow_icon = '<i class="mdfi_navigation_chevron_right job-arrow"></i>'; 216 } 217 218 // Well this is kinda stupid on Leaflet's part... 219 popup = popup.popup; 220 var html = ''; 221 var count = 0; 222 223 if(options.custom_popup){ 224 html = CWS.apply_filter('map_custom_popup', html, data); 225 popup.setContent(html); 226 popup.update(); 227 } 228 else { 229 var coords = popup.getLatLng().lat + ',' + popup.getLatLng().lng; 230 231 var dataset = options.high_volume ? data.queryResult : jobs[coords]; 232 233 var len = dataset.length; 234 var totalJobs = data.totalHits; 235 var $popup = $('<div/>').html(popup.getContent()).contents(); 236 237 $popup.find('.location-jobcount').html(' - ' + totalJobs + ' opening' + (totalJobs > 1 ? 's' : '')); 238 239 // Popup pin variable search starts 240 241 var pinned_city = ''; 242 var pinned_primary_city = ''; 243 var pinned_state = ''; 244 var pinned_primary_state = ''; 245 var pin_longitude = popup.getLatLng().lng; 246 var pin_latitude = popup.getLatLng().lat; 247 248 if(typeof pin_longitude !== 'undefined' && typeof pin_latitude !== 'undefined' ){ 249 250 var is_city_found = 0; 251 252 function find_jobs_with_add_locations(item, index){ 253 254 // It will check against only first item, if it does not have 'addtnl_locations' property defined (specifically for ES jobs API). Note: Google jobs API dataset object item pass blank string [] in case if it does not have any 'addtnl_locations' value that would let him to go into 'if' block. 255 256 if (typeof item.addtnl_locations !== 'undefined'){ 257 258 if(item.addtnl_locations.length > 0){ 259 260 function location_obj(item2, index2){ 261 262 if (typeof item2.addtnl_location !== 'undefined'){ 263 264 if( item2.addtnl_location.length == 2 ){ 265 266 if( item2.addtnl_location[0] == pin_longitude && item2.addtnl_location[1] == pin_latitude ){ 267 268 if( typeof item2.addtnl_city !== 'undefined' && typeof item2.addtnl_state !== 'undefined' && typeof item2.addtnl_country !== 'undefined' ){ 269 270 // if respective city/state is found, it will pass control out of location_obj iteration. 271 272 pinned_city = item2.addtnl_city; 273 pinned_state = item2.addtnl_country === 'US' ? item2.addtnl_state : item2.addtnl_country; 274 is_city_found = 1; 275 return true; 276 277 } 278 279 } 280 281 } 282 283 } 284 285 } 286 287 item.addtnl_locations.find(location_obj); 288 289 if(is_city_found == 1){ 290 return true; 291 } 292 293 } 294 295 }else{ 296 // In case of ES API - There is no 'addtnl_locations' object property 297 return true; 298 } 299 300 } 301
302 dataset.find(find_jobs_with_add_locations); 303 304 if ( pinned_city !== '' && pinned_state !== '' ) { 305 pinned_primary_city = pinned_city; 306 pinned_primary_state = pinned_state; 307 }else{ 308 pinned_primary_city = dataset[0].primary_city; 309 pinned_primary_state = dataset[0].primary_country === 'US' ? dataset[0].primary_state : dataset[0].primary_country; 310 } 311 312 }else{ 313 // in case we don't get coordinates longitude and latitude values 314 pinned_primary_city = dataset[0].primary_city; 315 pinned_primary_state = dataset[0].primary_country === 'US' ? dataset[0].primary_state : dataset[0].primary_country; 316 } 317 318 // Popup pin variable search Ends 319 320 html += '<div class="location-header">' + pinned_primary_city + ', ' + pinned_primary_state + '<span class="location-jobcount"> - ' + totalJobs + ' ' + (totalJobs > 1 ? CWS._('openings') : CWS._('opening')) + '</span></div>' 321 + '<div id="map-jobs-container" class="map-jobs">'; 322 323 if (stores_exist === false || (data && !data[0].store_id)) { 324 325 let result = processJobs(dataset, html, count); 326 html = result.html; 327 count = result.count; 328 329 } 330 // If we have store ids, they're grouped a bit differently 331 // TODO: repetitive code needs to be cleaned up 332 else { 333 var count = 0; 334 for (var store in stores[coords]) { 335 // Jobs with missing store id are just empty strings, we want to do those last 336 // We want store names bolded before its list of jobs, then go back and list ones without store names 337 if (store != '') { 338 for (var i = 0, len = stores[coords][store].length; i < len; i++) { 339 var job = stores[coords][store][i]; 340 341 if (i === 0) { 342 html += '<div class="store"><div class="store-name">' + store + '</div>'; 343 } 344 345 var joburl = options.jobdetail_path + '/' + job.id + '/' + CWS.seo_url(job); 346 html += '<a href="' + joburl + '" class="map-job ' + (i % 2 == 0 ? '' : 'alt') + '">' 347 + arrow_icon 348 + '<span class="title">' + job.title + '</span>' 349 + '</a>'; 350 351 count++; 352 } 353 354 html += '</div>'; // .store 355 } 356 } 357 358 // Ugh more repetition 359 // This ensures jobs without store names are last 360 if (stores[coords]['']) { 361 if (html != '') { 362 html += '<div class="no-stores"><div class="store-name">Other stores</div>'; 363 } 364 365 for (var i = 0, len = stores[coords][''].length; i < len; i++) { 366 var job = stores[coords][''][i]; 367 var joburl = options.jobdetail_path + '/' + job.id + '/' + CWS.seo_url(job); 368 html += '<a href="' + joburl + '" class="map-job ' + (i % 2 == 0 ? '' : 'alt') + '">' 369 + arrow_icon 370 + '<span class="title">' + job.title + '</span>' 371 + '</a>'; 372 373 } 374 375 html += '</div>'; // .no-stores 376 } 377 } 378 379 html += '<div id="map-job-sentinel"></div> </div>'; // .map-jobs 380 381 popup.setContent(CWS.apply_filter('map_popup_html', html)); 382 popup.update(); 383 384 var dir_content = '', dir_class = 'directions'; 385 if ($('body.l-body'.length)) { 386 dir_class = 'mdfi_maps_directions'; 387 dir_content = ''; 388 } 389 if (options.include_directions) { 390 $('.leaflet-popup').prepend('<a href="#" class="' + dir_class + '" title="Get Directions" onclick="CWS.map.directions([' + popup.getLatLng().lat + ',' + popup.getLatLng().lng + ']); return false;">' + dir_content + '</a>'); 391 } 392 393 // CFD-40 - Lazy loading done from here 394 current_criteria['offset'] = count + 1; 395 const jobsContainer = document.getElementById('map-jobs-container'); 396 const sentinel = document.getElementById('map-job-sentinel'); 397 398 const observer = new IntersectionObserver((entries) => { 399 // If sentinel is visible and not all jobs have been loaded 400 if (entries[0].isIntersecting && current_criteria['offset'] < totalJobs) { 401 load_jobs_with_offset(null, coords); 402 } 403 }, { 404 root: jobsContainer, 405 threshold: 1.0, 406 rootMargin: '0px' 407 }); 408 409 observer.observe(sentinel); 410 411 document.querySelector('.leaflet-popup-close-button')?.addEventListener('click', function(event) { 412 observer.disconnect(); 413 }); 414 415 } 416 }; 417 418 419 var get_jobs_at_coords = function (popup) { 420 421 var real_popup = popup.popup; 422 var coords = popup.target.orig_coords; 423 424 if(typeof current_criteria.facet !== 'object') { 425 current_criteria.facet = []; 426 } 427 428 var new_facets = []; 429 for(var facet in current_criteria.facet){ 430 if(current_criteria.facet[facet].indexOf('coordinates:') === -1){ 431 new_facets.push(current_criteria.facet[facet]); 432 } 433 } 434 current_criteria.facet = new_facets; 435 current_criteria.facet.push('coordinates:' + coords); 436 437 delete current_criteria['LocationRadius']; 438 delete current_criteria['latitude']; 439 delete current_criteria['longitude']; 440 delete current_criteria['Limit']; 441 delete current_criteria['offset']; 442 delete current_criteria['pageSize']; 443 444 // Failed experiment 445 real_popup.setContent('<div class="popup_loader_wrap"><div id="popup_loader" class="popup_loader"></div></div>'); 446 447 if(CWS.jobs && CWS.jobs.get_option('use_boolean_search')){ 448 current_criteria.useBooleanKeywordSearch = 'true'; 449 } 450 load_jobs_with_offset(popup, coords); 451 452 }; 453 454 function processJobs(dataset, html, count) { 455 try{ 456 if (!Array.isArray(dataset) || dataset.length === 0) { 457 console.warn('No jobs available to process.'); 458 return; 459 } 460 for (let i = 0; i < dataset.length; i++) { 461 let job = dataset[i]; 462 let joburl = options.jobdetail_path + '/' + job.id + '/' + CWS.seo_url(job); 463 464 html += `<a href="${joburl}" class="map-job ${i % 2 === 0 ? '' : 'alt'}"> 465 ${arrow_icon}
466 <span class="title">${job.title}</span> 467 </a>`; 468 count++; 469 } 470 return { count, html }; 471 }catch(e){ 472 console.error('Error processing jobs:', e) 473 } 474 475 } 476 477 let extendPopup = function (data) { 478 try { 479 const sentinel = document.getElementById('map-job-sentinel'); 480 let dataset = options.high_volume ? data.queryResult : {}; 481 let len = dataset.length; 482 let html = ''; 483 let count = 0; 484 let result = processJobs(dataset, html, count); 485 sentinel.insertAdjacentHTML('beforebegin', result.html); 486 current_criteria['offset'] += result.count; 487 } catch (e) { 488 console.error('Error extend popup:', e) 489 } 490 } 491 492 let load_jobs_with_offset = function (popup, coords ) { 493 $.ajax({ 494 url: CWS.apply_filter('map_popup_url', options.api_url), 495 data: CWS.apply_filter('map_popup_criteria', current_criteria, coords), 496 dataType: 'jsonp', 497 crossDomain: true, 498 cache: true, 499 jsonpCallback: 'mapCallback', 500 success: function (data) { 501 data = CWS.apply_filter('map_popup_response', data); 502 if(popup){ 503 popup_open(popup, data); 504 }else{ 505 extendPopup(data); 506 } 507 }, 508 complete: function () { 509 // Do Nothing 510 }, 511 error: function (error) { 512 console.log('Error loading jobs for map', error); 513 } 514 }); 515 } 516 517 var set_options = function (opts) { 518 // Underscore makes sure we don't lose our default options 519 options = _.defaults(opts, options); 520 options = CWS.apply_filter('map_script_options', options); 521 }; 522 523 // Public vars/methods 524 return { 525 init: function (opts) { 526 if (opts) { 527 set_options(opts); 528 } 529 530 arrow_icon = '<span class="job-arrow ' + CWS.layout_builder.icons.class() + '">' + CWS.layout_builder.icons.icon('arrow-right') + '</span>'; 531 532 // This is going to sound weird, but we want to move it below the "search results" header if it exists... 533 if ($('.widget-jobsearch-results,.widget-jobsearch-results-list').length > 0) { 534 var insert_before = '#search-filters'; 535 var new_search_layout = CWS && CWS.jobs && CWS.jobs.layout() === 'new'; 536 var mapPosition = CWS && CWS.jobs && CWS.jobs.mapPosition() == 'right'; 537 if($('.widget-jobsearch-results.table_tile').length > 0){ 538 if(new_search_layout) { 539 insert_before = '#widget-jobsearch-results-list'; 540 } 541 } 542 insert_before = CWS.apply_filter('insert_map_selector', insert_before); 543 $('.widget-job-map').insertBefore(insert_before); 544 545 // Might as well add the toggle button too 546 // Check to see if someone hid the map or left it to show by default 547 var icon = ''; 548 if($('body.l-body').length){ 549 icon = '<span class="mdfi_social_public"></span>'; 550 } 551 var button = '<span class="world-icon">' + icon + '</span><span class="toggle-text" aria-live="polite">' + ( $('#job-map').css('display') === 'none' ? CWS._('Show Map') : CWS._('Hide Map') ) + '</span>'; 552 if(CWS.layout_builder.theme() === 'avada'){ 553 button = '<span class="world-icon fas fontawesome-icon fa-globe-americas" aria-hidden="true"></span><span class="toggle-text" aria-live="polite">' + ( $('#job-map').css('display') === 'none' ? CWS._('Show Map') : CWS._('Hide Map') ) + '</span>'; 554 } 555 556 if(!new_search_layout) { 557 if(mapPosition) { 558 $('<a role="button" tabindex="0" href="javascript:void(0);" id="job-map-toggle" class="' + CWS.layout_builder.buttons.class('primary', 'small') + '">' 559 + button + '</a>').insertBefore('#live-results'); 560 } 561 else { 562 $('<a role="button" tabindex="0" style="float: left" href="javascript:void(0);" id="job-map-toggle" class="' + CWS.layout_builder.buttons.class('primary', 'small') + '">' 563 + button + '</a>').insertAfter('#live-results'); 564 } 565 566 } 567 options.wait_for_searchform = true; 568 } 569 570 $(document).on('ready keyup mouseover', function(){ 571 setTimeout( function(){ 572 if($('.widget-job-map').length > 0){ 573 if($('.leaflet-container').length > 0){ 574 $('.leaflet-container').find('.leaflet-tile-container > img:not([alt])').attr('alt',''); 575 $('.leaflet-container').find('.leaflet-shadow-
575pane > img:not([alt])').attr('alt',''); 576 $('.leaflet-container').find('.leaflet-marker-pane > img:not([alt])').attr('alt',''); 577 } 578 } 579 }, 2000); 580 }); 581 582 var mapId = document.getElementById('job-map-toggle'); 583 if(mapId) { 584 mapId.addEventListener("keydown", function (event) { 585 if (event.keyCode === 13 || event.keyCode === 32) { 586 event.preventDefault(); 587 mapId.click(CWS.map.toggle(this)); 588 return false; 589 } 590 }); 591 592 mapId.addEventListener("mousedown", function (event) { 593 event.preventDefault(); 594 mapId.click(CWS.map.toggle(this)); 595 return false; 596 }); 597 } 598 599 map = Leaflet.map('job-map', CWS.apply_filter('leaflet_map_options', { 600 maxZoom: options.max_zoom 601 })).setView(options.starting_coords, options.starting_zoom); 602 Leaflet.tileLayer(options.tilemap, {}).addTo(map); 603 604 // Geolocation 605 var icon = 'geolocation'; 606 if($('body.l-body').length){ 607 icon = 'mdfi_maps_my_location'; 608 } 609 if (options.include_geolocation) { 610 Leaflet.control.locate({ 611 icon: icon, 612 showPopup: false,
613 metric: false, 614 locateOptions: { 615 maxZoom: 11 616 } 617 }).addTo(map); 618 } 619 620 map = CWS.apply_filter('map_object', map); 621 622 if (!options.wait_for_searchform) { 623 search_jobs(); 624 } 625 }, 626 search: search_jobs, 627 set_options: set_options, 628 mapCallback: display_jobs, 629 set_geojson: function(json){ 630 geojson = json; 631 }, 632 set_view: function(coords, zoom){ 633 map.setView(coords, zoom); 634 }, 635 //loader: loader, // CWS-4574/CWS-4576 Job Map widget is going to use it 636 set_filters: function (opts) { 637 filters = opts; 638 }, 639 toggle: function (el) { 640 if ($('#job-map').css('display') === 'none') { 641 $(el).children('.toggle-text').html(CWS._('Hide Map')); 642 643 // Need to fix the map after it's been resized 644 $('#job-map').slideDown(function () { 645 map.invalidateSize(); 646 if (markers.getLayers().length > 0) { 647 map.fitBounds(markers.getBounds()); 648 } 649 }); 650 } 651 else { 652 $(el).children('.toggle-text').html(CWS._('Show Map')); 653 $('#job-map').slideUp(); 654 } 655 }, 656 directions: function (end, start) { 657 /*if(previous_router != null && previous_map_router){ 658 previour_map.removeControl(previous_router); 659 }else{ 660 previous_map_router = true; 661 }*/ 662 var units = 'imperial', unit_switch = $('.unit-switch'); 663 if ($('.unit-switch').length && $('.unit-switch').val() == 'km') { 664 units = 'metric'; 665 } 666 667 // Nominatim is a geocoder with no rate limiting per day, we'll go with that. 668 var opts = { 669 lineOptions: {addWaypoints: false}, 670 fitSelectedRoutes: true, 671 formatter: new Leaflet.Routing.Formatter({units: units}) 672 }; 673 674 // Use google if we've provided an api key 675 if (cws_opts && cws_opts.google_type && cws_opts.google_key) { 676 opts.geocoder = Leaflet.Control.Geocoder.google(); 677 } 678 // Otherwise use a free service 679 else { 680 opts.geocoder = Leaflet.Control.Geocoder.nominatim(); 681 } 682 683 // We want to make it so that this function can be called by anything, so start/end waypoints should be optional 684 opts.waypoints = []; 685 if (start) { 686 opts.waypoints.push(Leaflet.latLng(start[0], start[1])); 687 } 688 else opts.waypoints.push(null); 689 if (end) { 690 opts.waypoints.push(Leaflet.latLng(end[0], end[1])); 691 } 692 693 router = Leaflet.Routing.control(opts).addTo(map); 694 previour_map = map; 695 previous_router = router; 696 router_on_map = true; 697 698 $('.leaflet-routing-remove-waypoint').click(function () { 699 router_on_map = false; 700 previous_map_router = false; 701 map.removeControl(router); 702 }); 703 } 704 }; 705 706})(window, jQuery);
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.