PageSourceSearch

https://acme.com/GeoRSS/GeoRSS.js

js acme.com collected 2026-09-24 08:42:43 UTC 40,989 bytes, 1,501 lines download raw bytes

1// Copyright © 2005,2006,2009,2018,2026 by Jef Poskanzer <[email protected]>.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions
6// are met:
7// 1. Redistributions of source code must retain the above copyright
8//    notice, this list of conditions and the following disclaimer.
9// 2. Redistributions in binary form must reproduce the above copyright
10//    notice, this list of conditions and the following disclaimer in the
11//    documentation and/or other materials provided with the distribution.
12//
13// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23// SUCH DAMAGE.
24//
25// For commentary on this license please see http://acme.com/license.html
26
27'use strict';
28
29
30// Globals.
31
32var xmlsrcs = null;
33var xi;
34
35var headerElement = document.getElementById( 'header' );
36var imageElement = document.getElementById( 'image' );
37var titleElement = document.getElementById( 'title' );
38var descriptionElement = document.getElementById( 'description' );
39var groupsElement = document.getElementById( 'groups' );
40
41var footerElement = document.getElementById( 'footer' );
42var plainHtmlElement = document.getElementById( 'plainHtml' );
43
44var listElement = document.getElementById( 'list' );
45
46var mapElement = document.getElementById( 'map' );
47
48var searchElement = document.getElementById( 'search' );
49var searchFieldsElement = document.getElementById( 'searchFields' );
50var searchPatternElement = document.getElementById( 'searchPattern' );
51
52var selectsElement = document.getElementById( 'selects' );
53var selectsTbodyElement = document.getElementById( 'selectsTbody' );
54var potentialAutoSelects = [];
55potentialAutoSelects['Neighborhood'] = [];
56var selects = [];
57var selectsElements = [];
58
59var calendarElement = document.getElementById( 'calendar' );
60var datePickerElement = document.getElementById( 'datePicker' );
61var datePickerEarliestElement = document.getElementById( 'datePickerEarliest' );
62var datePickerLatestElement = document.getElementById( 'datePickerLatest' );
63var datePickerSelectElement = document.getElementById( 'datePickerSelect' );
64var datePickerEarliestContainerElement = document.getElementById( 'datePickerEarliestContainer' );
65var datePickerLatestContainerElement = document.getElementById( 'datePickerLatestContainer' );
66var pubDatesExist;
67
68var map = null;
69var channels = null;
70var points = null;
71var markers = null;
72
73var icons;
74var iconIdx;
75
76var iconSizeHelper = [];
77
78
79function Setup()
80    {
81    acme.maps.Initialize();
82
83    // ESC closes any popups.
84    acme.keynav.KeyDown( acme.keynav.escKeyCode, PopUpCloseAll );
85
86    // Collect the xmlsrc parameters  We don't use acme.utils.GetParameters
87    // because we want to allow multiple parameters with the same name.
88    var str = acme.utils.GetHash();
89    if ( ! str )
90	str = acme.utils.GetQuery();
91    var namevals = str.split( '&' );
92    xmlsrcs = [];
93    for ( var i = 0; i < namevals.length; ++i )
94	{
95	var nameval = namevals[i].split( '=' );
96	if ( nameval.length == 2 && nameval[0] == 'xmlsrc' )
97	    xmlsrcs.push( decodeURIComponent( nameval[1].replace( /\+/g, ' ' )) );
98	}
99
100    // If there were no xmlsrcs, go to the about page.
101    if ( xmlsrcs.length == 0 )
102	{
103	location.replace( location.protocol + '//' + location.host + '/GeoRSS/about.html' );
104	return;
105	}
106
107    // Fix protocols. To avoid problems with mixing http/https protocols,
108    // it's common to leave the protocol off of subsidiary resource URLs.
109    // Browsers know to fill in the same protocol as the parent page.
110    // However this doesn't happen automatically for the xmlsrc parameters.
111    // So, we make it happen.
112    for ( var i in xmlsrcs )
113	if ( xmlsrcs[i].charAt( 0 ) == '/' )
114	    xmlsrcs[i] = location.protocol + xmlsrcs[i];
115
116    // Layout the panels.
117    Layout();
118    window.onresize = Layout;
119
120    // Create the map.
121    map = acme.maps.CreateMap( 'map' );
122
123    icons = [ acme.maps.blueIcon, acme.maps.redIcon, acme.maps.greenIcon, acme.maps.yellowIcon, acme.maps.purpleIcon, acme.maps.brownIcon, acme.maps.orangeIcon, acme.maps.whiteIcon, acme.maps.blackIcon ];
124
125    iconSizeHelper['http://www.dbaclick.com/images/train8.gif'] = [ 28, 30 ];
126
127    AddControlElement( map, searchElement, 'topright' );
128    searchPatternElement.value = ''
129    searchTimer = setInterval( SearchCheck, 100 );
130
131    AddControlElement( map, calendarElement, 'topright' );
132
133    // Start fetching the XML files.
134    iconIdx = 0;
135    channels = [];
136    xi = 0;
137    FetchNextXmlSrc();
138    }
139
140
141function Layout()
142    {
143    var wh = acme.utils.GetPageWH();
144    var bodyWidth = wh.w;
145    var bodyHeight = wh.h;
146    listElement.style.bottom = footerElement.offsetHeight + 'px';
147    mapElement.style.top = headerElement.offsetHeight + 'px';
148    mapElement.style.left = 0;
149    if ( bodyWidth < bodyHeight )
150        {
151	// Narrow.
152	listElement.style.left = 0;
153	listElement.style.right = 0;
154	listElement.style.height = '30vw';
155	mapElement.style.right = 0;
156	mapElement.style.bottom = ( footerElement.offsetHeight + listElement.offsetHeight ) + 'px';
157        }
158    else
159	{
160	// Not narrow.
161	listElement.style.top = headerElement.offsetHeight + 'px';
162	listElement.style.right = 0;
163	listElement.style.width = '20vw';
164	mapElement.style.right = listElement.offsetWidth + 'px';
165	mapElement.style.bottom = footerElement.offsetHeight + 'px';
166	}
167    if ( map )
168	map.invalidateSize();
169    }
170
171
172function FetchNextXmlSrc()
173    {
174    if ( xi >= xmlsrcs.length )
175	{
176	// Done.
177	LoadAllJS( channels );
178	Layout();
179	Controls();
180	return;
181	}
182
183    acme.utils.HttpGet( 'geocode_proxy.cgi?url=' + encodeURIComponent( xmlsrcs[xi] ), RequestChecker );
184    }
185
186
187function RequestChecker( request )
188    {
189    if ( request.responseXML == null )
190	alert( 'XML parse failed!' );
191    else
192	{
193	TraverseDocument( request.responseXML.documentElement, xmlsrcs[xi] );
194	Display();
195	}
196
197    // Start the next fetch.
198    ++xi;
199    FetchNextXmlSrc();
200    }
201
202
203function Controls()
204    {
205    var i, j;
206
207    // Add any potential auto-selects that were used.
208    for ( i in potentialAutoSelects )
209	if ( potentialAutoSelects[i].length > 0 )
210	    {
211	    var options = potentialAutoSelects[i].sort();
212	    for ( j in options )
213		options[j] = { text: options[j], value: '"' + i + ' ' + options[j] + '"' };
214	    selects.push( { name: i, options: options } );
215	    }
216
217    // Add any selects.
218    if ( selects.length > 0 )
219	{
220	for ( i in selects )
221	    {
222	    var select = selects[i];
223	    var tr = acme.utils.AppendElement( selectsTbodyElement, 'tr', {} );
224	    var td1 = acme.utils.AppendElement( tr, 'td', {} );
225	    acme.utils.AppendText( td1, select.name );
226	    var td2 = acme.utils.AppendElement( tr, 'td', {} );
227	    var selectElement = acme.utils.AppendElement( td2, 'select', { name: select.name, onchange: SearchFieldsChanged } );
228	    acme.utils.AppendElement( selectElement, 'option', { innerHTML: '' } );
229	    for ( j in select.options )
230		{
231		var option = select.options[j];
232		acme.utils.AppendElement( selectElement, 'option', { value: option.value, innerHTML: option.text } );
233		}
234	    selectsElements.push( selectElement );
235	    }
236	}
237    else
238	acme.utils.HideElement( selectsElement );
239
240    pubDatesExist = false;
241    for ( i = 0; i < channels.length; ++i )
242	for ( j = 0; j < channels[i].items.length; ++j )
243	    if ( channels[i].items[j].pubDate )
244		pubDatesExist = true;
245    if ( pubDatesExist )
246	acme.utils.ShowElement( calendarElement );
247    }
248
249
250// The following few dozen routines traverse the XML parse tree.  There's
251// a separate routine for each type of element.  The code does get a
252// little spread out but the advantage is you hit each node precisely
253// once, no duplication of effort possible.
254
255
256function TraverseDocument( element, xmlsrc )
257    {
258    var child = element.firstChild;
259    while ( child )
260	{
261	if ( child.nodeName == 'channel' )
262	    TraverseChannel( child, xmlsrc );
263	child = child.nextSibling;
264	}
265    }
266
267
268function TraverseChannel( element, xmlsrc )
269    {
270    var channel = {};
271    channel.xmlsrc = xmlsrc;
272    channel.groups = [];
273    channel.groupIdxLookup = []
274    channel.items = [];
275
276    var child = element.firstChild;
277    while ( child )
278	{
279	if ( child.nodeName == 'link' )
280	    TraverseChannelLink( child, channel );
281	else if ( child.nodeName == 'title' )
282	    TraverseChannelTitle( child, channel );
283	else if ( child.nodeName == 'description' )
284	    TraverseChannelDescription( child, channel );
285	else if ( child.nodeName == 'managingEditor' )
286	    TraverseChannelManagingEditor( child, channel );
287	else if ( child.nodeName == 'webMaster' )
288	    TraverseChannelWebMaster( child, channel );
289	else if ( child.nodeName == 'image' )
290	    TraverseChannelImage( child, channel );
291	else if ( child.nodeName == 'geo:lat' || child.nodeName == 'lat' )
292	    TraverseChannelGeoLat( child, channel );
293	else if ( child.nodeName == 'geo:long' || child.nodeName == 'geo:lon' || child.nodeName == 'geo:lng' || child.nodeName == 'long' || child.nodeName == 'lon' || child.nodeName == 'lng' )
294	    TraverseChannelGeoLong( child, channel );
295	else if ( child.nodeName == 'georss:point' || child.nodeName == 'point' )
296	    TraverseChannelGeorssPoint( child, channel );
297	else if ( child.nodeName == 'ymaps:ZoomLevel' || child.nodeName == 'ZoomLevel' )
298	    TraverseChannelYmapsZoomLevel( child, channel );
299	else if ( child.nodeName == 'ymaps:Groups' || child.nodeName == 'Groups' )
300	    TraverseYmapsGroups( child, channel );
301//	else if ( child.nodeName == 'js' )
302//	    TraverseChannelJS( child, channel );
303	else if ( child.nodeName == 'acme:select' )
304	    TraverseAcmeSelect( child, channel );
305	else if ( child.nodeName == 'item' )
306	    TraverseItem( child, channel );
307	child = child.nextSibling;
308	}
309
310    if ( channel.yZoom != null )
311	channel.gZoom = 17 - Math.round( channel.yZoom * 3 / 2 );
312
313    // Save the channel.
314    channels.push( channel );
315    }
316
317
318function TraverseChannelLink( element, channel )
319    {
320    channel.link = acme.utils.GetXmlText( element );
321    }
322
323
324function TraverseChannelTitle( element, channel )
325    {
326    channel.title = acme.utils.GetXmlText( element );
327    }
328
329
330function TraverseChannelDescription( element, channel )
331    {
332    channel.description = acme.utils.GetXmlText( element );
333    }
334
335
336function TraverseChannelManagingEditor( element, channel )
337    {
338    channel.managingEditor = acme.utils.GetXmlText( element );
339    }
340
341
342function TraverseChannelWebMaster( element, channel )
343    {
344    channel.webMaster = acme.utils.GetXmlText( element );
345    }
346
347
348function TraverseChannelImage( element, channel )
349    {
350    var child = element.firstChild;
351    while ( child )
352	{
353	if ( child.nodeName == 'url' )
354	    TraverseChannelImageUrl( child, channel );
355	else if ( child.nodeName == 'title' )
356	    TraverseChannelImageTitle( child, channel );
357	else if ( child.nodeName == 'description' )
358	    TraverseChannelImageDescription( child, channel );
359	else if ( child.nodeName == 'link' )
360	    TraverseChannelImageLink( child, channel );
361	else if ( child.nodeName == 'width' )
362	    TraverseChannelImageWidth( child, channel );
363	else if ( child.nodeName == 'height' )
364	    TraverseChannelImageHeight( child, channel );
365	child = child.nextSibling;
366	}
367    }
368
369
370function TraverseChannelImageUrl( element, channel )
371    {
372    channel.imageUrl = acme.utils.GetXmlText( element );
373    }
374
375
376function TraverseChannelImageTitle( element, channel )
377    {
378    channel.imageTitle = acme.utils.GetXmlText( element );
379    }
380
381
382function TraverseChannelImageDescription( element, channel )
383    {
384    channel.imageDescription = acme.utils.GetXmlText( element );
385    }
386
387
388function TraverseChannelImageLink( element, channel )
389    {
390    channel.imageLink = acme.utils.GetXmlText( element );
391    }
392
393
394function TraverseChannelImageWidth( element, channel )
395    {
396    channel.imageWidth = parseInt( acme.utils.GetXmlText( element ) );
397    }
398
399
400function TraverseChannelImageHeight( element, channel )
401    {
402    channel.imageHeight = parseInt( acme.utils.GetXmlText( element ) );
403    }
404
405
406function TraverseChannelGeoLat( element, channel )
407    {
408    channel.lat = parseFloat( acme.utils.GetXmlText( element ) );
409    }
410
411
412function TraverseChannelGeoLong( element, channel )
413    {
414    channel.lng = parseFloat( acme.utils.GetXmlText( element ) );
415    }
416
417
418function TraverseChannelGeoPoint( element, channel )
419    {
420    var vals = acme.utils.GetXmlText( element ).split( ' ' );
421    if ( vals.length == 2 )
422	{
423	channel.lat = parseFloat( vals[0] );
424	channel.lng = parseFloat( vals[1] );
425	}
426    }
427
428
429
430function TraverseChannelYmapsZoomLevel( element, channel )
431    {
432    channel.yZoom = parseInt( acme.utils.GetXmlText( element ) );
433    }
434
435
436function TraverseYmapsGroups( element, channel )
437    {
438    var child = element.firstChild;
439    while ( child )
440	{
441	if ( child.nodeName == 'group' || child.nodeName == 'Group' )
442	    TraverseGroup( child, channel );
443	child = child.nextSibling;
444	}
445    }
446
447
448function TraverseGroup( element, channel )
449    {
450    var group = {};
451
452    var child = element.firstChild;
453    while ( child )
454	{
455	if ( child.nodeName == 'title' || child.nodeName == 'Title' )
456	    TraverseGroupTitle( child, group );
457	else if ( child.nodeName == 'id' || child.nodeName == 'Id' )
458	    TraverseGroupId( child, group );
459	else if ( child.nodeName == 'ymaps:BaseIcon' || child.nodeName == 'BaseIcon' )
460	    TraverseGroupBaseIcon( child, group );
461	else if ( child.nodeName == 'ymaps:HoverIcon' || child.nodeName == 'HoverIcon' )
462	    TraverseGroupHoverIcon( child, group );
463	else if ( child.nodeName == 'ymaps:PopupIcon' || child.nodeName == 'PopupIcon' )
464	    TraverseGroupPopupIcon( child, group );
465	child = child.nextSibling;
466	}
467
468    if ( group.id )
469	channel.groupIdxLookup[group.id] = channel.groups.length
470    if ( group.icon == null )
471	{
472	group.icon = icons[iconIdx];
473	iconIdx = ( iconIdx + 1 ) % icons.length;
474	}
475
476    channel.groups.push( group );
477    }
478
479
480function TraverseGroupTitle( element, group )
481    {
482    group.title = acme.utils.GetXmlText( element );
483    }
484
485
486function TraverseGroupId( element, group )
487    {
488    group.id = acme.utils.GetXmlText( element );
489    }
490
491
492function TraverseGroupBaseIcon( element, group )
493    {
494    group.baseIcon = acme.utils.GetXmlText( element );
495
496    group.icon = L.icon(
497	{
498	iconUrl: group.baseIcon
499	} );
500    LookupIconSize( group.icon );
501    }
502
503
504function TraverseGroupHoverIcon( element, group )
505    {
506    group.hoverIcon = acme.utils.GetXmlText( element );
507    // Nothing done with this yet.
508    }
509
510
511function TraverseGroupPopupIcon( element, group )
512    {
513    group.popupIcon = acme.utils.GetXmlText( element );
514    // Nothing done with this yet.
515    }
516
517
518function TraverseChannelJS( element, channel )
519    {
520    if ( channel.js == null )
521	channel.js = []
522    channel.js.push( acme.utils.GetXmlText( element ) );
523    }
524
525
526function TraverseAcmeSelect( element, channel )
527    {
528    var select = { options: [] }
529
530    select.name = element.getAttribute( 'name' );
531
532    var child = element.firstChild;
533    while ( child )
534	{
535	if ( child.nodeName == 'acme:option' )
536	    TraverseAcmeSelectOption( child, select );
537	child = child.nextSibling;
538	}
539
540    selects.push( select );
541    }
542
543
544function TraverseAcmeSelectOption( element, select )
545    {
546    var text = acme.utils.GetXmlText( element );
547    if ( text )
548	text = DeFang( text );
549    var value = element.getAttribute( 'value' );
550    if ( value )
551	value = DeFang( value );
552    else
553	value = '"' + text + '"';
554    select.options.push ( { text: text, value: value } );
555    }
556
557
558function TraverseItem( element, channel )
559    {
560    var item = {};
561    item.groupIdx = -1;
562    item.extraLinks = [];
563    item.extraImages = [];
564
565    var child = element.firstChild;
566    while ( child )
567	{
568	if ( child.nodeName == 'title' )
569	    TraverseItemTitle( child, item );
570	else if ( child.nodeName == 'pubDate' )
571	    TraverseItemPubDate( child, item );
572	else if ( child.nodeName == 'link' )
573	    TraverseItemLink( child, item );
574	else if ( child.nodeName == 'description' )
575	    TraverseItemDescription( child, item );
576	else if ( child.nodeName == 'author' )
577	    TraverseItemAuthor( child, item );
578	else if ( child.nodeName == 'geo:lat' || child.nodeName == 'lat' )
579	    TraverseItemGeoLat( child, item );
580	else if ( child.nodeName == 'geo:long' || child.nodeName == 'geo:lon' || child.nodeName == 'geo:lng' || child.nodeName == 'long' || child.nodeName == 'lon' || child.nodeName == 'lng' )
581	    TraverseItemGeoLong( child, item );
582	else if ( child.nodeName == 'georss:point' || child.nodeName == 'point' )
583	    TraverseItemGeorssPoint( child, item );
584	else if ( child.nodeName == 'ymaps:Address' || child.nodeName == 'Address' )
585	    TraverseItemYmapsAddress( child, item );
586	else if ( child.nodeName == 'acme:neighborhood' || child.nodeName == 'neighborhood' )
587	    TraverseItemAcmeNeighborhood( child, item );
588	else if ( child.nodeName == 'ymaps:CityState' || child.nodeName == 'CityState' )
589	    TraverseItemYmapsCityState( child, item );
590	else if ( child.nodeName == 'ymaps:Zip' || child.nodeName == 'Zip' )
591	    TraverseItemYmapsZip( child, item );
592	else if ( child.nodeName == 'ymaps:Country' || child.nodeName == 'Country' )
593	    TraverseItemYmapsCountry( child, item );
594	else if ( child.nodeName == 'ymaps:PhoneNumber' || child.nodeName == 'PhoneNumber' )
595	    TraverseItemYmapsPhoneNumber( child, item );
596	else if ( child.nodeName == 'ymaps:GroupId' || child.nodeName == 'GroupId' )
597	    TraverseItemYmapsGroupId( child, item );
598	else if ( child.nodeName == 'ymaps:BaseIcon' || child.nodeName == 'BaseIcon' )
599	    TraverseItemYmapsBaseIcon( child, item );
600	else if ( child.nodeName == 'ymaps:HoverIcon' || child.nodeName == 'HoverIcon' )
601	    TraverseItemYmapsHoverIcon( child, item );
602	else if ( child.nodeName == 'ymaps:PopupIcon' || child.nodeName == 'PopupIcon' )
603	    TraverseItemYmapsPopupIcon( child, item );
604	else if ( child.nodeName == 'ymaps:ExtraLink' || child.nodeName == '
604ExtraLink' )
605	    TraverseItemYmapsExtraLink( child, item );
606	else if ( child.nodeName == 'ymaps:ExtraImage' || child.nodeName == 'ExtraImage' )
607	    TraverseItemYmapsExtraImage( child, item );
608	else if ( child.nodeName == 'ymaps:ItemUrl' || child.nodeName == 'ItemUrl' )
609	    TraverseItemYmapsItemUrl( child, item );
610	child = child.nextSibling;
611	}
612
613    if ( item.groupId != null )
614	item.groupIdx = channel.groupIdxLookup[item.groupId];
615
616    channel.items.push( item );
617    }
618
619
620function TraverseItemTitle( element, item )
621    {
622    item.title = acme.utils.GetXmlText( element );
623    }
624
625
626function TraverseItemPubDate( element, item )
627    {
628    item.pubDateStr = acme.utils.GetXmlText( element );
629    item.pubDate = new Date( item.pubDateStr );
630    }
631
632
633function TraverseItemLink( element, item )
634    {
635    item.link = acme.utils.GetXmlText( element );
636    }
637
638
639function TraverseItemDescription( element, item )
640    {
641    item.description = acme.utils.GetXmlText( element );
642    }
643
644
645function TraverseItemAuthor( element, item )
646    {
647    item.author = acme.utils.GetXmlText( element );
648    }
649
650
651function TraverseItemGeoLat( element, item )
652    {
653    item.lat = parseFloat( acme.utils.GetXmlText( element ) );
654    }
655
656
657function TraverseItemGeoLong( element, item )
658    {
659    item.lng = parseFloat( acme.utils.GetXmlText( element ) );
660    }
661
662
663function TraverseItemGeorssPoint( element, item )
664    {
665    var vals = acme.utils.GetXmlText( element ).split( ' ' );
666    if ( vals.length == 2 )
667	{
668	item.lat = parseFloat( vals[0] );
669	item.lng = parseFloat( vals[1] );
670	}
671    }
672
673
674function TraverseItemYmapsAddress( element, item )
675    {
676    item.address = acme.utils.GetXmlText( element );
677    }
678
679
680function TraverseItemAcmeNeighborhood( element, item )
681    {
682    item.neighborhood = acme.utils.GetXmlText( element );
683    var options = potentialAutoSelects['Neighborhood'];
684    if ( ! options.includes( item.neighborhood ) )
685	options.push( item.neighborhood );
686    }
687
688
689function TraverseItemYmapsCityState( element, item )
690    {
691    item.cityState = acme.utils.GetXmlText( element );
692    }
693
694
695function TraverseItemYmapsZip( element, item )
696    {
697    item.zip = acme.utils.GetXmlText( element );
698    }
699
700
701function TraverseItemYmapsCountry( element, item )
702    {
703    item.country = acme.utils.GetXmlText( element );
704    }
705
706
707function TraverseItemYmapsPhoneNumber( element, item )
708    {
709    item.phoneNumber = acme.utils.GetXmlText( element );
710    }
711
712
713function TraverseItemYmapsGroupId( element, item )
714    {
715    item.groupId = acme.utils.GetXmlText( element );
716    if ( item.groupIdx == null )
717	item.groupIdx = -1;
718    }
719
720
721function TraverseItemYmapsBaseIcon( element, item )
722    {
723    item.baseIcon = acme.utils.GetXmlText( element );
724
725    item.icon = L.icon(
726	{
727	iconUrl: item.baseIcon
728	} );
729    LookupIconSize( item.icon );
730    }
731
732
733function TraverseItemYmapsHoverIcon( element, item )
734    {
735    item.hoverIcon = acme.utils.GetXmlText( element );
736    // Nothing done with this yet.
737    }
738
739
740function TraverseItemYmapsPopupIcon( element, item )
741    {
742    item.popupIcon = acme.utils.GetXmlText( element );
743    // Nothing done with this yet.
744    }
745
746
747function TraverseItemYmapsExtraLink( element, item )
748    {
749    var extraLink = {};
750    extraLink.text = acme.utils.GetXmlText( element );
751    extraLink.href = element.getAttribute( 'href' );
752    item.extraLinks.push( extraLink );
753    }
754
755
756function TraverseItemYmapsExtraImage( element, item )
757    {
758    var extraImage = {};
759
760    var child = element.firstChild;
761    while ( child )
762	{
763	if ( child.nodeName == 'url' )
764	    TraverseExtraImageUrl( child, extraImage );
765	else if ( child.nodeName == 'title' )
766	    TraverseExtraImageTitle( child, extraImage );
767	else if ( child.nodeName == 'link' )
768	    TraverseExtraImageLink( child, extraImage );
769	else if ( child.nodeName == 'width' )
770	    TraverseExtraImageWidth( child, extraImage );
771	else if ( child.nodeName == 'height' )
772	    TraverseExtraImageHeight( child, extraImage );
773	child = child.nextSibling;
774	}
775
776    item.extraImages.push( extraImage );
777    }
778
779
780function TraverseExtraImageUrl( element, extraImage )
781    {
782    extraImage.url = acme.utils.GetXmlText( element );
783    }
784
785
786function TraverseExtraImageTitle( element, extraImage )
787    {
788    extraImage.title = acme.utils.GetXmlText( element );
789    }
790
791
792function TraverseExtraImageLink( element, extraImage )
793    {
794    extraImage.link = acme.utils.GetXmlText( element );
795    }
796
797
798function TraverseExtraImageWidth( element, extraImage )
799    {
800    extraImage.imageWidth = parseInt( acme.utils.GetXmlText( element ) );
801    }
802
803
804function TraverseExtraImageHeight( element, extraImage )
805    {
806    extraImage.imageHeight = parseInt( acme.utils.GetXmlText( element ) );
807    }
808
809
810function TraverseItemYmapsItemUrl( element, item )
811    {
812    item.itemUrl = acme.utils.GetXmlText( element );
813    }
814
815
816// - - - - - - - - - - End of XML traversal. - - - - - - - - -
817
818
819function GetGroupIcon( channel, groupIdx )
820    {
821    if ( groupIdx == -1 || channel.groups[groupIdx] == null )
822	{
823	if ( channel.defaultIcon == null )
824	    {
825	    channel.defaultIcon = icons[iconIdx];
826	    iconIdx = ( iconIdx + 1 ) % icons.length;
827	    }
828	return channel.defaultIcon;
829	}
830    var groupIcon = channel.groups[groupIdx].icon;
831    if ( groupIcon == null )
832	return icons[0];	// shouldn't happen
833    return groupIcon;
834    }
835
836
837function GetItemIcon( channel, item )
838    {
839    if ( item.icon )
840	return item.icon;
841    return GetGroupIcon( channel, item.groupIdx );
842    }
843
844
845function GetGroupIconUrl( channel, groupIdx )
846    {
847    var groupIcon = GetGroupIcon( channel, groupIdx );
848    var groupUrl = groupIcon.url;
849    return groupUrl;
850    }
851
852
853function GetItemIconUrl( channel, item )
854    {
855    var itemIcon = GetItemIcon( channel, item );
856    var itemUrl = itemIcon.url;
857    return itemUrl;
858    }
859
860
861function GetGroupSmallUrl( channel, groupIdx )
862    {
863    var groupIcon = GetGroupIcon( channel, groupIdx );
864    var groupSmallUrl = groupIcon.options.smallUrl;
865    if ( ! groupSmallUrl )
866	groupSmallUrl = groupIcon.options.iconUrl;
867    return groupSmallUrl;
868    }
869
870
871function GetItemSmallUrl( channel, item )
872    {
873    var itemIcon = GetItemIcon( channel, item );
874    var itemSmallUrl = itemIcon.options.smallUrl;
875    if ( ! itemSmallUrl )
876	itemSmallUrl = itemIcon.options.iconUrl;
877    return itemSmallUrl;
878    }
879
880
881function LoadAllJS( channels )
882    {
883    for ( var i = 0; i < channels.length; ++i )
884	{
885	var channel = channels[i];
886	var js = channel.js;
887	if ( js != null )
888	    LoadJS( js, channel.xmlsrc )
889	}
890    }
891
892function LoadJS( js, xmlsrc )
893    {
894    for ( var i = 0; i < js.length; ++i )
895	{
896	var jurl = js[i];
897
898	// Check that the JS URL has the same origin as the XML URL.
899	try
900	    {
901	    if ( ! SameOrigin( jurl, xmlsrc ) )
902		{
903		alert( 'JS URL has a different origin!' );
904		return;
905		}
906	    }
907	catch ( e )
908	    {
909	    alert( 'JS URL parse error!' );
910	    return;
911	    }
912	alert( 'ok to load ' + jurl );	//!!!
913	}
914
915    //!!!
916    }
917
918function SameOrigin( urlstr1, urlstr2 )
919    {
920    var url1 = new URL( urlstr1 );
921    var url2 = new URL( urlstr2 );
922    return url1.origin == url2.origin;
923    // This check is too strict, really. But that's beter than too lenient.
924    }
925
926
927// Display the current state.
928function Display()
929    {
930    var i, j, channel, item, same, html, address, title;
931    var imageHtml = '';
932    var titleHtml = '';
933    var descriptionHtml = '';
934    var groupsHtml = '';
935    var listHtml = '';
936    var plainHtmlHtml = '';
937
938    // Get rid of any existing points and markers.
939    if ( markers !== null )
940	for ( i = 0; i < markers.length; ++i )
941	    markers[i].remove();
942    points = [];
943    markers = [];
944
945    for ( i = 0; i < channels.length; ++i )
946	{
947	channel = channels[i];
948
949	// Image.
950	if ( channel.imageUrl || channel.imageDescription )
951	    {
952	    same = false;
953	    for ( j = 0; j < i; ++j )
954		if ( channel.imageUrl == channels[j].imageUrl && channel.imageLink == channels[j].imageLink && channel.imageTitle == channels[j].imageTitle && channel.imageDescription == channels[j].imageDescription )
955		    {
956		    same = true;
957		    break;
958		    }
959	    if ( ! same )
960		{
961		html = '<table border="0">';
962		if ( channel.imageUrl )
963		    {
964		    html += '<tr><td>'
965		    if ( channel.imageLink )
966			html += '<a target="_top" href="' + encodeURI( channel.imageLink ) + '">';
967		    html += '<img ';
968		    if ( channel.imageTitle )
969			html += 'alt="' + channel.imageTitle + '" ';
970		    if ( channel.imageWidth != null )
971			html += 'width="' + channel.imageWidth + '" ';
972		    if ( channel.imageHeight != null )
973			html += 'height="' + channel.imageHeight + '" ';
974		    html += 'src="' + channel.imageUrl + '" />';
975		    if ( channel.imageLink )
976			html += '</a>';
977		    html += '</td></tr>'
978		    }
979		if ( channel.imageDescription )
980		    {
981		    html += '<tr><td>'
982		    if ( channel.imageLink )
983			html += '<a target="_top" href="' + channel.imageLink + '">';
984		    html += channel.imageDescription;
985		    if ( channel.imageLink )
986			html += '</a>';
987		    html += '</td></tr>'
988		    }
989		html += '</table>'
990		imageHtml += html;
991		}
992	    }
993	if ( imageHtml == '' )
994	    imageHtml = '<img src="//acme.com/resources/images/atom_ani.gif" />';
995
996	// Title.
997	if ( channel.title )
998	    {
999	    same = false;
1000	    for ( j = 0; j < i; ++j )
1001		if ( channel.title == channels[j].title && channel.link == channels[j].link )
1002		    {
1003		    same = true;
1004		    break;
1005		    }
1006	    if ( ! same )
1007		{
1008		html = '';
1009		if ( channel.link )
1010		    html += '<a href="' + channel.link + '">';
1011		html += channel.title;
1012		if ( channel.link )
1013		    html += '</a>';
1014		html += '<br />';
1015		titleHtml += html;
1016		}
1017	    }
1018
1019	// Description.
1020	if ( channel.description )
1021	    {
1022	    same = false;
1023	    for ( j = 0; j < i; ++j )
1024		if ( channel.description == channels[j].description )
1025		    {
1026		    same = true;
1027		    break;
1028		    }
1029	    if ( ! same )
1030		descriptionHtml += channel.description + '<br />';
1031	    }
1032
1033	// Groups.
1034	if ( channel.groups.length > 0 )
1035	    {
1036	    html = '<table border="1" cellpadding="5"><tbody><tr>';
1037	    var perRow = 0;
1038	    for ( j = 0; j < channel.groups.length; ++j )
1039		{
1040		if ( perRow >= 5 )
1041		    {
1042		    html += '</tr><tr>';
1043		    perRow = 0;
1044		    }
1045		html += '<td><img src="' + GetGroupSmallUrl( channel, j ) + '" />' + channel.groups[j].title + '</td>';
1046		++perRow;
1047		}
1048	    html += '</tr></tbody></table><br />';
1049	    groupsHtml += html;
1050	    }
1051
1052	// Items: markers and list.
1053	for ( j = 0; j < channel.items.length; ++j )
1054	    {
1055	    item = channel.items[j];
1056
1057	    if ( searchFieldsShowing )
1058		{
1059		// Text search and selects check.
1060		var searchPattern = searchPatternElement.value;
1061		var searchText = '';
1062		if ( item.title ) searchText += item.title + ' ';
1063		if ( item.description ) searchText += item.description + ' ';
1064		if ( item.address ) searchText += item.address + ' ';
1065		if ( item.neighborhood ) searchText += '"Neighborhood ' + item.neighborhood + '" ';
1066		if ( item.cityState ) searchText += item.cityState + ' ';
1067		if ( selects.length > 0 )
1068		    {
1069		    for ( var k in selectsElements )
1070			{
1071			var value = selectsElements[k].value;
1072			if ( value )
1073			    searchPattern += value + ' ';
1074			}
1075		    }
1076		if ( searchPattern && searchText )
1077		    if ( ! GooGrep( searchPattern, searchText ) )
1078			continue;	// don't show this item
1079		}
1080
1081	    if ( datePickerShowing )
1082		{
1083		// Date check. Add a day of slop on each end for timezones.
1084		if ( pubDatesExist && item.pubDate &&
1085		     datePickerEarliest && datePickerLatest &&
1086		     ( item.pubDate.getTime() < datePickerEarliest.getTime() - dayMsecs ||
1087		       item.pubDate.getTime() > datePickerLatest.getTime() + dayMsecs ) )
1088		    continue;	// don't show this item
1089		}
1090
1091	    if ( item.lat != null && item.lng != null )
1092		{
1093		var point = L.latLng( item.lat, item.lng );
1094		var marker = new L.marker( point, { icon: GetItemIcon( channel, item ), title: acme.utils.EntityToIso8859( item.title ), alt: item.title } ).addTo( map );
1095		marker.channelIdx = i;
1096		marker.itemIdx = j;
1097		item.markerIdx = markers.length;
1098		marker.bindPopup( acme.utils.Partial( PopUpHTML, item.markerIdx ) );
1099		points.push( point );
1100		markers.push( marker );
1101		}
1102
1103	    if ( item.title )
1104		{
1105		html = '';
1106		if ( item.markerIdx != null )
1107		    {
1108		    var link = '<a href="javascript:PopUpOpen( ' + item.markerIdx + ' )">';
1109		    html += link + '<img border="0" src="' + GetItemSmallUrl( channel, item ) + '" /></a> ' + link + item.title + '</a>';
1110		    }
1111		else if ( item.link )
1112		    html += '<a target="_top" href="' + item.link + '">' + item.title + '</a>';
1113		else
1114		    html += item.title;
1115		html += '<br />';
1116		listHtml += html;
1117		}
1118	    }
1119
1120	if ( channel.title )
1121	    title = channel.title;
1122	else
1123	    title = xmlsrcs[xi];
1124	html = '<a target="_top" href="html.cgi?xmlsrc=' + encodeURIComponent( xmlsrcs[xi] ) + '">' + title + ' as plain HTML</a>';
1125	plainHtmlHtml += html;
1126	}
1127
1128    if ( channels.length == 1 && channels[0].lat != null && channels[0].lng != null && channels[0].gZoom != null )
1129	map.setView( L.latLng( channels[0].lat, channels[0].lng ), channels[0].gZoom );
1130    else if ( points.length > 0 )
1131	map.fitBounds( L.latLngBounds( points ), { animate: false } );
1132
1133    imageElement.innerHTML = imageHtml;
1134    titleElement.innerHTML = titleHtml;
1135    descriptionElement.innerHTML = descriptionHtml;
1136    groupsElement.innerHTML = groupsHtml;
1137    listElement.innerHTML = listHtml;
1138    plainHtmlElement.innerHTML = plainHtmlHtml;
1139    }
1140
1141
1142// Text search stuff.
1143
1144var searchFieldsShowing = false;
1145var searchTimer;
1146var searchPrevPattern = '';
1147
1148function SearchFieldsToggle()
1149    {
1150    searchFieldsShowing = ! searchFieldsShowing;
1151    if ( searchFieldsShowing )
1152	{
1153	acme.utils.ShowElement( searchFieldsElement );
1154	searchPatternElement.focus();
1155	}
1156    else
1157	{
1158	acme.utils.HideElement( searchFieldsElement );
1159	}
1160    SearchFieldsChanged();
1161    }
1162
1163
1164function SearchCheck()
1165    {
1166    if ( searchPatternElement.value != searchPrevPattern )
1167	{
1168	searchPrevPattern = searchPatternElement.value;
1169	SearchFieldsChanged();
1170	}
1171    }
1172
1173
1174function GooGrep( pattern, text )
1175    {
1176    // Split pattern up into an array of terms.
1177    var terms = [];
1178    var i = 0, j, prefix, postfix;
1179    while ( i < pattern.length )
1180	{
1181	// Skip spaces.
1182	if ( pattern.charAt( i ) == ' ' )
1183	    {
1184	    ++i;
1185	    continue;
1186	    }
1187	// Find the end of this term.
1188	var c = pattern.charAt( i );
1189	if ( c == '"' || c == "'" )
1190	    {
1191	    // Quoted string.
1192	    ++i;
1193	    j = i + 1;
1194	    while ( j < pattern.length && pattern.charAt( j ) != c )
1195		++j;
1196	    // Quoted strings must match at word boundaries. For reasons.
1197	    prefix = '(?:^|[^a-z0-9_&;])';
1198	    postfix = '(?:[^a-z0-9_&;]|$)';
1199	    //!!! Adding &; to the non-words character class is kind of hackish.
1200	    }
1201	else
1202	    {
1203	    // Not quoted.
1204	    j = i + 1;
1205	    while ( j < pattern.length && pattern.charAt( j ) != ' ' )
1206		++j;
1207	    prefix = postfix = '';
1208	    }
1209	// Turn it into a regular expression and save it.
1210	var str = pattern.slice( i, j );
1211	if ( RegExp.escape )
1212	    str = RegExp.escape( str );
1213	var restr = prefix + str + postfix;
1214	terms.push( new RegExp( restr, 'i' ) );
1215	// And go on to the next one.
1216	i = j + 1;
1217	}
1218
1219    // Now do the matches. The text must match all the terms.
1220    for ( i in terms )
1221	{
1222	if ( ! terms[i].test( text ) )
1223	    return false;
1224	}
1225    return true;
1226    }
1227
1228
1229function DeFang( str )
1230    {
1231    return str.replaceAll( '<', '&lt;' ).replaceAll( '>', '&gt;' );
1232    }
1233
1234
1235function SearchFieldsChanged()
1236    {
1237    Display();
1238    }
1239
1240
1241// Date picker stuff.
1242
1243var datePickerShowing = false;
1244var datePickerFirstTime = true;
1245var datePickerEarliest = null;
1246var datePickerLatest = null;
1247var datePickerEarliestCalendarOpen;
1248var datePickerLatestCalendarOpen;
1249
1250function DatePickerToggle()
1251    {
1252    datePickerShowing = ! datePickerShowing;
1253    if ( datePickerShowing )
1254	{
1255	acme.utils.ShowElement( datePickerElement );
1256	datePickerEarliestCalendarOpen = false;
1257	datePickerLatestCalendarOpen = false;
1258	}
1259    else
1260	{
1261	acme.utils.HideElement( datePickerElement );
1262	acme.utils.HideElement( datePickerEarliestContainer );
1263	acme.utils.HideElement( datePickerLatestContainer );
1264	}
1265    DatePickerChanged();
1266    }
1267
1268
1269var secondMsecs = 1000;
1270var minuteMsecs = secondMsecs * 60;
1271var hourMsecs = minuteMsecs * 60;
1272var dayMsecs = hourMsecs * 24;
1273var weekMsecs = dayMsecs * 7;
1274var monthMsecs = dayMsecs * 31;
1275var yearMsecs = dayMsecs * 365;
1276
1277function DatePickerChanged()
1278    {
1279    if ( datePickerFirstTime )
1280	{
1281	datePickerSelectElement.value = 'all';
1282	datePickerFirstTime = false;
1283	}
1284    var now = new Date();
1285    switch ( datePickerSelectElement.value )
1286	{
1287	case 'day':
1288	datePickerLatest = now;
1289	datePickerEarliest = new Date( now.getTime() - dayMsecs );
1290	break;
1291	case 'week':
1292	datePickerLatest = now;
1293	datePickerEarliest = new Date( now.getTime() - weekMsecs );
1294	break;
1295	case 'month':
1296	datePickerLatest = now;
1297	datePickerEarliest = new Date( now.getTime() - monthMsecs );
1298	break;
1299	case 'year':
1300	datePickerLatest = now;
1301	datePickerEarliest = new Date( now.getTime() - yearMsecs );
1302	break;
1303	case 'all':
1304	var datePickerEarliestTime = 100000000000000;
1305	var datePickerLatestTime = 0;
1306	for ( var i = 0; i < channels.length; ++i )
1307	    {
1308	    var channel = channels[i];
1309	    for ( var j = 0; j < channel.items.length; ++j )
1310		{
1311		var item = channel.items[j];
1312		var pubDate = item.pubDate;
1313		if ( pubDate )
1314		    {
1315		    var pubDateTime = pubDate.getTime();
1316		    if ( pubDateTime < datePickerEarliestTime )
1317			datePickerEarliestTime = pubDateTime;
1318		    if ( pubDateTime > datePickerLatestTime )
1319			datePickerLatestTime = pubDateTime;
1320		    }
1321		}
1322	    }
1323	datePickerEarliest = new Date( datePickerEarliestTime );
1324	datePickerLatest = new Date( datePickerLatestTime );
1325	break;
1326	case 'custom':
1327	// Leave the values alone.
1328	break;
1329	}
1330    datePickerEarliestElement.textContent = acme.utils.Strftime( '%d%b%Y', datePickerEarliest );
1331    datePickerLatestElement.textContent = acme.utils.Strftime( '%d%b%Y', datePickerLatest );
1332    Display();
1333    }
1334
1335
1336function DatePickerCalendarToggle( which )
1337    {
1338    var container;
1339    var date;
1340    switch ( which )
1341	{
1342	case 'earliest':
1343	if ( datePickerEarliestCalendarOpen )
1344	    {
1345	    acme.utils.HideElement( datePickerEarliestContainer );
1346	    datePickerEarliestCalendarOpen = false;
1347	    return;
1348	    }
1349	datePickerEarliestCalendarOpen = true;
1350	datePickerLatestCalendarOpen = false;
1351	container = datePickerEarliestContainer;
1352	date = datePickerEarliest;
1353	break;
1354	case 'latest':
1355	if ( datePickerLatestCalendarOpen )
1356	    {
1357	    acme.utils.HideElement( datePickerLatestContainer );
1358	    datePickerLatestCalendarOpen = false;
1359	    return;
1360	    }
1361	datePickerLatestCalendarOpen = true;
1362	datePickerEarliestCalendarOpen = false;
1363	container = datePickerLatestContainer;
1364	date = datePickerLatest;
1365	break;
1366	}
1367    acme.utils.HideElement( datePickerEarliestContainer );
1368    acme.utils.HideElement( datePickerLatestContainer );
1369    acme.utils.ClearElement( container );
1370    acme.calendar.MakeCalendarDMY( container, date.getDate(), date.getMonth() + 1, date.getFullYear(), acme.utils.Partial( DatePickerCallback, which, container ) );
1371    acme.utils.ShowElement( container );
1372    }
1373
1374
1375function DatePickerCallback( which, container, day, month, year )
1376    {
1377    datePickerSelectElement.value = 'custom';
1378    var date = new Date( year, month - 1, day, 12, 0, 0, 0 );
1379    switch ( which )
1380	{
1381	case 'earliest':
1382	datePickerEarliest = date;
1383	break;
1384	case 'latest':
1385	datePickerLatest = date;
1386	break;
1387	}
1388    DatePickerChanged();
1389    acme.utils.HideElement( container );
1390    datePickerEarliestCalendarOpen = false;
1391    datePickerLatestCalendarOpen = false;
1392    }
1393
1394
1395// Open the info box for the specified marker.
1396function PopUpOpen( m )
1397    {
1398    markers[m].openPopup();
1399    }
1400
1401
1402// Close any open info boxes.
1403function PopUpCloseAll()
1404    {
1405    if ( searchFieldsShowing )
1406	SearchFieldsToggle();
1407    if ( datePickerShowing )
1408	DatePickerToggle();
1409    for ( var m in markers )
1410	markers[m].closePopup();
1411    }
1412
1413
1414// Generate the popup HTML for the specified marker.
1415function PopUpHTML( m )
1416    {
1417    var i;
1418
1419    var marker = markers[m];
1420    if ( marker.channelIdx == null || marker.itemIdx == null )
1421	return;
1422    var item = channels[marker.channelIdx].items[marker.itemIdx];
1423    var html = '';
1424    if ( item.link )
1425	html += '<a target="_top" href="' + item.link + '">';
1426    if ( item.title )
1427	html += '<b>' + item.title + '</b>';
1428    if ( item.link )
1429	html += '</a>';
1430    html += '<br />';
1431    if ( item.pubDate )
1432	{
1433	html += acme.utils.Strftime( '%d%b%Y', item.pubDate );
1434	html += '<br />';
1435	}
1436    if ( item.address )
1437	html += item.address + '<br />';
1438    if ( item.cityState || item.zip || item.country )
1439	{
1440	if ( item.cityState )
1441	    html += item.cityState + ' ';
1442	if ( item.zip )
1443	    html += item.zip + ' ';
1444	if ( item.country )
1445	    html += item.country + ' ';
1446	html += '<br />';
1447	}
1448    if ( item.phoneNumber )
1449	html += item.phoneNumber + '<br />';
1450    if ( item.description )
1451	html += '<p>' + item.description + '</p>';
1452    if ( item.author )
1453	html += '<p>
1453<a target="_top" href="mailto:' + item.author + '">' + item.author + '</a></p>';
1454    for ( i = 0; i < item.extraLinks.length; ++i )
1455	html += '<a target="_top" href="' + item.extraLinks[i].href + '">' + item.extraLinks[i].text + '</a><br />';
1456    for ( i = 0; i < item.extraImages.length; ++i )
1457	{
1458	if ( item.extraImages[i].link )
1459	    html += '<a target="_top" href="' + item.extraImages[i].link + '">';
1460	if ( item.extraImages[i].url )
1461	    {
1462	    html += '<img ';
1463	    if ( item.extraImages[i].title )
1464		html += 'alt="' + item.extraImages[i].title + '" ';
1465	    if ( item.extraImages[i].imageWidth != null )
1466		html += 'width="' + item.extraImages[i].imageWidth + '" ';
1467	    if ( item.extraImages[i].imageHeight != null )
1468		html += 'height="' + item.extraImages[i].imageHeight + '" ';
1469	    html += 'src="' + item.extraImages[i].url + '" style="max-width: 300px; max-height: 300px;" />';
1470	    }
1471	if ( item.extraImages[i].link )
1472	    html += '</a>';
1473	}
1474    if ( item.itemUrl )
1475	html += '<p><iframe src="' + item.itemUrl + '"></iframe></p>';
1476    return html;
1477    }
1478
1479
1480function LookupIconSize( icon )
1481    {
1482    var size = iconSizeHelper[icon.options.iconUrl];
1483    if ( size )
1484	icon.iconSize = size;
1485    }
1486
1487
1488function AddControlElement( aMap, anElement, aPosition )
1489    {
1490    var aControlClass = L.Control.extend( {
1491	onAdd: function ( anotherMap ) {
1492		return anElement;
1493	    }
1494	} );
1495    var options = {};
1496    if ( aPosition )
1497	options.position = aPosition;
1498    var aControl = new aControlClass( options );
1499    aControl.addTo( aMap );
1500    return aControl;
1501    }

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.