diff options
-rw-r--r-- | lib/tagging/__init__.py | 62 | ||||
-rw-r--r-- | lib/tagging/admin.py | 13 | ||||
-rw-r--r-- | lib/tagging/fields.py | 119 | ||||
-rw-r--r-- | lib/tagging/forms.py | 40 | ||||
-rw-r--r-- | lib/tagging/generic.py | 40 | ||||
-rw-r--r-- | lib/tagging/managers.py | 68 | ||||
-rw-r--r-- | lib/tagging/models.py | 490 | ||||
-rw-r--r-- | lib/tagging/settings.py | 13 | ||||
-rw-r--r-- | lib/tagging/templatetags/__init__.py | 0 | ||||
-rw-r--r-- | lib/tagging/templatetags/tagging_tags.py | 231 | ||||
-rw-r--r-- | lib/tagging/tests/__init__.py | 0 | ||||
-rw-r--r-- | lib/tagging/tests/models.py | 42 | ||||
-rw-r--r-- | lib/tagging/tests/settings.py | 27 | ||||
-rw-r--r-- | lib/tagging/tests/tags.txt | 122 | ||||
-rw-r--r-- | lib/tagging/tests/tests.py | 920 | ||||
-rw-r--r-- | lib/tagging/utils.py | 263 | ||||
-rw-r--r-- | lib/tagging/views.py | 52 | ||||
-rw-r--r-- | media/js/mainmap.js | 2015 |
18 files changed, 2502 insertions, 2015 deletions
diff --git a/lib/tagging/__init__.py b/lib/tagging/__init__.py new file mode 100644 index 0000000..fb37886 --- /dev/null +++ b/lib/tagging/__init__.py @@ -0,0 +1,62 @@ +VERSION = (0, 4, 0, "dev", 1) + + + +def get_version(): + if VERSION[3] == "final": + return "%s.%s.%s" % (VERSION[0], VERSION[1], VERSION[2]) + elif VERSION[3] == "dev": + if VERSION[2] == 0: + return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[3], VERSION[4]) + return "%s.%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3], VERSION[4]) + else: + return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3]) + + +__version__ = get_version() + + +class AlreadyRegistered(Exception): + """ + An attempt was made to register a model more than once. + """ + pass + + +registry = [] + + +def register(model, tag_descriptor_attr='tags', + tagged_item_manager_attr='tagged'): + """ + Sets the given model class up for working with tags. + """ + + from tagging.managers import ModelTaggedItemManager, TagDescriptor + + if model in registry: + raise AlreadyRegistered("The model '%s' has already been " + "registered." % model._meta.object_name) + if hasattr(model, tag_descriptor_attr): + raise AttributeError("'%s' already has an attribute '%s'. You must " + "provide a custom tag_descriptor_attr to register." % ( + model._meta.object_name, + tag_descriptor_attr, + ) + ) + if hasattr(model, tagged_item_manager_attr): + raise AttributeError("'%s' already has an attribute '%s'. You must " + "provide a custom tagged_item_manager_attr to register." % ( + model._meta.object_name, + tagged_item_manager_attr, + ) + ) + + # Add tag descriptor + setattr(model, tag_descriptor_attr, TagDescriptor()) + + # Add custom manager + ModelTaggedItemManager().contribute_to_class(model, tagged_item_manager_attr) + + # Finally register in registry + registry.append(model) diff --git a/lib/tagging/admin.py b/lib/tagging/admin.py new file mode 100644 index 0000000..bec3922 --- /dev/null +++ b/lib/tagging/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from tagging.models import Tag, TaggedItem +from tagging.forms import TagAdminForm + +class TagAdmin(admin.ModelAdmin): + form = TagAdminForm + +admin.site.register(TaggedItem) +admin.site.register(Tag, TagAdmin) + + + + diff --git a/lib/tagging/fields.py b/lib/tagging/fields.py new file mode 100644 index 0000000..f471467 --- /dev/null +++ b/lib/tagging/fields.py @@ -0,0 +1,119 @@ +""" +A custom Model Field for tagging. +""" +from django.db.models import signals +from django.db.models.fields import CharField +from django.utils.translation import ugettext_lazy as _ + +from tagging import settings +from tagging.models import Tag +from tagging.utils import edit_string_for_tags + +class TagField(CharField): + """ + A "special" character field that actually works as a relationship to tags + "under the hood". This exposes a space-separated string of tags, but does + the splitting/reordering/etc. under the hood. + """ + def __init__(self, *args, **kwargs): + kwargs['max_length'] = kwargs.get('max_length', 255) + kwargs['blank'] = kwargs.get('blank', True) + kwargs['default'] = kwargs.get('default', '') + super(TagField, self).__init__(*args, **kwargs) + + def contribute_to_class(self, cls, name): + super(TagField, self).contribute_to_class(cls, name) + + # Make this object the descriptor for field access. + setattr(cls, self.name, self) + + # Save tags back to the database post-save + signals.post_save.connect(self._save, cls, True) + + # Update tags from Tag objects post-init + signals.post_init.connect(self._update, cls, True) + + def __get__(self, instance, owner=None): + """ + Tag getter. Returns an instance's tags if accessed on an instance, and + all of a model's tags if called on a class. That is, this model:: + + class Link(models.Model): + ... + tags = TagField() + + Lets you do both of these:: + + >>> l = Link.objects.get(...) + >>> l.tags + 'tag1 tag2 tag3' + + >>> Link.tags + 'tag1 tag2 tag3 tag4' + + """ + # Handle access on the model (i.e. Link.tags) + if instance is None: + return edit_string_for_tags(Tag.objects.usage_for_model(owner)) + + return self._get_instance_tag_cache(instance) + + def __set__(self, instance, value): + """ + Set an object's tags. + """ + if instance is None: + raise AttributeError(_('%s can only be set on instances.') % self.name) + if settings.FORCE_LOWERCASE_TAGS and value is not None: + value = value.lower() + self._set_instance_tag_cache(instance, value) + + def _save(self, **kwargs): #signal, sender, instance): + """ + Save tags back to the database + """ + tags = self._get_instance_tag_cache(kwargs['instance']) + Tag.objects.update_tags(kwargs['instance'], tags) + + def _update(self, **kwargs): #signal, sender, instance): + """ + Update tag cache from TaggedItem objects. + """ + instance = kwargs['instance'] + self._update_instance_tag_cache(instance) + + def __delete__(self, instance): + """ + Clear all of an object's tags. + """ + self._set_instance_tag_cache(instance, '') + + def _get_instance_tag_cache(self, instance): + """ + Helper: get an instance's tag cache. + """ + return getattr(instance, '_%s_cache' % self.attname, None) + + def _set_instance_tag_cache(self, instance, tags): + """ + Helper: set an instance's tag cache. + """ + setattr(instance, '_%s_cache' % self.attname, tags) + + def _update_instance_tag_cache(self, instance): + """ + Helper: update an instance's tag cache from actual Tags. + """ + # for an unsaved object, leave the default value alone + if instance.pk is not None: + tags = edit_string_for_tags(Tag.objects.get_for_object(instance)) + self._set_instance_tag_cache(instance, tags) + + def get_internal_type(self): + return 'CharField' + + def formfield(self, **kwargs): + from tagging import forms + defaults = {'form_class': forms.TagField} + defaults.update(kwargs) + return super(TagField, self).formfield(**defaults) diff --git a/lib/tagging/forms.py b/lib/tagging/forms.py new file mode 100644 index 0000000..a2d9fd9 --- /dev/null +++ b/lib/tagging/forms.py @@ -0,0 +1,40 @@ +""" +Tagging components for Django's form library. +""" +from django import forms +from django.utils.translation import ugettext as _ + +from tagging import settings +from tagging.models import Tag +from tagging.utils import parse_tag_input + +class TagAdminForm(forms.ModelForm): + class Meta: + model = Tag + + def clean_name(self): + value = self.cleaned_data['name'] + tag_names = parse_tag_input(value) + if len(tag_names) > 1: + raise forms.ValidationError(_('Multiple tags were given.')) + elif len(tag_names[0]) > settings.MAX_TAG_LENGTH: + raise forms.ValidationError( + _('A tag may be no more than %s characters long.') % + settings.MAX_TAG_LENGTH) + return value + +class TagField(forms.CharField): + """ + A ``CharField`` which validates that its input is a valid list of + tag names. + """ + def clean(self, value): + value = super(TagField, self).clean(value) + if value == u'': + return value + for tag_name in parse_tag_input(value): + if len(tag_name) > settings.MAX_TAG_LENGTH: + raise forms.ValidationError( + _('Each tag may be no more than %s characters long.') % + settings.MAX_TAG_LENGTH) + return value diff --git a/lib/tagging/generic.py b/lib/tagging/generic.py new file mode 100644 index 0000000..75d1b8e --- /dev/null +++ b/lib/tagging/generic.py @@ -0,0 +1,40 @@ +from django.contrib.contenttypes.models import ContentType + +def fetch_content_objects(tagged_items, select_related_for=None): + """ + Retrieves ``ContentType`` and content objects for the given list of + ``TaggedItems``, grouping the retrieval of content objects by model + type to reduce the number of queries executed. + + This results in ``number_of_content_types + 1`` queries rather than + the ``number_of_tagged_items * 2`` queries you'd get by iterating + over the list and accessing each item's ``object`` attribute. + + A ``select_related_for`` argument can be used to specify a list of + of model names (corresponding to the ``model`` field of a + ``ContentType``) for which ``select_related`` should be used when + retrieving model instances. + """ + if select_related_for is None: select_related_for = [] + + # Group content object pks by their content type pks + objects = {} + for item in tagged_items: + objects.setdefault(item.content_type_id, []).append(item.object_id) + + # Retrieve content types and content objects in bulk + content_types = ContentType._default_manager.in_bulk(objects.keys()) + for content_type_pk, object_pks in objects.iteritems(): + model = content_types[content_type_pk].model_class() + if content_types[content_type_pk].model in select_related_for: + objects[content_type_pk] = model._default_manager.select_related().in_bulk(object_pks) + else: + objects[content_type_pk] = model._default_manager.in_bulk(object_pks) + + # Set content types and content objects in the appropriate cache + # attributes, so accessing the 'content_type' and 'object' + # attributes on each tagged item won't result in further database + # hits. + for item in tagged_items: + item._object_cache = objects[item.content_type_id][item.object_id] + item._content_type_cache = content_types[item.content_type_id] diff --git a/lib/tagging/managers.py b/lib/tagging/managers.py new file mode 100644 index 0000000..02cd1c2 --- /dev/null +++ b/lib/tagging/managers.py @@ -0,0 +1,68 @@ +""" +Custom managers for Django models registered with the tagging +application. +""" +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from tagging.models import Tag, TaggedItem + +class ModelTagManager(models.Manager): + """ + A manager for retrieving tags for a particular model. + """ + def get_query_set(self): + ctype = ContentType.objects.get_for_model(self.model) + return Tag.objects.filter( + items__content_type__pk=ctype.pk).distinct() + + def cloud(self, *args, **kwargs): + return Tag.objects.cloud_for_model(self.model, *args, **kwargs) + + def related(self, tags, *args, **kwargs): + return Tag.objects.related_for_model(tags, self.model, *args, **kwargs) + + def usage(self, *args, **kwargs): + return Tag.objects.usage_for_model(self.model, *args, **kwargs) + +class ModelTaggedItemManager(models.Manager): + """ + A manager for retrieving model instances based on their tags. + """ + def related_to(self, obj, queryset=None, num=None): + if queryset is None: + return TaggedItem.objects.get_related(obj, self.model, num=num) + else: + return TaggedItem.objects.get_related(obj, queryset, num=num) + + def with_all(self, tags, queryset=None): + if queryset is None: + return TaggedItem.objects.get_by_model(self.model, tags) + else: + return TaggedItem.objects.get_by_model(queryset, tags) + + def with_any(self, tags, queryset=None): + if queryset is None: + return TaggedItem.objects.get_union_by_model(self.model, tags) + else: + return TaggedItem.objects.get_union_by_model(queryset, tags) + +class TagDescriptor(object): + """ + A descriptor which provides access to a ``ModelTagManager`` for + model classes and simple retrieval, updating and deletion of tags + for model instances. + """ + def __get__(self, instance, owner): + if not instance: + tag_manager = ModelTagManager() + tag_manager.model = owner + return tag_manager + else: + return Tag.objects.get_for_object(instance) + + def __set__(self, instance, value): + Tag.objects.update_tags(instance, value) + + def __delete__(self, instance): + Tag.objects.update_tags(instance, None) diff --git a/lib/tagging/models.py b/lib/tagging/models.py new file mode 100644 index 0000000..860cf81 --- /dev/null +++ b/lib/tagging/models.py @@ -0,0 +1,490 @@ +""" +Models and managers for generic tagging. +""" +# Python 2.3 compatibility +try: + set +except NameError: + from sets import Set as set + +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.db import connection, models +from django.db.models.query import QuerySet +from django.utils.translation import ugettext_lazy as _ + +from tagging import settings +from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input +from tagging.utils import LOGARITHMIC + +qn = connection.ops.quote_name + +############ +# Managers # +############ + +class TagManager(models.Manager): + def update_tags(self, obj, tag_names): + """ + Update tags associated with an object. + """ + ctype = ContentType.objects.get_for_model(obj) + current_tags = list(self.filter(items__content_type__pk=ctype.pk, + items__object_id=obj.pk)) + updated_tag_names = parse_tag_input(tag_names) + if settings.FORCE_LOWERCASE_TAGS: + updated_tag_names = [t.lower() for t in updated_tag_names] + + # Remove tags which no longer apply + tags_for_removal = [tag for tag in current_tags \ + if tag.name not in updated_tag_names] + if len(tags_for_removal): + TaggedItem._default_manager.filter(content_type__pk=ctype.pk, + object_id=obj.pk, + tag__in=tags_for_removal).delete() + # Add new tags + current_tag_names = [tag.name for tag in current_tags] + for tag_name in updated_tag_names: + if tag_name not in current_tag_names: + tag, created = self.get_or_create(name=tag_name) + TaggedItem._default_manager.create(tag=tag, object=obj) + + def add_tag(self, obj, tag_name): + """ + Associates the given object with a tag. + """ + tag_names = parse_tag_input(tag_name) + if not len(tag_names): + raise AttributeError(_('No tags were given: "%s".') % tag_name) + if len(tag_names) > 1: + raise AttributeError(_('Multiple tags were given: "%s".') % tag_name) + tag_name = tag_names[0] + if settings.FORCE_LOWERCASE_TAGS: + tag_name = tag_name.lower() + tag, created = self.get_or_create(name=tag_name) + ctype = ContentType.objects.get_for_model(obj) + TaggedItem._default_manager.get_or_create( + tag=tag, content_type=ctype, object_id=obj.pk) + + def get_for_object(self, obj): + """ + Create a queryset matching all tags associated with the given + object. + """ + ctype = ContentType.objects.get_for_model(obj) + return self.filter(items__content_type__pk=ctype.pk, + items__object_id=obj.pk) + + def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None): + """ + Perform the custom SQL query for ``usage_for_model`` and + ``usage_for_queryset``. + """ + if min_count is not None: counts = True + + model_table = qn(model._meta.db_table) + model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column)) + query = """ + SELECT DISTINCT %(tag)s.id, %(tag)s.name%(count_sql)s + FROM + %(tag)s + INNER JOIN %(tagged_item)s + ON %(tag)s.id = %(tagged_item)s.tag_id + INNER JOIN %(model)s + ON %(tagged_item)s.object_id = %(model_pk)s + %%s + WHERE %(tagged_item)s.content_type_id = %(content_type_id)s + %%s + GROUP BY %(tag)s.id, %(tag)s.name + %%s + ORDER BY %(tag)s.name ASC""" % { + 'tag': qn(self.model._meta.db_table), + 'count_sql': counts and (', COUNT(%s)' % model_pk) or '', + 'tagged_item': qn(TaggedItem._meta.db_table), + 'model': model_table, + 'model_pk': model_pk, + 'content_type_id': ContentType.objects.get_for_model(model).pk, + } + + min_count_sql = '' + if min_count is not None: + min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk + params.append(min_count) + + cursor = connection.cursor() + cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params) + tags = [] + for row in cursor.fetchall(): + t = self.model(*row[:2]) + if counts: + t.count = row[2] + tags.append(t) + return tags + + def usage_for_model(self, model, counts=False, min_count=None, filters=None): + """ + Obtain a list of tags associated with instances of the given + Model class. + + If ``counts`` is True, a ``count`` attribute will be added to + each tag, indicating how many times it has been used against + the Model class in question. + + If ``min_count`` is given, only tags which have a ``count`` + greater than or equal to ``min_count`` will be returned. + Passing a value for ``min_count`` implies ``counts=True``. + + To limit the tags (and counts, if specified) returned to those + used by a subset of the Model's instances, pass a dictionary + of field lookups to be applied to the given Model as the + ``filters`` argument. + """ + if filters is None: filters = {} + + queryset = model._default_manager.filter() + for f in filters.items(): + queryset.query.add_filter(f) + usage = self.usage_for_queryset(queryset, counts, min_count) + + return usage + + def usage_for_queryset(self, queryset, counts=False, min_count=None): + """ + Obtain a list of tags associated with instances of a model + contained in the given queryset. + + If ``counts`` is True, a ``count`` attribute will be added to + each tag, indicating how many times it has been used against + the Model class in question. + + If ``min_count`` is given, only tags which have a ``count`` + greater than or equal to ``min_count`` will be returned. + Passing a value for ``min_count`` implies ``counts=True``. + """ + + if getattr(queryset.query, 'get_compiler', None): + # Django 1.2+ + compiler = queryset.query.get_compiler(using='default') + extra_joins = ' '.join(compiler.get_from_clause()[0][1:]) + where, params = queryset.query.where.as_sql( + compiler.quote_name_unless_alias, compiler.connection + ) + else: + # Django pre-1.2 + extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:]) + where, params = queryset.query.where.as_sql() + + if where: + extra_criteria = 'AND %s' % where + else: + extra_criteria = '' + return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params) + + def related_for_model(self, tags, model, counts=False, min_count=None): + """ + Obtain a list of tags related to a given list of tags - that + is, other tags used by items which have all the given tags. + + If ``counts`` is True, a ``count`` attribute will be added to + each tag, indicating the number of items which have it in + addition to the given list of tags. + + If ``min_count`` is given, only tags which have a ``count`` + greater than or equal to ``min_count`` will be returned. + Passing a value for ``min_count`` implies ``counts=True``. + """ + if min_count is not None: counts = True + tags = get_tag_list(tags) + tag_count = len(tags) + tagged_item_table = qn(TaggedItem._meta.db_table) + query = """ + SELECT %(tag)s.id, %(tag)s.name%(count_sql)s + FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id + WHERE %(tagged_item)s.content_type_id = %(content_type_id)s + AND %(tagged_item)s.object_id IN + ( + SELECT %(tagged_item)s.object_id + FROM %(tagged_item)s, %(tag)s + WHERE %(tagged_item)s.content_type_id = %(content_type_id)s + AND %(tag)s.id = %(tagged_item)s.tag_id + AND %(tag)s.id IN (%(tag_id_placeholders)s) + GROUP BY %(tagged_item)s.object_id + HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s + ) + AND %(tag)s.id NOT IN (%(tag_id_placeholders)s) + GROUP BY %(tag)s.id, %(tag)s.name + %(min_count_sql)s + ORDER BY %(tag)s.name ASC""" % { + 'tag': qn(self.model._meta.db_table), + 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '', + 'tagged_item': tagged_item_table, + 'content_type_id': ContentType.objects.get_for_model(model).pk, + 'tag_id_placeholders': ','.join(['%s'] * tag_count), + 'tag_count': tag_count, + 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '', + } + + params = [tag.pk for tag in tags] * 2 + if min_count is not None: + params.append(min_count) + + cursor = connection.cursor() + cursor.execute(query, params) + related = [] + for row in cursor.fetchall(): + tag = self.model(*row[:2]) + if counts is True: + tag.count = row[2] + related.append(tag) + return related + + def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC, + filters=None, min_count=None): + """ + Obtain a list of tags associated with instances of the given + Model, giving each tag a ``count`` attribute indicating how + many times it has been used and a ``font_size`` attribute for + use in displaying a tag cloud. + + ``steps`` defines the range of font sizes - ``font_size`` will + be an integer between 1 and ``steps`` (inclusive). + + ``distribution`` defines the type of font size distribution + algorithm which will be used - logarithmic or linear. It must + be either ``tagging.utils.LOGARITHMIC`` or + ``tagging.utils.LINEAR``. + + To limit the tags displayed in the cloud to those associated + with a subset of the Model's instances, pass a dictionary of + field lookups to be applied to the given Model as the + ``filters`` argument. + + To limit the tags displayed in the cloud to those with a + ``count`` greater than or equal to ``min_count``, pass a value + for the ``min_count`` argument. + """ + tags = list(self.usage_for_model(model, counts=True, filters=filters, + min_count=min_count)) + return calculate_cloud(tags, steps, distribution) + +class TaggedItemManager(models.Manager): + """ + FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING`` + SQL clauses required by many of this manager's methods into + Django's ORM. + + For now, we manually execute a query to retrieve the PKs of + objects we're interested in, then use the ORM's ``__in`` + lookup to return a ``QuerySet``. + + Now that the queryset-refactor branch is in the trunk, this can be + tidied up significantly. + """ + def get_by_model(self, queryset_or_model, tags): + """ + Create a ``QuerySet`` containing instances of the specified + model associated with a given tag or list of tags. + """ + tags = get_tag_list(tags) + tag_count = len(tags) + if tag_count == 0: + # No existing tags were given + queryset, model = get_queryset_and_model(queryset_or_model) + return model._default_manager.none() + elif tag_count == 1: + # Optimisation for single tag - fall through to the simpler + # query below. + tag = tags[0] + else: + return self.get_intersection_by_model(queryset_or_model, tags) + + queryset, model = get_queryset_and_model(queryset_or_model) + content_type = ContentType.objects.get_for_model(model) + opts = self.model._meta + tagged_item_table = qn(opts.db_table) + return queryset.extra( + tables=[opts.db_table], + where=[ + '%s.content_type_id = %%s' % tagged_item_table, + '%s.tag_id = %%s' % tagged_item_table, + '%s.%s = %s.object_id' % (qn(model._meta.db_table), + qn(model._meta.pk.column), + tagged_item_table) + ], + params=[content_type.pk, tag.pk], + ) + + def get_intersection_by_model(self, queryset_or_model, tags): + """ + Create a ``QuerySet`` containing instances of the specified + model associated with *all* of the given list of tags. + """ + tags = get_tag_list(tags) + tag_count = len(tags) + queryset, model = get_queryset_and_model(queryset_or_model) + + if not tag_count: + return model._default_manager.none() + + model_table = qn(model._meta.db_table) + # This query selects the ids of all objects which have all the + # given tags. + query = """ + SELECT %(model_pk)s + FROM %(model)s, %(tagged_item)s + WHERE %(tagged_item)s.content_type_id = %(content_type_id)s + AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s) + AND %(model_pk)s = %(tagged_item)s.object_id + GROUP BY %(model_pk)s + HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % { + 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), + 'model': model_table, + 'tagged_item': qn(self.model._meta.db_table), + 'content_type_id': ContentType.objects.get_for_model(model).pk, + 'tag_id_placeholders': ','.join(['%s'] * tag_count), + 'tag_count': tag_count, + } + + cursor = connection.cursor() + cursor.execute(query, [tag.pk for tag in tags]) + object_ids = [row[0] for row in cursor.fetchall()] + if len(object_ids) > 0: + return queryset.filter(pk__in=object_ids) + else: + return model._default_manager.none() + + def get_union_by_model(self, queryset_or_model, tags): + """ + Create a ``QuerySet`` containing instances of the specified + model associated with *any* of the given list of tags. + """ + tags = get_tag_list(tags) + tag_count = len(tags) + queryset, model = get_queryset_and_model(queryset_or_model) + + if not tag_count: + return model._default_manager.none() + + model_table = qn(model._meta.db_table) + # This query selects the ids of all objects which have any of + # the given tags. + query = """ + SELECT %(model_pk)s + FROM %(model)s, %(tagged_item)s + WHERE %(tagged_item)s.content_type_id = %(content_type_id)s + AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s) + AND %(model_pk)s = %(tagged_item)s.object_id + GROUP BY %(model_pk)s""" % { + 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), + 'model': model_table, + 'tagged_item': qn(self.model._meta.db_table), + 'content_type_id': ContentType.objects.get_for_model(model).pk, + 'tag_id_placeholders': ','.join(['%s'] * tag_count), + } + + cursor = connection.cursor() + cursor.execute(query, [tag.pk for tag in tags]) + object_ids = [row[0] for row in cursor.fetchall()] + if len(object_ids) > 0: + return queryset.filter(pk__in=object_ids) + else: + return model._default_manager.none() + + def get_related(self, obj, queryset_or_model, num=None): + """ + Retrieve a list of instances of the specified model which share + tags with the model instance ``obj``, ordered by the number of + shared tags in descending order. + + If ``num`` is given, a maximum of ``num`` instances will be + returned. + """ + queryset, model = get_queryset_and_model(queryset_or_model) + model_table = qn(model._meta.db_table) + content_type = ContentType.objects.get_for_model(obj) + related_content_type = ContentType.objects.get_for_model(model) + query = """ + SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s + FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item + WHERE %(tagged_item)s.object_id = %%s + AND %(tagged_item)s.content_type_id = %(content_type_id)s + AND %(tag)s.id = %(tagged_item)s.tag_id + AND related_tagged_item.content_type_id = %(related_content_type_id)s + AND related_tagged_item.tag_id = %(tagged_item)s.tag_id + AND %(model_pk)s = related_tagged_item.object_id""" + if content_type.pk == related_content_type.pk: + # Exclude the given instance itself if determining related + # instances for the same model. + query += """ + AND related_tagged_item.object_id != %(tagged_item)s.object_id""" + query += """ + GROUP BY %(model_pk)s + ORDER BY %(count)s DESC + %(limit_offset)s""" + query = query % { + 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)), + 'count': qn('count'), + 'model': model_table, + 'tagged_item': qn(self.model._meta.db_table), + 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table), + 'content_type_id': content_type.pk, + 'related_content_type_id': related_content_type.pk, + # Hardcoding this for now just to get tests working again - this + # should now be handled by the query object. + 'limit_offset': num is not None and 'LIMIT %s' or '', + } + + cursor = connection.cursor() + params = [obj.pk] + if num is not None: + params.append(num) + cursor.execute(query, params) + object_ids = [row[0] for row in cursor.fetchall()] + if len(object_ids) > 0: + # Use in_bulk here instead of an id__in lookup, because id__in would + # clobber the ordering. + object_dict = queryset.in_bulk(object_ids) + return [object_dict[object_id] for object_id in object_ids \ + if object_id in object_dict] + else: + return [] + +########## +# Models # +########## + +class Tag(models.Model): + """ + A tag. + """ + name = models.CharField(_('name'), max_length=50, unique=True, db_index=True) + + objects = TagManager() + + class Meta: + ordering = ('name',) + verbose_name = _('tag') + verbose_name_plural = _('tags') + + def __unicode__(self): + return self.name + +class TaggedItem(models.Model): + """ + Holds the relationship between a tag and the item being tagged. + """ + tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items') + content_type = models.ForeignKey(ContentType, verbose_name=_('content type')) + object_id = models.PositiveIntegerField(_('object id'), db_index=True) + object = generic.GenericForeignKey('content_type', 'object_id') + + objects = TaggedItemManager() + + class Meta: + # Enforce unique tag association per object + unique_together = (('tag', 'content_type', 'object_id'),) + verbose_name = _('tagged item') + verbose_name_plural = _('tagged items') + + def __unicode__(self): + return u'%s [%s]' % (self.object, self.tag) diff --git a/lib/tagging/settings.py b/lib/tagging/settings.py new file mode 100644 index 0000000..1d6224c --- /dev/null +++ b/lib/tagging/settings.py @@ -0,0 +1,13 @@ +""" +Convenience module for access of custom tagging application settings, +which enforces default settings when the main settings module does not +contain the appropriate settings. +""" +from django.conf import settings + +# The maximum length of a tag's name. +MAX_TAG_LENGTH = getattr(settings, 'MAX_TAG_LENGTH', 50) + +# Whether to force all tags to lowercase before they are saved to the +# database. +FORCE_LOWERCASE_TAGS = getattr(settings, 'FORCE_LOWERCASE_TAGS', False) diff --git a/lib/tagging/templatetags/__init__.py b/lib/tagging/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/tagging/templatetags/__init__.py diff --git a/lib/tagging/templatetags/tagging_tags.py b/lib/tagging/templatetags/tagging_tags.py new file mode 100644 index 0000000..11d31cc --- /dev/null +++ b/lib/tagging/templatetags/tagging_tags.py @@ -0,0 +1,231 @@ +from django.db.models import get_model +from django.template import Library, Node, TemplateSyntaxError, Variable, resolve_variable +from django.utils.translation import ugettext as _ + +from tagging.models import Tag, TaggedItem +from tagging.utils import LINEAR, LOGARITHMIC + +register = Library() + +class TagsForModelNode(Node): + def __init__(self, model, context_var, counts): + self.model = model + self.context_var = context_var + self.counts = counts + + def render(self, context): + model = get_model(*self.model.split('.')) + if model is None: + raise TemplateSyntaxError(_('tags_for_model tag was given an invalid model: %s') % self.model) + context[self.context_var] = Tag.objects.usage_for_model(model, counts=self.counts) + return '' + +class TagCloudForModelNode(Node): + def __init__(self, model, context_var, **kwargs): + self.model = model + self.context_var = context_var + self.kwargs = kwargs + + def render(self, context): + model = get_model(*self.model.split('.')) + if model is None: + raise TemplateSyntaxError(_('tag_cloud_for_model tag was given an invalid model: %s') % self.model) + context[self.context_var] = \ + Tag.objects.cloud_for_model(model, **self.kwargs) + return '' + +class TagsForObjectNode(Node): + def __init__(self, obj, context_var): + self.obj = Variable(obj) + self.context_var = context_var + + def render(self, context): + context[self.context_var] = \ + Tag.objects.get_for_object(self.obj.resolve(context)) + return '' + +class TaggedObjectsNode(Node): + def __init__(self, tag, model, context_var): + self.tag = Variable(tag) + self.context_var = context_var + self.model = model + + def render(self, context): + model = get_model(*self.model.split('.')) + if model is None: + raise TemplateSyntaxError(_('tagged_objects tag was given an invalid model: %s') % self.model) + context[self.context_var] = \ + TaggedItem.objects.get_by_model(model, self.tag.resolve(context)) + return '' + +def do_tags_for_model(parser, token): + """ + Retrieves a list of ``Tag`` objects associated with a given model + and stores them in a context variable. + + Usage:: + + {% tags_for_model [model] as [varname] %} + + The model is specified in ``[appname].[modelname]`` format. + + Extended usage:: + + {% tags_for_model [model] as [varname] with counts %} + + If specified - by providing extra ``with counts`` arguments - adds + a ``count`` attribute to each tag containing the number of + instances of the given model which have been tagged with it. + + Examples:: + + {% tags_for_model products.Widget as widget_tags %} + {% tags_for_model products.Widget as widget_tags with counts %} + + """ + bits = token.contents.split() + len_bits = len(bits) + if len_bits not in (4, 6): + raise TemplateSyntaxError(_('%s tag requires either three or five arguments') % bits[0]) + if bits[2] != 'as': + raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + if len_bits == 6: + if bits[4] != 'with': + raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0]) + if bits[5] != 'counts': + raise TemplateSyntaxError(_("if given, fifth argument to %s tag must be 'counts'") % bits[0]) + if len_bits == 4: + return TagsForModelNode(bits[1], bits[3], counts=False) + else: + return TagsForModelNode(bits[1], bits[3], counts=True) + +def do_tag_cloud_for_model(parser, token): + """ + Retrieves a list of ``Tag`` objects for a given model, with tag + cloud attributes set, and stores them in a context variable. + + Usage:: + + {% tag_cloud_for_model [model] as [varname] %} + + The model is specified in ``[appname].[modelname]`` format. + + Extended usage:: + + {% tag_cloud_for_model [model] as [varname] with [options] %} + + Extra options can be provided after an optional ``with`` argument, + with each option being specified in ``[name]=[value]`` format. Valid + extra options are: + + ``steps`` + Integer. Defines the range of font sizes. + + ``min_count`` + Integer. Defines the minimum number of times a tag must have + been used to appear in the cloud. + + ``distribution`` + One of ``linear`` or ``log``. Defines the font-size + distribution algorithm to use when generating the tag cloud. + + Examples:: + + {% tag_cloud_for_model products.Widget as widget_tags %} + {% tag_cloud_for_model products.Widget as widget_tags with steps=9 min_count=3 distribution=log %} + + """ + bits = token.contents.split() + len_bits = len(bits) + if len_bits != 4 and len_bits not in range(6, 9): + raise TemplateSyntaxError(_('%s tag requires either three or between five and seven arguments') % bits[0]) + if bits[2] != 'as': + raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + kwargs = {} + if len_bits > 5: + if bits[4] != 'with': + raise TemplateSyntaxError(_("if given, fourth argument to %s tag must be 'with'") % bits[0]) + for i in range(5, len_bits): + try: + name, value = bits[i].split('=') + if name == 'steps' or name == 'min_count': + try: + kwargs[str(name)] = int(value) + except ValueError: + raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid integer: '%(value)s'") % { + 'tag': bits[0], + 'option': name, + 'value': value, + }) + elif name == 'distribution': + if value in ['linear', 'log']: + kwargs[str(name)] = {'linear': LINEAR, 'log': LOGARITHMIC}[value] + else: + raise TemplateSyntaxError(_("%(tag)s tag's '%(option)s' option was not a valid choice: '%(value)s'") % { + 'tag': bits[0], + 'option': name, + 'value': value, + }) + else: + raise TemplateSyntaxError(_("%(tag)s tag was given an invalid option: '%(option)s'") % { + 'tag': bits[0], + 'option': name, + }) + except ValueError: + raise TemplateSyntaxError(_("%(tag)s tag was given a badly formatted option: '%(option)s'") % { + 'tag': bits[0], + 'option': bits[i], + }) + return TagCloudForModelNode(bits[1], bits[3], **kwargs) + +def do_tags_for_object(parser, token): + """ + Retrieves a list of ``Tag`` objects associated with an object and + stores them in a context variable. + + Usage:: + + {% tags_for_object [object] as [varname] %} + + Example:: + + {% tags_for_object foo_object as tag_list %} + """ + bits = token.contents.split() + if len(bits) != 4: + raise TemplateSyntaxError(_('%s tag requires exactly three arguments') % bits[0]) + if bits[2] != 'as': + raise TemplateSyntaxError(_("second argument to %s tag must be 'as'") % bits[0]) + return TagsForObjectNode(bits[1], bits[3]) + +def do_tagged_objects(parser, token): + """ + Retrieves a list of instances of a given model which are tagged with + a given ``Tag`` and stores them in a context variable. + + Usage:: + + {% tagged_objects [tag] in [model] as [varname] %} + + The model is specified in ``[appname].[modelname]`` format. + + The tag must be an instance of a ``Tag``, not the name of a tag. + + Example:: + + {% tagged_objects comedy_tag in tv.Show as comedies %} + + """ + bits = token.contents.split() + if len(bits) != 6: + raise TemplateSyntaxError(_('%s tag requires exactly five arguments') % bits[0]) + if bits[2] != 'in': + raise TemplateSyntaxError(_("second argument to %s tag must be 'in'") % bits[0]) + if bits[4] != 'as': + raise TemplateSyntaxError(_("fourth argument to %s tag must be 'as'") % bits[0]) + return TaggedObjectsNode(bits[1], bits[3], bits[5]) + +register.tag('tags_for_model', do_tags_for_model) +register.tag('tag_cloud_for_model', do_tag_cloud_for_model) +register.tag('tags_for_object', do_tags_for_object) +register.tag('tagged_objects', do_tagged_objects) diff --git a/lib/tagging/tests/__init__.py b/lib/tagging/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/tagging/tests/__init__.py diff --git a/lib/tagging/tests/models.py b/lib/tagging/tests/models.py new file mode 100644 index 0000000..e3274ff --- /dev/null +++ b/lib/tagging/tests/models.py @@ -0,0 +1,42 @@ +from django.db import models + +from tagging.fields import TagField + +class Perch(models.Model): + size = models.IntegerField() + smelly = models.BooleanField(default=True) + +class Parrot(models.Model): + state = models.CharField(max_length=50) + perch = models.ForeignKey(Perch, null=True) + + def __unicode__(self): + return self.state + + class Meta: + ordering = ['state'] + +class Link(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + + class Meta: + ordering = ['name'] + +class Article(models.Model): + name = models.CharField(max_length=50) + + def __unicode__(self): + return self.name + + class Meta: + ordering = ['name'] + +class FormTest(models.Model): + tags = TagField('Test', help_text='Test') + +class FormTestNull(models.Model): + tags = TagField(null=True) + diff --git a/lib/tagging/tests/settings.py b/lib/tagging/tests/settings.py new file mode 100644 index 0000000..74eb909 --- /dev/null +++ b/lib/tagging/tests/settings.py @@ -0,0 +1,27 @@ +import os +DIRNAME = os.path.dirname(__file__) + +DEFAULT_CHARSET = 'utf-8' + +test_engine = os.environ.get("TAGGING_TEST_ENGINE", "sqlite3") + +DATABASE_ENGINE = test_engine +DATABASE_NAME = os.environ.get("TAGGING_DATABASE_NAME", "tagging_test") +DATABASE_USER = os.environ.get("TAGGING_DATABASE_USER", "") +DATABASE_PASSWORD = os.environ.get("TAGGING_DATABASE_PASSWORD", "") +DATABASE_HOST = os.environ.get("TAGGING_DATABASE_HOST", "localhost") + +if test_engine == "sqlite": + DATABASE_NAME = os.path.join(DIRNAME, 'tagging_test.db') + DATABASE_HOST = "" +elif test_engine == "mysql": + DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 3306) +elif test_engine == "postgresql_psycopg2": + DATABASE_PORT = os.environ.get("TAGGING_DATABASE_PORT", 5432) + + +INSTALLED_APPS = ( + 'django.contrib.contenttypes', + 'tagging', + 'tagging.tests', +) diff --git a/lib/tagging/tests/tags.txt b/lib/tagging/tests/tags.txt new file mode 100644 index 0000000..8543411 --- /dev/null +++ b/lib/tagging/tests/tags.txt @@ -0,0 +1,122 @@ +NewMedia 53 +Website 45 +PR 44 +Status 44 +Collaboration 41 +Drupal 34 +Journalism 31 +Transparency 30 +Theory 29 +Decentralization 25 +EchoChamberProject 24 +OpenSource 23 +Film 22 +Blog 21 +Interview 21 +Political 21 +Worldview 21 +Communications 19 +Conference 19 +Folksonomy 15 +MediaCriticism 15 +Volunteer 15 +Dialogue 13 +InternationalLaw 13 +Rosen 12 +Evolution 11 +KentBye 11 +Objectivity 11 +Plante 11 +ToDo 11 +Advisor 10 +Civics 10 +Roadmap 10 +Wilber 9 +About 8 +CivicSpace 8 +Ecosystem 8 +Choice 7 +Murphy 7 +Sociology 7 +ACH 6 +del.icio.us 6 +IntelligenceAnalysis 6 +Science 6 +Credibility 5 +Distribution 5 +Diversity 5 +Errors 5 +FinalCutPro 5 +Fundraising 5 +Law 5 +PhilosophyofScience 5 +Podcast 5 +PoliticalBias 5 +Activism 4 +Analysis 4 +CBS 4 +DeceptionDetection 4 +Editing 4 +History 4 +RSS 4 +Social 4 +Subjectivity 4 +Vlog 4 +ABC 3 +ALTubes 3 +Economics 3 +FCC 3 +NYT 3 +Sirota 3 +Sundance 3 +Training 3 +Wiki 3 +XML 3 +Borger 2 +Brody 2 +Deliberation 2 +EcoVillage 2 +Identity 2 +LAMP 2 +Lobe 2 +Maine 2 +May 2 +MediaLogic 2 +Metaphor 2 +Mitchell 2 +NBC 2 +OHanlon 2 +Psychology 2 +Queen 2 +Software 2 +SpiralDynamics 2 +Strobel 2 +Sustainability 2 +Transcripts 2 +Brown 1 +Buddhism 1 +Community 1 +DigitalDivide 1 +Donnelly 1 +Education 1 +FairUse 1 +FireANT 1 +Google 1 +HumanRights 1 +KM 1 +Kwiatkowski 1 +Landay 1 +Loiseau 1 +Math 1 +Music 1 +Nature 1 +Schechter 1 +Screencast 1 +Sivaraksa 1 +Skype 1 +SocialCapital 1 +TagCloud 1 +Thielmann 1 +Thomas 1 +Tiger 1 +Wedgwood 1
\ No newline at end of file diff --git a/lib/tagging/tests/tests.py b/lib/tagging/tests/tests.py new file mode 100644 index 0000000..1852444 --- /dev/null +++ b/lib/tagging/tests/tests.py @@ -0,0 +1,920 @@ +# -*- coding: utf-8 -*- + +import os +from django import forms +from django.db.models import Q +from django.test import TestCase +from tagging.forms import TagField +from tagging import settings +from tagging.models import Tag, TaggedItem +from tagging.tests.models import Article, Link, Perch, Parrot, FormTest, FormTestNull +from tagging.utils import calculate_cloud, edit_string_for_tags, get_tag_list, get_tag, parse_tag_input +from tagging.utils import LINEAR + +############# +# Utilities # +############# + +class TestParseTagInput(TestCase): + def test_with_simple_space_delimited_tags(self): + """ Test with simple space-delimited tags. """ + + self.assertEquals(parse_tag_input('one'), [u'one']) + self.assertEquals(parse_tag_input('one two'), [u'one', u'two']) + self.assertEquals(parse_tag_input('one two three'), [u'one', u'three', u'two']) + self.assertEquals(parse_tag_input('one one two two'), [u'one', u'two']) + + def test_with_comma_delimited_multiple_words(self): + """ Test with comma-delimited multiple words. + An unquoted comma in the input will trigger this. """ + + self.assertEquals(parse_tag_input(',one'), [u'one']) + self.assertEquals(parse_tag_input(',one two'), [u'one two']) + self.assertEquals(parse_tag_input(',one two three'), [u'one two three']) + self.assertEquals(parse_tag_input('a-one, a-two and a-three'), + [u'a-one', u'a-two and a-three']) + + def test_with_double_quoted_multiple_words(self): + """ Test with double-quoted multiple words. + A completed quote will trigger this. Unclosed quotes are ignored. """ + + self.assertEquals(parse_tag_input('"one'), [u'one']) + self.assertEquals(parse_tag_input('"one two'), [u'one', u'two']) + self.assertEquals(parse_tag_input('"one two three'), [u'one', u'three', u'two']) + self.assertEquals(parse_tag_input('"one two"'), [u'one two']) + self.assertEquals(parse_tag_input('a-one "a-two and a-three"'), + [u'a-one', u'a-two and a-three']) + + def test_with_no_loose_commas(self): + """ Test with no loose commas -- split on spaces. """ + self.assertEquals(parse_tag_input('one two "thr,ee"'), [u'one', u'thr,ee', u'two']) + + def test_with_loose_commas(self): + """ Loose commas - split on commas """ + self.assertEquals(parse_tag_input('"one", two three'), [u'one', u'two three']) + + def test_tags_with_double_quotes_can_contain_commas(self): + """ Double quotes can contain commas """ + self.assertEquals(parse_tag_input('a-one "a-two, and a-three"'), + [u'a-one', u'a-two, and a-three']) + self.assertEquals(parse_tag_input('"two", one, one, two, "one"'), + [u'one', u'two']) + + def test_with_naughty_input(self): + """ Test with naughty input. """ + + # Bad users! Naughty users! + self.assertEquals(parse_tag_input(None), []) + self.assertEquals(parse_tag_input(''), []) + self.assertEquals(parse_tag_input('"'), []) + self.assertEquals(parse_tag_input('""'), []) + self.assertEquals(parse_tag_input('"' * 7), []) + self.assertEquals(parse_tag_input(',,,,,,'), []) + self.assertEquals(parse_tag_input('",",",",",",","'), [u',']) + self.assertEquals(parse_tag_input('a-one "a-two" and "a-three'), + [u'a-one', u'a-three', u'a-two', u'and']) + +class TestNormalisedTagListInput(TestCase): + def setUp(self): + self.cheese = Tag.objects.create(name='cheese') + self.toast = Tag.objects.create(name='toast') + + def test_single_tag_object_as_input(self): + self.assertEquals(get_tag_list(self.cheese), [self.cheese]) + + def test_space_delimeted_string_as_input(self): + ret = get_tag_list('cheese toast') + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_comma_delimeted_string_as_input(self): + ret = get_tag_list('cheese,toast') + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_empty_list(self): + self.assertEquals(get_tag_list([]), []) + + def test_list_of_two_strings(self): + ret = get_tag_list(['cheese', 'toast']) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_tag_primary_keys(self): + ret = get_tag_list([self.cheese.id, self.toast.id]) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_strings_with_strange_nontag_string(self): + ret = get_tag_list(['cheese', 'toast', 'ŠĐĆŽćžšđ']) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_list_of_tag_instances(self): + ret = get_tag_list([self.cheese, self.toast]) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_tuple_of_instances(self): + ret = get_tag_list((self.cheese, self.toast)) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_tag_filter(self): + ret = get_tag_list(Tag.objects.filter(name__in=['cheese', 'toast'])) + self.assertEquals(len(ret), 2) + self.failUnless(self.cheese in ret) + self.failUnless(self.toast in ret) + + def test_with_invalid_input_mix_of_string_and_instance(self): + try: + get_tag_list(['cheese', self.toast]) + except ValueError, ve: + self.assertEquals(str(ve), + 'If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + + def test_with_invalid_input(self): + try: + get_tag_list(29) + except ValueError, ve: + self.assertEquals(str(ve), 'The tag input given was invalid.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + + def test_with_tag_instance(self): + self.assertEquals(get_tag(self.cheese), self.cheese) + + def test_with_string(self): + self.assertEquals(get_tag('cheese'), self.cheese) + + def test_with_primary_key(self): + self.assertEquals(get_tag(self.cheese.id), self.cheese) + + def test_nonexistent_tag(self): + self.assertEquals(get_tag('mouse'), None) + +class TestCalculateCloud(TestCase): + def setUp(self): + self.tags = [] + for line in open(os.path.join(os.path.dirname(__file__), 'tags.txt')).readlines(): + name, count = line.rstrip().split() + tag = Tag(name=name) + tag.count = int(count) + self.tags.append(tag) + + def test_default_distribution(self): + sizes = {} + for tag in calculate_cloud(self.tags, steps=5): + sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1 + + # This isn't a pre-calculated test, just making sure it's consistent + self.assertEquals(sizes[1], 48) + self.assertEquals(sizes[2], 30) + self.assertEquals(sizes[3], 19) + self.assertEquals(sizes[4], 15) + self.assertEquals(sizes[5], 10) + + def test_linear_distribution(self): + sizes = {} + for tag in calculate_cloud(self.tags, steps=5, distribution=LINEAR): + sizes[tag.font_size] = sizes.get(tag.font_size, 0) + 1 + + # This isn't a pre-calculated test, just making sure it's consistent + self.assertEquals(sizes[1], 97) + self.assertEquals(sizes[2], 12) + self.assertEquals(sizes[3], 7) + self.assertEquals(sizes[4], 2) + self.assertEquals(sizes[5], 4) + + def test_invalid_distribution(self): + try: + calculate_cloud(self.tags, steps=5, distribution='cheese') + except ValueError, ve: + self.assertEquals(str(ve), 'Invalid distribution algorithm specified: cheese.') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('a ValueError exception was supposed to be raised!') + +########### +# Tagging # +########### + +class TestBasicTagging(TestCase): + def setUp(self): + self.dead_parrot = Parrot.objects.create(state='dead') + + def test_update_tags(self): + Tag.objects.update_tags(self.dead_parrot, 'foo,bar,"ter"') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('foo') in tags) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('ter') in tags) + + Tag.objects.update_tags(self.dead_parrot, '"foo" bar "baz"') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + def test_add_tag(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + # try to add a tag that already exists + Tag.objects.add_tag(self.dead_parrot, 'foo') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + # now add a tag that doesn't already exist + Tag.objects.add_tag(self.dead_parrot, 'zip') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 4) + self.failUnless(get_tag('zip') in tags) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + def test_add_tag_invalid_input_no_tags_specified(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + try: + Tag.objects.add_tag(self.dead_parrot, ' ') + except AttributeError, ae: + self.assertEquals(str(ae), 'No tags were given: " ".') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('an AttributeError exception was supposed to be raised!') + + def test_add_tag_invalid_input_multiple_tags_specified(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + try: + Tag.objects.add_tag(self.dead_parrot, 'one two') + except AttributeError, ae: + self.assertEquals(str(ae), 'Multiple tags were given: "one two".') + except Exception, e: + raise self.failureException('the wrong type of exception was raised: type [%s] value [%s]' %\ + (str(type(e)), str(e))) + else: + raise self.failureException('an AttributeError exception was supposed to be raised!') + + def test_update_tags_exotic_characters(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, u'ŠĐĆŽćžšđ') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0].name, u'ŠĐĆŽćžšđ') + + Tag.objects.update_tags(self.dead_parrot, u'你好') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0].name, u'你好') + + def test_update_tags_with_none(self): + # start off in a known, mildly interesting state + Tag.objects.update_tags(self.dead_parrot, 'foo bar baz') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(get_tag('bar') in tags) + self.failUnless(get_tag('baz') in tags) + self.failUnless(get_tag('foo') in tags) + + Tag.objects.update_tags(self.dead_parrot, None) + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 0) + +class TestModelTagField(TestCase): + """ Test the 'tags' field on models. """ + + def test_create_with_tags_specified(self): + f1 = FormTest.objects.create(tags=u'test3 test2 test1') + tags = Tag.objects.get_for_object(f1) + test1_tag = get_tag('test1') + test2_tag = get_tag('test2') + test3_tag = get_tag('test3') + self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEquals(len(tags), 3) + self.failUnless(test1_tag in tags) + self.failUnless(test2_tag in tags) + self.failUnless(test3_tag in tags) + + def test_update_via_tags_field(self): + f1 = FormTest.objects.create(tags=u'test3 test2 test1') + tags = Tag.objects.get_for_object(f1) + test1_tag = get_tag('test1') + test2_tag = get_tag('test2') + test3_tag = get_tag('test3') + self.failUnless(None not in (test1_tag, test2_tag, test3_tag)) + self.assertEquals(len(tags), 3) + self.failUnless(test1_tag in tags) + self.failUnless(test2_tag in tags) + self.failUnless(test3_tag in tags) + + f1.tags = u'test4' + f1.save() + tags = Tag.objects.get_for_object(f1) + test4_tag = get_tag('test4') + self.assertEquals(len(tags), 1) + self.assertEquals(tags[0], test4_tag) + + f1.tags = '' + f1.save() + tags = Tag.objects.get_for_object(f1) + self.assertEquals(len(tags), 0) + + def test_update_via_tags(self): + f1 = FormTest.objects.create(tags=u'one two three') + Tag.objects.get(name='three').delete() + t2 = Tag.objects.get(name='two') + t2.name = 'new' + t2.save() + f1again = FormTest.objects.get(pk=f1.pk) + self.failIf('three' in f1again.tags) + self.failIf('two' in f1again.tags) + self.failUnless('new' in f1again.tags) + + def test_creation_without_specifying_tags(self): + f1 = FormTest() + self.assertEquals(f1.tags, '') + + def test_creation_with_nullable_tags_field(self): + f1 = FormTestNull() + self.assertEquals(f1.tags, '') + +class TestSettings(TestCase): + def setUp(self): + self.original_force_lower_case_tags = settings.FORCE_LOWERCASE_TAGS + self.dead_parrot = Parrot.objects.create(state='dead') + + def tearDown(self): + settings.FORCE_LOWERCASE_TAGS = self.original_force_lower_case_tags + + def test_force_lowercase_tags(self): + """ Test forcing tags to lowercase. """ + + settings.FORCE_LOWERCASE_TAGS = True + + Tag.objects.update_tags(self.dead_parrot, 'foO bAr Ter') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + foo_tag = get_tag('foo') + bar_tag = get_tag('bar') + ter_tag = get_tag('ter') + self.failUnless(foo_tag in tags) + self.failUnless(bar_tag in tags) + self.failUnless(ter_tag in tags) + + Tag.objects.update_tags(self.dead_parrot, 'foO bAr baZ') + tags = Tag.objects.get_for_object(self.dead_parrot) + baz_tag = get_tag('baz') + self.assertEquals(len(tags), 3) + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + + Tag.objects.add_tag(self.dead_parrot, 'FOO') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 3) + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + + Tag.objects.add_tag(self.dead_parrot, 'Zip') + tags = Tag.objects.get_for_object(self.dead_parrot) + self.assertEquals(len(tags), 4) + zip_tag = get_tag('zip') + self.failUnless(bar_tag in tags) + self.failUnless(baz_tag in tags) + self.failUnless(foo_tag in tags) + self.failUnless(zip_tag in tags) + + f1 = FormTest.objects.create() + f1.tags = u'TEST5' + f1.save() + tags = Tag.objects.get_for_object(f1) + test5_tag = get_tag('test5') + self.assertEquals(len(tags), 1) + self.failUnless(test5_tag in tags) + self.assertEquals(f1.tags, u'test5') + +class TestTagUsageForModelBaseCase(TestCase): + def test_tag_usage_for_model_empty(self): + self.assertEquals(Tag.objects.usage_for_model(Parrot), []) + +class TestTagUsageForModel(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + def test_tag_usage_for_model(self): + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 3) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 3) in relevant_attribute_list) + + def test_tag_usage_for_model_with_min_count(self): + tag_usage = Tag.objects.usage_for_model(Parrot, min_count = 2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 3) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 3) in relevant_attribute_list) + + def test_tag_usage_with_filter_on_model_objects(self): + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state='no more')) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(state__startswith='p')) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, counts=True, filters=dict(perch__smelly=True)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, min_count=2, filters=dict(perch__smelly=True)) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'baz', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_model(Parrot, filters=dict(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 0) + +class TestTagsRelatedForModel(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + def test_related_for_model_with_tag_query_sets_as_input(self): + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar']), Parrot, counts=False) + relevant_attribute_list = [tag.name for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless(u'baz' in relevant_attribute_list) + self.failUnless(u'foo' in relevant_attribute_list) + self.failUnless(u'ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(Tag.objects.filter(name__in=['bar', 'ter', 'baz']), Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 0) + + def test_related_for_model_with_tag_strings_as_input(self): + # Once again, with feeling (strings) + related_tags = Tag.objects.related_for_model('bar', Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model('bar', Parrot, min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model('bar', Parrot, counts=False) + relevant_attribute_list = [tag.name for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless(u'baz' in relevant_attribute_list) + self.failUnless(u'foo' in relevant_attribute_list) + self.failUnless(u'ter' in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(['bar', 'ter'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'baz', 1) in relevant_attribute_list) + + related_tags = Tag.objects.related_for_model(['bar', 'ter', 'baz'], Parrot, counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in related_tags] + self.assertEquals(len(relevant_attribute_list), 0) + +class TestGetTaggedObjectsByModel(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + self.foo = Tag.objects.get(name='foo') + self.bar = Tag.objects.get(name='bar') + self.baz = Tag.objects.get(name='baz') + self.ter = Tag.objects.get(name='ter') + + self.pining_for_the_fjords_parrot = Parrot.objects.get(state='pining for the fjords') + self.passed_on_parrot = Parrot.objects.get(state='passed on') + self.no_more_parrot = Parrot.objects.get(state='no more') + self.late_parrot = Parrot.objects.get(state='late') + + def test_get_by_model_simple(self): + parrots = TaggedItem.objects.get_by_model(Parrot, self.foo) + self.assertEquals(len(parrots), 2) + self.failUnless(self.no_more_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, self.bar) + self.assertEquals(len(parrots), 3) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + def test_get_by_model_intersection(self): + parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.baz]) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, [self.foo, self.bar]) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, [self.bar, self.ter]) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + # Issue 114 - Intersection with non-existant tags + parrots = TaggedItem.objects.get_intersection_by_model(Parrot, []) + self.assertEquals(len(parrots), 0) + + def test_get_by_model_with_tag_querysets_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'baz'])) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['foo', 'bar'])) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, Tag.objects.filter(name__in=['bar', 'ter'])) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_model_with_strings_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, 'foo baz') + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, 'foo bar') + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, 'bar ter') + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_model_with_lists_of_strings_as_input(self): + parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'baz']) + self.assertEquals(len(parrots), 0) + + parrots = TaggedItem.objects.get_by_model(Parrot, ['foo', 'bar']) + self.assertEquals(len(parrots), 1) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_by_model(Parrot, ['bar', 'ter']) + self.assertEquals(len(parrots), 2) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + + def test_get_by_nonexistent_tag(self): + # Issue 50 - Get by non-existent tag + parrots = TaggedItem.objects.get_by_model(Parrot, 'argatrons') + self.assertEquals(len(parrots), 0) + + def test_get_union_by_model(self): + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['foo', 'ter']) + self.assertEquals(len(parrots), 4) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.no_more_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + parrots = TaggedItem.objects.get_union_by_model(Parrot, ['bar', 'baz']) + self.assertEquals(len(parrots), 3) + self.failUnless(self.late_parrot in parrots) + self.failUnless(self.passed_on_parrot in parrots) + self.failUnless(self.pining_for_the_fjords_parrot in parrots) + + # Issue 114 - Union with non-existant tags + parrots = TaggedItem.objects.get_union_by_model(Parrot, []) + self.assertEquals(len(parrots), 0) + +class TestGetRelatedTaggedItems(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + self.l1 = Link.objects.create(name='link 1') + Tag.objects.update_tags(self.l1, 'tag1 tag2 tag3 tag4 tag5') + self.l2 = Link.objects.create(name='link 2') + Tag.objects.update_tags(self.l2, 'tag1 tag2 tag3') + self.l3 = Link.objects.create(name='link 3') + Tag.objects.update_tags(self.l3, 'tag1') + self.l4 = Link.objects.create(name='link 4') + + self.a1 = Article.objects.create(name='article 1') + Tag.objects.update_tags(self.a1, 'tag1 tag2 tag3 tag4') + + def test_get_related_objects_of_same_model(self): + related_objects = TaggedItem.objects.get_related(self.l1, Link) + self.assertEquals(len(related_objects), 2) + self.failUnless(self.l2 in related_objects) + self.failUnless(self.l3 in related_objects) + + related_objects = TaggedItem.objects.get_related(self.l4, Link) + self.assertEquals(len(related_objects), 0) + + def test_get_related_objects_of_same_model_limited_number_of_results(self): + # This fails on Oracle because it has no support for a 'LIMIT' clause. + # See http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:127412348064 + + # ask for no more than 1 result + related_objects = TaggedItem.objects.get_related(self.l1, Link, num=1) + self.assertEquals(len(related_objects), 1) + self.failUnless(self.l2 in related_objects) + + def test_get_related_objects_of_same_model_limit_related_items(self): + related_objects = TaggedItem.objects.get_related(self.l1, Link.objects.exclude(name='link 3')) + self.assertEquals(len(related_objects), 1) + self.failUnless(self.l2 in related_objects) + + def test_get_related_objects_of_different_model(self): + related_objects = TaggedItem.objects.get_related(self.a1, Link) + self.assertEquals(len(related_objects), 3) + self.failUnless(self.l1 in related_objects) + self.failUnless(self.l2 in related_objects) + self.failUnless(self.l3 in related_objects) + + Tag.objects.update_tags(self.a1, 'tag6') + related_objects = TaggedItem.objects.get_related(self.a1, Link) + self.assertEquals(len(related_objects), 0) + +class TestTagUsageForQuerySet(TestCase): + def setUp(self): + parrot_details = ( + ('pining for the fjords', 9, True, 'foo bar'), + ('passed on', 6, False, 'bar baz ter'), + ('no more', 4, True, 'foo ter'), + ('late', 2, False, 'bar ter'), + ) + + for state, perch_size, perch_smelly, tags in parrot_details: + perch = Perch.objects.create(size=perch_size, smelly=perch_smelly) + parrot = Parrot.objects.create(state=state, perch=perch) + Tag.objects.update_tags(parrot, tags) + + def test_tag_usage_for_queryset(self): + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state='no more'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(state__startswith='p'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'baz', 1) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__smelly=True), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'foo', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=4)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 4) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'baz', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(perch__size__gt=99)) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 0) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l')), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'bar', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.filter(Q(perch__size__gt=6) | Q(state__startswith='l'))) + relevant_attribute_list = [(tag.name, hasattr(tag, 'counts')) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', False) in relevant_attribute_list) + self.failUnless((u'foo', False) in relevant_attribute_list) + self.failUnless((u'ter', False) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state='passed on'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 3) + self.failUnless((u'bar', 2) in relevant_attribute_list) + self.failUnless((u'foo', 2) in relevant_attribute_list) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(state__startswith='p'), min_count=2) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 1) + self.failUnless((u'ter', 2) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(Q(perch__size__gt=6) | Q(perch__smelly=False)), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'foo', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + + tag_usage = Tag.objects.usage_for_queryset(Parrot.objects.exclude(perch__smelly=True).filter(state__startswith='l'), counts=True) + relevant_attribute_list = [(tag.name, tag.count) for tag in tag_usage] + self.assertEquals(len(relevant_attribute_list), 2) + self.failUnless((u'bar', 1) in relevant_attribute_list) + self.failUnless((u'ter', 1) in relevant_attribute_list) + +################ +# Model Fields # +################ + +class TestTagFieldInForms(TestCase): + def test_tag_field_in_modelform(self): + # Ensure that automatically created forms use TagField + class TestForm(forms.ModelForm): + class Meta: + model = FormTest + + form = TestForm() + self.assertEquals(form.fields['tags'].__class__.__name__, 'TagField') + + def test_recreation_of_tag_list_string_representations(self): + plain = Tag.objects.create(name='plain') + spaces = Tag.objects.create(name='spa ces') + comma = Tag.objects.create(name='com,ma') + self.assertEquals(edit_string_for_tags([plain]), u'plain') + self.assertEquals(edit_string_for_tags([plain, spaces]), u'plain, spa ces') + self.assertEquals(edit_string_for_tags([plain, spaces, comma]), u'plain, spa ces, "com,ma"') + self.assertEquals(edit_string_for_tags([plain, comma]), u'plain "com,ma"') + self.assertEquals(edit_string_for_tags([comma, spaces]), u'"com,ma", spa ces') + + def test_tag_d_validation(self): + t = TagField() + self.assertEquals(t.clean('foo'), u'foo') + self.assertEquals(t.clean('foo bar baz'), u'foo bar baz') + self.assertEquals(t.clean('foo,bar,baz'), u'foo,bar,baz') + self.assertEquals(t.clean('foo, bar, baz'), u'foo, bar, baz') + self.assertEquals(t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar'), + u'foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvb bar') + try: + t.clean('foo qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbn bar') + except forms.ValidationError, ve: + self.assertEquals(str(ve), "[u'Each tag may be no more than 50 characters long.']") + except Exception, e: + raise e + else: + raise self.failureException('a ValidationError exception was supposed to have been raised.') diff --git a/lib/tagging/utils.py b/lib/tagging/utils.py new file mode 100644 index 0000000..e89bab0 --- /dev/null +++ b/lib/tagging/utils.py @@ -0,0 +1,263 @@ +""" +Tagging utilities - from user tag input parsing to tag cloud +calculation. +""" +import math +import types + +from django.db.models.query import QuerySet +from django.utils.encoding import force_unicode +from django.utils.translation import ugettext as _ + +# Python 2.3 compatibility +try: + set +except NameError: + from sets import Set as set + +def parse_tag_input(input): + """ + Parses tag input, with multiple word input being activated and + delineated by commas and double quotes. Quotes take precedence, so + they may contain commas. + + Returns a sorted list of unique tag names. + """ + if not input: + return [] + + input = force_unicode(input) + + # Special case - if there are no commas or double quotes in the + # input, we don't *do* a recall... I mean, we know we only need to + # split on spaces. + if u',' not in input and u'"' not in input: + words = list(set(split_strip(input, u' '))) + words.sort() + return words + + words = [] + buffer = [] + # Defer splitting of non-quoted sections until we know if there are + # any unquoted commas. + to_be_split = [] + saw_loose_comma = False + open_quote = False + i = iter(input) + try: + while 1: + c = i.next() + if c == u'"': + if buffer: + to_be_split.append(u''.join(buffer)) + buffer = [] + # Find the matching quote + open_quote = True + c = i.next() + while c != u'"': + buffer.append(c) + c = i.next() + if buffer: + word = u''.join(buffer).strip() + if word: + words.append(word) + buffer = [] + open_quote = False + else: + if not saw_loose_comma and c == u',': + saw_loose_comma = True + buffer.append(c) + except StopIteration: + # If we were parsing an open quote which was never closed treat + # the buffer as unquoted. + if buffer: + if open_quote and u',' in buffer: + saw_loose_comma = True + to_be_split.append(u''.join(buffer)) + if to_be_split: + if saw_loose_comma: + delimiter = u',' + else: + delimiter = u' ' + for chunk in to_be_split: + words.extend(split_strip(chunk, delimiter)) + words = list(set(words)) + words.sort() + return words + +def split_strip(input, delimiter=u','): + """ + Splits ``input`` on ``delimiter``, stripping each resulting string + and returning a list of non-empty strings. + """ + if not input: + return [] + + words = [w.strip() for w in input.split(delimiter)] + return [w for w in words if w] + +def edit_string_for_tags(tags): + """ + Given list of ``Tag`` instances, creates a string representation of + the list suitable for editing by the user, such that submitting the + given string representation back without changing it will give the + same list of tags. + + Tag names which contain commas will be double quoted. + + If any tag name which isn't being quoted contains whitespace, the + resulting string of tag names will be comma-delimited, otherwise + it will be space-delimited. + """ + names = [] + use_commas = False + for tag in tags: + name = tag.name + if u',' in name: + names.append('"%s"' % name) + continue + elif u' ' in name: + if not use_commas: + use_commas = True + names.append(name) + if use_commas: + glue = u', ' + else: + glue = u' ' + return glue.join(names) + +def get_queryset_and_model(queryset_or_model): + """ + Given a ``QuerySet`` or a ``Model``, returns a two-tuple of + (queryset, model). + + If a ``Model`` is given, the ``QuerySet`` returned will be created + using its default manager. + """ + try: + return queryset_or_model, queryset_or_model.model + except AttributeError: + return queryset_or_model._default_manager.all(), queryset_or_model + +def get_tag_list(tags): + """ + Utility function for accepting tag input in a flexible manner. + + If a ``Tag`` object is given, it will be returned in a list as + its single occupant. + + If given, the tag names in the following will be used to create a + ``Tag`` ``QuerySet``: + + * A string, which may contain multiple tag names. + * A list or tuple of strings corresponding to tag names. + * A list or tuple of integers corresponding to tag ids. + + If given, the following will be returned as-is: + + * A list or tuple of ``Tag`` objects. + * A ``Tag`` ``QuerySet``. + + """ + from tagging.models import Tag + if isinstance(tags, Tag): + return [tags] + elif isinstance(tags, QuerySet) and tags.model is Tag: + return tags + elif isinstance(tags, types.StringTypes): + return Tag.objects.filter(name__in=parse_tag_input(tags)) + elif isinstance(tags, (types.ListType, types.TupleType)): + if len(tags) == 0: + return tags + contents = set() + for item in tags: + if isinstance(item, types.StringTypes): + contents.add('string') + elif isinstance(item, Tag): + contents.add('tag') + elif isinstance(item, (types.IntType, types.LongType)): + contents.add('int') + if len(contents) == 1: + if 'string' in contents: + return Tag.objects.filter(name__in=[force_unicode(tag) \ + for tag in tags]) + elif 'tag' in contents: + return tags + elif 'int' in contents: + return Tag.objects.filter(id__in=tags) + else: + raise ValueError(_('If a list or tuple of tags is provided, they must all be tag names, Tag objects or Tag ids.')) + else: + raise ValueError(_('The tag input given was invalid.')) + +def get_tag(tag): + """ + Utility function for accepting single tag input in a flexible + manner. + + If a ``Tag`` object is given it will be returned as-is; if a + string or integer are given, they will be used to lookup the + appropriate ``Tag``. + + If no matching tag can be found, ``None`` will be returned. + """ + from tagging.models import Tag + if isinstance(tag, Tag): + return tag + + try: + if isinstance(tag, types.StringTypes): + return Tag.objects.get(name=tag) + elif isinstance(tag, (types.IntType, types.LongType)): + return Tag.objects.get(id=tag) + except Tag.DoesNotExist: + pass + + return None + +# Font size distribution algorithms +LOGARITHMIC, LINEAR = 1, 2 + +def _calculate_thresholds(min_weight, max_weight, steps): + delta = (max_weight - min_weight) / float(steps) + return [min_weight + i * delta for i in range(1, steps + 1)] + +def _calculate_tag_weight(weight, max_weight, distribution): + """ + Logarithmic tag weight calculation is based on code from the + `Tag Cloud`_ plugin for Mephisto, by Sven Fuchs. + + .. _`Tag Cloud`: http://www.artweb-design.de/projects/mephisto-plugin-tag-cloud + """ + if distribution == LINEAR or max_weight == 1: + return weight + elif distribution == LOGARITHMIC: + return math.log(weight) * max_weight / math.log(max_weight) + raise ValueError(_('Invalid distribution algorithm specified: %s.') % distribution) + +def calculate_cloud(tags, steps=4, distribution=LOGARITHMIC): + """ + Add a ``font_size`` attribute to each tag according to the + frequency of its use, as indicated by its ``count`` + attribute. + + ``steps`` defines the range of font sizes - ``font_size`` will + be an integer between 1 and ``steps`` (inclusive). + + ``distribution`` defines the type of font size distribution + algorithm which will be used - logarithmic or linear. It must be + one of ``tagging.utils.LOGARITHMIC`` or ``tagging.utils.LINEAR``. + """ + if len(tags) > 0: + counts = [tag.count for tag in tags] + min_weight = float(min(counts)) + max_weight = float(max(counts)) + thresholds = _calculate_thresholds(min_weight, max_weight, steps) + for tag in tags: + font_set = False + tag_weight = _calculate_tag_weight(tag.count, max_weight, distribution) + for i in range(steps): + if not font_set and tag_weight <= thresholds[i]: + tag.font_size = i + 1 + font_set = True + return tags diff --git a/lib/tagging/views.py b/lib/tagging/views.py new file mode 100644 index 0000000..9e7e2f5 --- /dev/null +++ b/lib/tagging/views.py @@ -0,0 +1,52 @@ +""" +Tagging related views. +""" +from django.http import Http404 +from django.utils.translation import ugettext as _ +from django.views.generic.list_detail import object_list + +from tagging.models import Tag, TaggedItem +from tagging.utils import get_tag, get_queryset_and_model + +def tagged_object_list(request, queryset_or_model=None, tag=None, + related_tags=False, related_tag_counts=True, **kwargs): + """ + A thin wrapper around + ``django.views.generic.list_detail.object_list`` which creates a + ``QuerySet`` containing instances of the given queryset or model + tagged with the given tag. + + In addition to the context variables set up by ``object_list``, a + ``tag`` context variable will contain the ``Tag`` instance for the + tag. + + If ``related_tags`` is ``True``, a ``related_tags`` context variable + will contain tags related to the given tag for the given model. + Additionally, if ``related_tag_counts`` is ``True``, each related + tag will have a ``count`` attribute indicating the number of items + which have it in addition to the given tag. + """ + if queryset_or_model is None: + try: + queryset_or_model = kwargs.pop('queryset_or_model') + except KeyError: + raise AttributeError(_('tagged_object_list must be called with a queryset or a model.')) + + if tag is None: + try: + tag = kwargs.pop('tag') + except KeyError: + raise AttributeError(_('tagged_object_list must be called with a tag.')) + + tag_instance = get_tag(tag) + if tag_instance is None: + raise Http404(_('No Tag found matching "%s".') % tag) + queryset = TaggedItem.objects.get_by_model(queryset_or_model, tag_instance) + if not kwargs.has_key('extra_context'): + kwargs['extra_context'] = {} + kwargs['extra_context']['tag'] = tag_instance + if related_tags: + kwargs['extra_context']['related_tags'] = \ + Tag.objects.related_for_model(tag_instance, queryset_or_model, + counts=related_tag_counts) + return object_list(request, queryset, **kwargs) diff --git a/media/js/mainmap.js b/media/js/mainmap.js deleted file mode 100644 index c24956d..0000000 --- a/media/js/mainmap.js +++ /dev/null @@ -1,2015 +0,0 @@ - - - - - // center on a country - function focusCountry(latitude, longitude, zoom) { - map.setZoom(zoom); - map.panTo(new google.maps.LatLng(latitude, longitude)); - }; - - - var w1_route = [ - - new google.maps.LatLng(33.6340591334, -117.905273421), - - new google.maps.LatLng(39.8720671401, -75.2433013811), - - new google.maps.LatLng(48.8591810972, 2.36274719205), - - new google.maps.LatLng(25.2571166566, 55.3573608321), - - new google.maps.LatLng(9.96462383888, 76.241512288), - - new google.maps.LatLng(9.97983994818, 76.3058853043), - - new google.maps.LatLng(12.8800862022, 74.8512267962), - - new google.maps.LatLng(15.2835227519, 73.9537811176), - - new google.maps.LatLng(15.2785549849, 73.9194488422), - - new google.maps.LatLng(15.2878280549, 73.9520645039), - - new google.maps.LatLng(15.3815292342, 73.8308715718), - - new google.maps.LatLng(19.0916444473, 72.8647613424), - - new google.maps.LatLng(23.0317150151, 72.5839233297), - - new google.maps.LatLng(24.5671083494, 73.6962890522), - - new google.maps.LatLng(26.2934150009, 72.9931640523), - - new google.maps.LatLng(27.0395565987, 70.8837890526), - - new google.maps.LatLng(28.6538383041, 77.235259999), - - new google.maps.LatLng(27.1837989989, 78.0139160048), - - new google.maps.LatLng(28.6351574683, 77.321777333), - - new google.maps.LatLng(27.693864327, 85.3122711063), - - new google.maps.LatLng(28.2634144723, 83.9657592657), - - new google.maps.LatLng(27.7163574808, 85.3318405033), - - new google.maps.LatLng(13.7480558966, 100.491943345), - - new google.maps.LatLng(18.7828168844, 98.688297258), - - new google.maps.LatLng(18.6228217836, 98.1161498887), - - new google.maps.LatLng(18.7867173121, 98.6627197128), - - new google.maps.LatLng(19.1503574528, 98.4429931504), - - new google.maps.LatLng(20.2441597546, 100.449371324), - - new google.maps.LatLng(20.1610319993, 100.581893907), - - new google.maps.LatLng(19.8190360667, 100.583267198), - - new google.maps.LatLng(19.9049273981, 100.80917357), - - new google.maps.LatLng(19.841643557, 100.996627794), - - new google.maps.LatLng(19.892014381, 101.130523668), - - new google.maps.LatLng(19.817744113, 101.252059922), - - new google.maps.LatLng(19.8306631778, 101.485519395), - - new google.maps.LatLng(19.8377682157, 101.622161851), - - new google.maps.LatLng(19.9333323258, 101.717605577), - - new google.maps.LatLng(19.9843192647, 101.908493028), - - new google.maps.LatLng(20.0585112659, 101.994323716), - - new google.maps.LatLng(20.0514161545, 102.056121812), - - new google.maps.LatLng(20.0920474486, 102.144012437), - - new google.maps.LatLng(20.0546412449, 102.216110215), - - new google.maps.LatLng(19.9559235088, 102.242202745), - - new google.maps.LatLng(19.8868488792, 102.130966172), - - new google.maps.LatLng(20.3085692942, 102.425537095), - - new google.maps.LatLng(20.3858253792, 102.301940904), - - new google.maps.LatLng(20.5839386078, 102.400817857), - - new google.maps.LatLng(20.630213815, 101.969604478), - - new google.maps.LatLng(20.8998713443, 101.892700181), - - new google.maps.LatLng(21.0588708637, 101.640014634), - - new google.maps.LatLng(20.9306586408, 101.398315416), - - new google.maps.LatLng(20.6713355245, 101.260986314), - - new google.maps.LatLng(20.655916187, 101.063232408), - - new google.maps.LatLng(20.3343256141, 100.68695067), - - new google.maps.LatLng(20.1874572588, 100.55236815), - - new google.maps.LatLng(20.2286974872, 100.464477525), - - new google.maps.LatLng(20.1436275548, 100.599060045), - - new google.maps.LatLng(19.8442270653, 100.582580552), - - new google.maps.LatLng(19.8855574775, 100.810546861), - - new google.maps.LatLng(19.8700598353, 100.983581529), - - new google.maps.LatLng(19.8984710212, 101.10717772), - - new google.maps.LatLng(19.8390600067, 101.22802733), - - new google.maps.LatLng(19.8338927799, 101.453247056), - - new google.maps.LatLng(19.8648936179, 101.585082994), - - new google.maps.LatLng(19.9526963949, 101.703186021), - - new google.maps.LatLng(20.0017414002, 101.881713853), - - new google.maps.LatLng(20.0559312625, 101.969604478), - - new google.maps.LatLng(20.0843089663, 102.065734849), - - new google.maps.LatLng(20.1049440695, 102.12066649), - - new google.maps.LatLng(20.0791497658, 102.216796861), - - new google.maps.LatLng(19.9862551527, 102.236022935), - - new google.maps.LatLng(19.9088010977, 102.145385728), - - new google.maps.LatLng(19.7279276408, 102.200317369), - - new google.maps.LatLng(19.5546138828, 102.233276353), - - new google.maps.LatLng(19.4989587001, 102.422790513), - - new google.maps.LatLng(19.3979539457, 102.433776841), - - new google.maps.LatLng(19.3370617985, 102.369232163), - - new google.maps.LatLng(19.2541083139, 102.275848374), - - new google.maps.LatLng(19.1516547421, 102.212676988), - - new google.maps.LatLng(19.1321943319, 102.332153306), - - new google.maps.LatLng(19.0763951195, 102.426910386), - - new google.maps.LatLng(18.9283714639, 102.453002915), - - new google.maps.LatLng(18.8543103594, 102.531280503), - - new google.maps.LatLng(18.7815167218, 102.516174302), - - new google.maps.LatLng(18.6605578234, 102.329406724), - - new google.maps.LatLng(18.5590418612, 102.387084947), - - new google.maps.LatLng(18.3636492713, 102.424163804), - - new google.maps.LatLng(18.1210551472, 102.506561265), - - new google.maps.LatLng(17.9669765887, 102.597198472), - - new google.maps.LatLng(18.3858049288, 103.211059556), - - new google.maps.LatLng(18.3440977996, 103.925170884), - - new google.maps.LatLng(18.0936442681, 104.304199204), - - new google.maps.LatLng(18.2032620171, 104.985351548), - - new google.maps.LatLng(17.8898868162, 105.413818345), - - new google.maps.LatLng(17.7329908472, 105.227050767), - - new google.maps.LatLng(17.5131057154, 104.754638657), - - new google.maps.LatLng(17.2195113315, 105.018310532), - - new google.maps.LatLng(16.6888169539, 104.968872056), - - new google.maps.LatLng(16.5730227169, 104.754638657), - - new google.maps.LatLng(16.0141359999, 105.463256821), - - new google.maps.LatLng(16.0088559356, 106.188354477), - - new google.maps.LatLng(15.7129507237, 106.81457518), - - new google.maps.LatLng(15.3848394606, 107.089233384), - - new google.maps.LatLng(14.8333015135, 106.798095688), - - new google.maps.LatLng(15.4060236625, 107.017822251), - - new google.maps.LatLng(15.702374674, 106.710205063), - - new google.maps.LatLng(15.1198559395, 105.814819321), - - new google.maps.LatLng(14.019355705, 105.897216782), - - new google.maps.LatLng(13.758060281, 106.100463852), - - new google.maps.LatLng(13.5071554577, 105.963134751), - - new google.maps.LatLng(13.7527246625, 106.995849594), - - new google.maps.LatLng(13.421680541, 106.08398436), - - new google.maps.LatLng(12.5331153556, 106.034545884), - - new google.maps.LatLng(12.0608090567, 106.419067368), - - new google.maps.LatLng(12.447304849, 107.182617173), - - new google.maps.LatLng(12.1145227695, 106.46301268), - - new google.maps.LatLng(11.5553803207, 104.935913071), - - new google.maps.LatLng(12.7635890668, 103.969116196), - - new google.maps.LatLng(13.1008799677, 103.222045884), - - new google.maps.LatLng(13.1383284513, 103.710937486), - - new google.maps.LatLng(13.3575543677, 103.864746079), - - new google.maps.LatLng(12.7421584486, 104.875488267), - - new google.maps.LatLng(11.6307157364, 104.985351548), - - new google.maps.LatLng(10.6066196404, 104.183349595), - - new google.maps.LatLng(10.320323624, 104.29870604), - - new google.maps.LatLng(10.6012202844, 104.144897446), - - new google.maps.LatLng(10.6822005986, 103.908691392), - - new google.maps.LatLng(10.7847446584, 104.024047837), - - new google.maps.LatLng(10.6983940764, 103.820800767), - - new google.maps.LatLng(10.6174180665, 103.5406494), - - new google.maps.LatLng(11.6091934063, 102.991332993), - - new google.maps.LatLng(11.9318523253, 102.788085923), - - new google.maps.LatLng(12.2165490146, 102.529907212), - - new google.maps.LatLng(12.8010882779, 101.645507798), - - new google.maps.LatLng(13.6673382578, 100.508422838), - - new google.maps.LatLng(8.02115545545, 98.9044189315), - - new google.maps.LatLng(7.74365134419, 98.7615966659), - - new google.maps.LatLng(7.52587276796, 99.0802001815), - - new google.maps.LatLng(7.32092487789, 99.1365051132), - - new google.maps.LatLng(7.45847521581, 99.6391296248), - - new google.maps.LatLng(7.56263066107, 99.616470323), - - new google.maps.LatLng(9.06412676503, 99.1571044784), - - new google.maps.LatLng(10.5364205975, 99.124145494), - - new google.maps.LatLng(11.8834776976, 99.7943115095), - - new google.maps.LatLng(13.2239035109, 99.8052978377), - - new google.maps.LatLng(13.6780132549, 100.415039049), - - new google.maps.LatLng(51.4882243211, -0.0988769531112), - - new google.maps.LatLng(47.5172006927, 19.0502929661), - - new google.maps.LatLng(44.9103591696, 15.6198120095), - - new google.maps.LatLng(44.2963328751, 15.8477783181), - - new google.maps.LatLng(44.1211132327, 15.6170654275), - - new google.maps.LatLng(44.0224465695, 15.5758666971), - - new google.maps.LatLng(43.695679693, 16.0263061501), - - new google.maps.LatLng(43.5704418011, 16.6909790016), - - new google.maps.LatLng(43.2951993872, 17.0205688453), - - new google.maps.LatLng(43.0769131212, 17.4188232398), - - new google.maps.LatLng(42.6662807008, 18.064270017), - - new google.maps.LatLng(42.8699254822, 17.6934814428), - - new google.maps.LatLng(43.2592059246, 17.0672607398), - - new google.maps.LatLng(43.4509250027, 16.6168212867), - - new google.maps.LatLng(43.5027446733, 16.4218139626), - - new google.maps.LatLng(43.5425757176, 16.01257324), - - new google.maps.LatLng(43.5206718975, 16.2542724587), - - new google.maps.LatLng(43.5346116125, 16.0372924782), - - new google.maps.LatLng(43.6380629226, 15.930175779), - - new google.maps.LatLng(43.8325455167, 15.6472778299), - - new google.maps.LatLng(44.103365373, 15.2737426737), - - new google.maps.LatLng(45.3405631339, 14.4305419902), - - new google.maps.LatLng(46.0560792712, 14.506072996), - - new google.maps.LatLng(46.3687265468, 14.1133117656), - - new google.maps.LatLng(46.2843258376, 13.8935852031), - - new google.maps.LatLng(46.2121502356, 14.0020751934), - - new google.maps.LatLng(46.2264029426, 14.1806030254), - - new google.maps.LatLng(46.3052010508, 14.2080688457), - - new google.maps.LatLng(46.3990410423, 14.1380310039), - - new google.maps.LatLng(47.8002416277, 13.0435180646), - - new google.maps.LatLng(47.8205318624, 13.0902099591), - - new google.maps.LatLng(48.8195241368, 14.3151855449), - - new google.maps.LatLng(50.0906310996, 14.415435789), - - new google.maps.LatLng(48.2118624121, 16.376495359), - - new google.maps.LatLng(46.9352608755, 7.47070312396), - - new google.maps.LatLng(47.3164829292, 4.97680663993), - - new google.maps.LatLng(48.8357974573, 2.54882812465), - - new google.maps.LatLng(40.6483455119, -73.7807464497), - - new google.maps.LatLng(40.7175989451, -73.9984130756), - - new google.maps.LatLng(40.7758618059, -73.8720703022), - - new google.maps.LatLng(33.640919208, -84.4244384648), - - new google.maps.LatLng(33.9456384488, -118.421630843) - - ]; - - var nic_route = [ - - new google.maps.LatLng(12.140536467, -86.1697196841), - - new google.maps.LatLng(12.1521159412, -86.2722015261), - - new google.maps.LatLng(12.026560945, -86.177444446), - - new google.maps.LatLng(12.0178303361, -86.1437988161), - - new google.maps.LatLng(12.0003682688, -86.1307525515), - - new google.maps.LatLng(12.0003682688, -86.1115264773), - - new google.maps.LatLng(11.9741730472, -86.0703277468), - - new google.maps.LatLng(11.9788749408, -86.0524749636), - - new google.maps.LatLng(11.977531551, -86.0291290163), - - new google.maps.LatLng(11.961410352, -86.0009765505), - - new google.maps.LatLng(11.9607386145, -85.9913635134), - - new google.maps.LatLng(11.9258059681, -85.9652709841), - - new google.maps.LatLng(11.8928845495, -85.994796741), - - new google.maps.LatLng(11.8545831929, -86.0215759158), - - new google.maps.LatLng(11.7786365588, -86.0531616091), - - new google.maps.LatLng(11.7551090001, -86.044921863), - - new google.maps.LatLng(11.7470419455, -86.0256957888), - - new google.maps.LatLng(11.7147713667, -86.0112762331), - - new google.maps.LatLng(11.6569438261, -85.9707641482), - - new google.maps.LatLng(11.5984316183, -85.9158325076), - - new google.maps.LatLng(11.5836334823, -85.9020995974), - - new google.maps.LatLng(11.5257787151, -85.8993530154), - - new google.maps.LatLng(11.4423392523, -85.8238220095), - - new google.maps.LatLng(11.3070343889, -85.8554077029), - - new google.maps.LatLng(11.3009744617, -85.8876800418), - - new google.maps.LatLng(11.2740399031, -85.8876800418), - - new google.maps.LatLng(11.262591951, -85.8732604861), - - new google.maps.LatLng(11.2888542231, -85.8904266238), - - new google.maps.LatLng(11.3003011285, -85.8698272586), - - new google.maps.LatLng(11.3440644904, -85.8519744754), - - new google.maps.LatLng(11.4362822027, -85.8300018191), - - new google.maps.LatLng(11.5304882703, -85.8924865603), - - new google.maps.LatLng(11.5836334823, -85.8938598513), - - new google.maps.LatLng(11.6051577845, -85.9103393435), - - new google.maps.LatLng(11.6582887897, -85.9803771853), - - new google.maps.LatLng(11.7127543302, -86.0195159792), - - new google.maps.LatLng(11.743680603, -86.0325622439), - - new google.maps.LatLng(11.7524200082, -86.0517883181), - - new google.maps.LatLng(11.7766199898, -86.0593414187), - - new google.maps.LatLng(11.857271183, -86.0291290163), - - new google.maps.LatLng(11.9103535541, -86.0771942019), - - new google.maps.LatLng(11.9325241345, -86.090927112), - - new google.maps.LatLng(11.9674559143, -86.0923004031), - - new google.maps.LatLng(11.9929801305, -86.1039733767), - - new google.maps.LatLng(12.0003682688, -86.1383056521), - - new google.maps.LatLng(12.0178303361, -86.1520385622), - - new google.maps.LatLng(12.0359628223, -86.1856841921), - - new google.maps.LatLng(12.1454032639, -86.2728881716), - - new google.maps.LatLng(12.1420468617, -86.1767578005), - - new google.maps.LatLng(12.0122896105, -83.7633705023), - - new google.maps.LatLng(12.1752733775, -83.0580139045), - - new google.maps.LatLng(12.2823082653, -82.9728698615), - - new google.maps.LatLng(12.2779471901, -82.9797363166), - - new google.maps.LatLng(12.1759445775, -83.0552673224), - - new google.maps.LatLng(12.0151439379, -83.7676620367), - - new google.maps.LatLng(12.1400330001, -86.1812209963), - - new google.maps.LatLng(12.1551365908, -86.2701415896), - - new google.maps.LatLng(12.1383547705, -86.308593738), - - new google.maps.LatLng(12.1302991211, -86.3103103518), - - new google.maps.LatLng(12.1410399328, -86.3329696535), - - new google.maps.LatLng(12.1735953703, -86.3463592409), - - new google.maps.LatLng(12.2353390434, -86.4239501833), - - new google.maps.LatLng(12.2796245352, -86.5159606813), - - new google.maps.LatLng(12.2675474115, -86.5461730837), - - new google.maps.LatLng(12.2769407778, -86.5708923219), - - new google.maps.LatLng(12.3198776097, -86.6107177614), - - new google.maps.LatLng(12.337318692, -86.664276111), - - new google.maps.LatLng(12.4258477813, -86.8702697633), - - new google.maps.LatLng(12.4204832376, -86.9444274781), - - new google.maps.LatLng(12.3748801543, -87.0323181031), - - new google.maps.LatLng(12.3614659657, -87.018585193), - - new google.maps.LatLng(12.3762215353, -87.021331775), - - new google.maps.LatLng(12.3963414227, -86.9705200074), - - new google.maps.LatLng(12.4325533053, -86.8881225465), - - new google.maps.LatLng(12.3426849453, -86.6862487672), - - new google.maps.LatLng(12.3091440596, -86.6065978883), - - new google.maps.LatLng(12.2809664037, -86.5873718141), - - new google.maps.LatLng(12.2648635311, -86.5338134645), - - new google.maps.LatLng(12.2729150904, -86.5008544801), - - new google.maps.LatLng(12.2407073777, -86.4418029665), - - new google.maps.LatLng(12.1641983336, -86.3580322145), - - new google.maps.LatLng(12.1319774015, -86.3250732302), - - new google.maps.LatLng(12.1548009647, -86.2866210817), - - new google.maps.LatLng(12.1507734193, -86.1877441286) - - ]; - - function showRoute(route, zoom, latitude, longitude) { - var routePath = new google.maps.Polyline({ - path: eval(route), - strokeColor: "#FF0000", - strokeOpacity: 1.0, - strokeWeight: 2 - }); - map.setZoom(zoom); - map.panTo(new google.maps.LatLng(latitude, longitude)); - routePath.setMap(map); - return false; - }; -var map; -function initialize() { - - //custom marker - var image = new google.maps.MarkerImage('http://media.luxagraf.net/img/marker-entry.png', - new google.maps.Size(15, 26), - new google.maps.Point(0, 0), - new google.maps.Point(7, 26) - ); - //custom marker shadow - var shadow = new google.maps.MarkerImage('http://media.luxagraf.net/img/shadow.png', - new google.maps.Size(37, 34), - new google.maps.Point(0,0), - new google.maps.Point(8, 34) - ); - - - //check for a permalink - var location = window.location.hash; - //find a centerpoint - var pts = new Array(); - pts[0] = ["#austria", 47.683,14.912,8];pts[1] = ["#cambodia", 12.714,104.564,7];pts[2] = ["#croatia", 45.723,16.693,7];pts[3] = ["#czech-republic", 49.9017112173,14.9963378906,7];pts[4] = ["#france", 46.565,2.55,6];pts[5] = ["#hungary", 47.07,19.134,7];pts[6] = ["#india", 21.0,78.5,5];pts[7] = ["#laos", 19.905,102.471,6];pts[8] = ["#nepal", 28.253,83.939,6];pts[9] = ["#nicaragua", 12.84,-85.034,7];pts[10] = ["#slovenia", 46.124,14.827,9];pts[11] = ["#thailand", 15.7,100.844,5];pts[12] = ["#united-kingdom", 53.0,-1.6,6];pts[13] = ["#united-states", 39.622,-98.606,4]; - pts[pts.length] = ["#central-america", 14.3708339734,-87.8686523438,6];pts[pts.length] = ["#southeast-asia", 12.2970682929,102.744140625,5];pts[pts.length] = ["#central-asia", 20.0146454453,79.892578125,5];pts[pts.length] = ["#europe", 47.0102256557,4.74609375,5];pts[pts.length] = ["#north-america", 39.0277188402,-98.349609375,4]; - if (location.length>1) { - for (i=0;i<pts.length;i++) { - if (location == pts[i][0]) { - centerCoord = new google.maps.LatLng(pts[i][1],pts[i][2]); - zoom = pts[i][3]; - break; - } else { - centerCoord = new google.maps.LatLng(19.311143,2.460938); - zoom = 2; - } - } - } else { - centerCoord = new google.maps.LatLng(19.311143,2.460938); - zoom = 2; - } - //set up map options - var mapOptions = { - zoom: zoom, - center: centerCoord, - mapTypeId: google.maps.MapTypeId.TERRAIN, - disableDefaultUI: true, - navigationControl: true, - navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL} - }; - //create map - map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); - - - //loop through and set up markers/info windows - - - var marker_dinosaur_national = new google.maps.Marker({ - position: new google.maps.LatLng(40.4574623906, -109.258432373), - map: map, - shadow: shadow, - icon: image - }); - - var c_dinosaur_national = '<div class="infowin"><h4>Dinosaur National Monument, Part Two: Down the River<\/h4><span class="date blok">August 2, 2010 (Dinosaur National Monument, Colorado)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/lodorecanyonv.jpg" height="100" alt="Dinosaur National Monument, Part Two: Down the River" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>This is the only real way to see Dinosaur National Monument \u0026mdash\u003B you must journey down the river. There are two major rivers running through Dinosaur, the Yampa, which carves through Yampa Canyon, and the Green, which cuts through Lodore. \u003Ca href\u003D\u0022http://www.adventureboundusa.com/\u0022 title\u003D\u0022Adventure Bound Rafting\u0022\u003EAdventure Bound Rafting\u003C/a\u003E runs some of the best whitewater rafting trips in Colorado and I was lucky enough to go down the Green River with them, through the majestic Lodore Canyon. <a href="/2010/aug/02/dinosaur-national-monument-part-two-down-river/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_dinosaur_national, 'click', function() { - openWin(c_dinosaur_national,marker_dinosaur_national); - }); - - - var marker_dinosaur_national = new google.maps.Marker({ - position: new google.maps.LatLng(40.5206340265, -108.993880734), - map: map, - shadow: shadow, - icon: image - }); - - var c_dinosaur_national = '<div class="infowin"><h4>Dinosaur National Monument, Part One: Echo Park<\/h4><span class="date blok">July 28, 2010 (Dinosaur National Monument, Colorado)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/dinosaurv.jpg" height="100" alt="Dinosaur National Monument, Part One: Echo Park" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Dinosaur National Monument was poorly named. The best parts of it are not the fossils in the quarry (which is closed for 2010 anyway) but the canyon country \u0026mdash\u003B some of the best, most remote canyon country you\u0027ll find in this part of the world. <a href="/2010/jul/28/dinosaur-national-monument-part-one-echo-park/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_dinosaur_national, 'click', function() { - openWin(c_dinosaur_national,marker_dinosaur_national); - }); - - - var marker_the_endless = new google.maps.Marker({ - position: new google.maps.LatLng(44.4618029245, -110.821969792), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_endless = '<div class="infowin"><h4>The Endless Crowds of Yellowstone<\/h4><span class="date blok">July 25, 2010 (Yellowstone National Park, Wyoming)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/yellowstonev.jpg" height="100" alt="The Endless Crowds of Yellowstone" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>There is wilderness in Yellowstone, even if it\u0027s just inches from the boardwalks that transport thousands around the geothermal pools. It may not be wilderness on a grand scale \u0026mdash\u003B the sweeping mountain peaks or wild rivers of other parks \u0026mdash\u003B but in some ways that makes it more enticing. As one Ranger told me, Yellowstone isn\u0027t about the big picture, the grand scenery, it\u0027s about the tiny details within each pool. To really see Yellowstone, he said, you have to take your time, move slowly and look closely. <a href="/2010/jul/25/endless-crowds-yellowstone/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_endless, 'click', function() { - openWin(c_the_endless,marker_the_endless); - }); - - - var marker_backpacking_in = new google.maps.Marker({ - position: new google.maps.LatLng(43.7931543168, -110.79651831), - map: map, - shadow: shadow, - icon: image - }); - - var c_backpacking_in = '<div class="infowin"><h4>Backpacking in the Grand Tetons<\/h4><span class="date blok">July 22, 2010 (Grand Teton National Park, Wyoming)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/grandtetonsv.jpg" height="100" alt="Backpacking in the Grand Tetons" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Hiking into the wilderness empties your mind. You fall into the silence of the mountains and you can relax in a way that\u0027s very difficult to do in the midst of civilization. The white noise that surrounds us in our everyday lives, that noise we don\u0027t even notice as it adds thin layers of stress that build up over days, weeks, years, does not seem capable of following us into the mountains. <a href="/2010/jul/22/backpacking-grand-tetons/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_backpacking_in, 'click', function() { - openWin(c_backpacking_in,marker_backpacking_in); - }); - - - var marker_great_sand = new google.maps.Marker({ - position: new google.maps.LatLng(37.7267371803, -105.550975785), - map: map, - shadow: shadow, - icon: image - }); - - var c_great_sand = '<div class="infowin"><h4>Great Sand Dunes National Park<\/h4><span class="date blok">July 17, 2010 (Great Sand Dunes National Park, Colorado)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/greatsanddunesv_4.jpg" height="100" alt="Great Sand Dunes National Park" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Something about the desert inspires me to get up early and watch the sunrise. The cool mornings seem worth getting up for out here in the high plains of Colorado, especially when there\u0027s the chance to watch the sunrise from the largest sand dunes in North America, here in Great Sand Dune National Park. <a href="/2010/jul/17/great-sand-dunes-national-park/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_great_sand, 'click', function() { - openWin(c_great_sand,marker_great_sand); - }); - - - var marker_comanche_national = new google.maps.Marker({ - position: new google.maps.LatLng(37.14748996, -103.009572015), - map: map, - shadow: shadow, - icon: image - }); - - var c_comanche_national = '<div class="infowin"><h4>Comanche National Grasslands<\/h4><span class="date blok">July 16, 2010 (Comanche National Grasslands, Colorado)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/comanchegrasslands.jpg" height="100" alt="Comanche National Grasslands" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>To say the Comanche National Grasslands is off the grid would be an understatement. With the exception of Highway 50 in Nevada, I\u0027ve never driven through such isolation and vast openness anywhere in the world. And it\u0027s easy to get lost. There are no signs, no road names even, just dirt paths crisscrossing a wide, perfectly flat expanses of grass. <a href="/2010/jul/16/comanche-national-grasslands/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_comanche_national, 'click', function() { - openWin(c_comanche_national,marker_comanche_national); - }); - - - var marker_why_national = new google.maps.Marker({ - position: new google.maps.LatLng(35.1885403096, -101.919479356), - map: map, - shadow: shadow, - icon: image - }); - - var c_why_national = '<div class="infowin"><h4>Why National Parks Are Better Than State Parks<\/h4><span class="date blok">July 15, 2010 (Amarillo, Texas)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/paloduratn.jpg" height="100" alt="Why National Parks Are Better Than State Parks" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>There are many reasons, but here\u0027s the one I currently consider most important: National Parks never close. Take Palo Dura State park outside of Amarillo, Texas. Were it a National Park, I would be there right now. But it\u0027s not, it\u0027s a state park and so I\u0027m sitting in a hotel room in Amarillo because everyone knows nature closes at 10PM. <a href="/2010/jul/15/why-national-parks-are-better-state-parks/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_why_national, 'click', function() { - openWin(c_why_national,marker_why_national); - }); - - - var marker_the_legend = new google.maps.Marker({ - position: new google.maps.LatLng(31.9819206926, -98.0308770997), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_legend = '<div class="infowin"><h4>The Legend of Billy the Kid<\/h4><span class="date blok">July 11, 2010 (Hico, Texas)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/Billykid.jpg" height="100" alt="The Legend of Billy the Kid" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>History rarely offers neat, tidy stories. But the messier, more confusing and more controversial the story becomes, the more it works its way into our imaginations. The legend of Billy the Kid is like that of Amelia Earhart or D.B. Cooper \u0026mdash\u003B the less we know for sure, the more compelling the story becomes. <a href="/2010/jul/11/legend-billy-the-kid/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_legend, 'click', function() { - openWin(c_the_legend,marker_the_legend); - }); - - - var marker_the_dixie = new google.maps.Marker({ - position: new google.maps.LatLng(29.9559036138, -90.0651186579), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_dixie = '<div class="infowin"><h4>The Dixie Drug Store<\/h4><span class="date blok">July 8, 2010 (New Orleans, Louisiana)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/nopharmacymuseum.jpg" height="100" alt="The Dixie Drug Store" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>New Orleans is it\u0027s own world. So much so that\u0027s it\u0027s impossible to put your finger on what it is that makes it different. New Orleans is a place where the line between consensus reality and private dream seems to have never fully developed. And a wonderful world it is. <a href="/2010/jul/08/dixie-drug-store/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_dixie, 'click', function() { - openWin(c_the_dixie,marker_the_dixie); - }); - - - var marker_begin_the = new google.maps.Marker({ - position: new google.maps.LatLng(30.3804002966, -89.0308105822), - map: map, - shadow: shadow, - icon: image - }); - - var c_begin_the = '<div class="infowin"><h4>Begin the Begin<\/h4><span class="date blok">July 5, 2010 (Gulf Port, Mississippi)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/gulf_port_beach_tn.jpg" height="100" alt="Begin the Begin" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>It\u0027s travel time again. This time I\u0027m driving my 1969 Ford truck out west, to Texas, Colorado, Utah and more\u0026nbsp\u003B\u0026mdash\u003B a road trip around the western United States. The first stop is Gulf Port, Mississippi. It\u0027s hard to believe, sitting here on the deserted beaches of Gulf Shore, watching the sun break through the ominous clouds, but soon this beauty will be gone. The BP oil spill is somewhere out there, blown slowly ashore by the storm hovering over us, waiting to drown the beaches in crude. <a href="/2010/jul/05/begin-the-begin/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_begin_the, 'click', function() { - openWin(c_begin_the,marker_begin_the); - }); - - - var marker_los_angeles = new google.maps.Marker({ - position: new google.maps.LatLng(34.0558238743, -118.235882504), - map: map, - shadow: shadow, - icon: image - }); - - var c_los_angeles = '<div class="infowin"><h4>Los Angeles, I'm Yours<\/h4><span class="date blok">May 17, 2010 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/launiontickets.jpg" height="100" alt="Los Angeles, I'm Yours" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Los Angeles is all about the car. Shiny, air\u002Dconditioned comfort, gliding you soundlessly from one place to another without the need to interact with anything in between. But I have discovered that if you abandon the car for the subway and your own two feet, the illusion that L.A. is just a model train set world \u0026mdash\u003B tiny, plastic and devoid of any ground beneath the ground \u0026mdash\u003B fades and you find yourself, for a time, in a real city. <a href="/2010/may/17/los-angeles-im-yours/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_los_angeles, 'click', function() { - openWin(c_los_angeles,marker_los_angeles); - }); - - - var marker_therell_be = new google.maps.Marker({ - position: new google.maps.LatLng(36.4209025772, -116.80985926), - map: map, - shadow: shadow, - icon: image - }); - - var c_therell_be = '<div class="infowin"><h4>(There'll Be) Peace in the Valley<\/h4><span class="date blok">April 24, 2010 (Death Valley, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbnail/2010/deathvalley.jpg" height="100" alt="(There'll Be) Peace in the Valley" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Sometimes you ignore the places close to home because, well, there\u0027s always next weekend. Which is why I never made it Death Valley in the twenty\u002Dfive years I lived in California. It took being all the way across the country to get me out to Death Valley. Which might explain why I actually got up before dawn just to watch the sunrise at Zabriskie Point. <a href="/2010/apr/24/death-valley/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_therell_be, 'click', function() { - openWin(c_therell_be,marker_therell_be); - }); - - - var marker_so_far = new google.maps.Marker({ - position: new google.maps.LatLng(30.9134155185, -82.1832228796), - map: map, - shadow: shadow, - icon: image - }); - - var c_so_far = '<div class="infowin"><h4>So Far, I Have Not Found The Science<\/h4><span class="date blok">March 13, 2010 (Okefenokee Swamp, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2010/okeefenokee.jpg" height="100" alt="So Far, I Have Not Found The Science" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>A canoe trip through the Okefenokee Swamp down in the southern most corner of Georgia. Paddling the strange reddish and incredibly still waters. Begging alligators, aching muscles and the kindly folks of Stintson\u0027s Barbecue all getting their due. <a href="/2010/mar/13/so-far-i-have-not-found-science/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_so_far, 'click', function() { - openWin(c_so_far,marker_so_far); - }); - - - var marker_how_to = new google.maps.Marker({ - position: new google.maps.LatLng(33.9576352028, -83.4087180975), - map: map, - shadow: shadow, - icon: image - }); - - var c_how_to = '<div class="infowin"><h4>How to Get Off Your Butt and Travel the World<\/h4><span class="date blok">May 3, 2009 (Athens, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2010/traveltheworld.jpg" height="100" alt="How to Get Off Your Butt and Travel the World" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>How do you make the leap from cubicle daydreams to life on to the road? You want to travel the world, but, like me, you have a million excuses stopping you. How do overcome the inertia that keeps you trapped in a life that isn\u0027t what you want it to be? Here\u0027s a few practical tips and how tos designed to motivate you to get off your butt and travel the world. <a href="/2009/may/03/how-to-get-your-butt-and-travel-world/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_how_to, 'click', function() { - openWin(c_how_to,marker_how_to); - }); - - - var marker_no_strangers = new google.maps.Marker({ - position: new google.maps.LatLng(33.9581869416, -83.4082460287), - map: map, - shadow: shadow, - icon: image - }); - - var c_no_strangers = '<div class="infowin"><h4>No Strangers on a Train<\/h4><span class="date blok">April 13, 2009 (Athens, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2009/strangersonatrain.jpg" height="100" alt="No Strangers on a Train" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>We mythologize trains because they harken back to an age of community travel, a real, tangible community of travelers, not just backpackers, but people from all walks of life, people traveling near and far together in a shared space that isn\u0027t locked down like an airplane and isn\u0027t isolated like a car\u003B it\u0027s a shared travel experience and there are precious few of those left in our world. <a href="/2009/apr/13/strangers-on-a-train/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_no_strangers, 'click', function() { - openWin(c_no_strangers,marker_no_strangers); - }); - - - var marker_leonardo_da = new google.maps.Marker({ - position: new google.maps.LatLng(33.5214419937, -86.810799825), - map: map, - shadow: shadow, - icon: image - }); - - var c_leonardo_da = '<div class="infowin"><h4>Leonardo Da Vinci and the Codex on Bunnies<\/h4><span class="date blok">December 9, 2008 (Birmingham, Alabama)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2009/codexofbunnies.jpg" height="100" alt="Leonardo Da Vinci and the Codex on Bunnies" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>A few pages from Leonardo Da Vinci\u0027s notebooks make a rare trip outside Italy, to Birmingham, AL, of all places. But the Birmingham Museum of Art is home to far more alarming works of art, works which depict the eventual, inevitable, bunny takeover, after which all the elements of our reality will be replaced by bunnies. Seriously. You heard it here first. <a href="/2008/dec/09/leonardo-da-vinci-and-codex-bunnies/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_leonardo_da, 'click', function() { - openWin(c_leonardo_da,marker_leonardo_da); - }); - - - var marker_elkmont_and = new google.maps.Marker({ - position: new google.maps.LatLng(35.6804462348, -83.6502456549), - map: map, - shadow: shadow, - icon: image - }); - - var c_elkmont_and = '<div class="infowin"><h4>Elkmont and the Great Smoky Mountains<\/h4><span class="date blok">October 31, 2008 (Great Smoky Mountains, Tennessee)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/reflectedtrees.jpg" height="100" alt="Elkmont and the Great Smoky Mountains" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Pigeon Forge is Myrtle Beach in the mountains. Redneck weddings cascade straight out of the chapel and into the mini golf reception area. Pigeon Forge is everything that\u0027s wrong with America. But we aren\u0027t here for Pigeon Forge, it just happens to have a free condo we\u0027re staying in. We\u0027re here for the mountains. Smoky Mountain National Park is just a few miles up the road. <a href="/2008/oct/31/elkmont-and-great-smoky-mountains/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_elkmont_and, 'click', function() { - openWin(c_elkmont_and,marker_elkmont_and); - }); - - - var marker_rope_swings = new google.maps.Marker({ - position: new google.maps.LatLng(34.5346315992, -83.9028024557), - map: map, - shadow: shadow, - icon: image - }); - - var c_rope_swings = '<div class="infowin"><h4>Rope Swings and River Floats<\/h4><span class="date blok">July 27, 2008 (Mountain Cabin, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/chestateeriver.jpg" height="100" alt="Rope Swings and River Floats" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Two weekends ago we went up to the mountains, just outside of Dahlonega GA, and floated the Chestatee River using inner tubes, various pool toys and one super\u002Dcool inflatable seahorse. Unfortunately, proving one of my travel mottos \u002D\u002D you can never go back \u002D\u002D a return trip proved disastrous. <a href="/2008/jul/27/rope-swings-and-river-floats/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_rope_swings, 'click', function() { - openWin(c_rope_swings,marker_rope_swings); - }); - - - var marker_our_days = new google.maps.Marker({ - position: new google.maps.LatLng(12.4364822429, -86.8845820306), - map: map, - shadow: shadow, - icon: image - }); - - var c_our_days = '<div class="infowin"><h4>Our Days Are Becoming Nights<\/h4><span class="date blok">July 6, 2008 (León, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/daysnights.jpg" height="100" alt="Our Days Are Becoming Nights" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>A short thought on the eve of our departure from Nicaragua: Everywhere I go I think, I should live here... I should be able to not just visit places, but in habit them. Of course that isn\u0027t possible, which is too bad. <a href="/2008/jul/06/our-days-are-becoming-nights/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_our_days, 'click', function() { - openWin(c_our_days,marker_our_days); - }); - - - var marker_tiny_cities = new google.maps.Marker({ - position: new google.maps.LatLng(12.4356545517, -86.882200229), - map: map, - shadow: shadow, - icon: image - }); - - var c_tiny_cities = '<div class="infowin"><h4>Tiny Cities Made of Ash<\/h4><span class="date blok">July 3, 2008 (León, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/citiesmadeofash.jpg" height="100" alt="Tiny Cities Made of Ash" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The church bells of Le\u0026oacute\u003Bn have become a constant cacophony, not the rhythmic ringing out of the hours or tolling from Mass that the human mind seems to find pleasant, but the atonal banging that only appeals to the young and dumb. But Francisco is entirely unperturbed\u003B He\u0027s too fascinated with the tattoo on Corrinne\u0027s shoulder to bother with what slowly just becomes yet another sound echoing through Le\u0026oacute\u003Bn. <a href="/2008/jul/03/tiny-cities-made-ash/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_tiny_cities, 'click', function() { - openWin(c_tiny_cities,marker_tiny_cities); - }); - - - var marker_you_cant = new google.maps.Marker({ - position: new google.maps.LatLng(12.2896883818, -82.9709815864), - map: map, - shadow: shadow, - icon: image - }); - - var c_you_cant = '<div class="infowin"><h4>You Can't Go Home Again<\/h4><span class="date blok">June 30, 2008 (Little Corn Island, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/nohomeagain.jpg" height="100" alt="You Can't Go Home Again" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The first time we came to Little Corn Island it was April, the tail end of the dry season. It rained once or twice, but never for more than five minutes and always followed by more sunshine. This time it\u0027s the end of June, just well into the wet season, and the island is an entirely different place. <a href="/2008/jun/30/you-cant-go-home-again/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_you_cant, 'click', function() { - openWin(c_you_cant,marker_you_cant); - }); - - - var marker_returning_again = new google.maps.Marker({ - position: new google.maps.LatLng(12.2906947452, -82.9713249091), - map: map, - shadow: shadow, - icon: image - }); - - var c_returning_again = '<div class="infowin"><h4>Returning Again &mdash; Back on Little Corn Island<\/h4><span class="date blok">June 26, 2008 (Little Corn Island, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/littlecornagain.jpg" height="100" alt="Returning Again &mdash; Back on Little Corn Island" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/> Generally speaking, the world seems so huge and so full of amazing destinations that repeating one never struck me as a judicious use of my short allotment of time. But for Little Corn Island I\u0027m willing to make an exception and of course, the universe being what it is, our second trip to Little Corn Island has been unpredictable and entirely new. <a href="/2008/jun/26/returning-again-back-little-corn-island/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_returning_again, 'click', function() { - openWin(c_returning_again,marker_returning_again); - }); - - - var marker_in_love = new google.maps.Marker({ - position: new google.maps.LatLng(33.94487747, -83.3886068943), - map: map, - shadow: shadow, - icon: image - }); - - var c_in_love = '<div class="infowin"><h4>In Love With a View: Vagabonds, Responsibilty and Living Well<\/h4><span class="date blok">June 7, 2008 (Athens, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/wrong.jpg" height="100" alt="In Love With a View: Vagabonds, Responsibilty and Living Well" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Why all the vitriol about a seemingly innocuous concept \u002D\u002D that traveling doesn\u0027t have to cost a lot of money, isn\u0027t all that difficult and hey, you can even go right now? People like us, who feel tied down by responsibility, find the suggestion that we actually aren\u0027t tied down patronizing and yes, elitist. <a href="/2008/jun/07/love-with-a-view-vagabonds-responsibilty-living-we/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_in_love, 'click', function() { - openWin(c_in_love,marker_in_love); - }); - - - var marker_little_island = new google.maps.Marker({ - position: new google.maps.LatLng(12.2974037367, -82.9745864753), - map: map, - shadow: shadow, - icon: image - }); - - var c_little_island = '<div class="infowin"><h4>Little Island in the Sun<\/h4><span class="date blok">April 5, 2008 (Little Corn Island, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/coconutsun.jpg" height="100" alt="Little Island in the Sun" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>We arrived on Little Corn Island around sundown and met Ali, whom I at first took to be a tout, but he showed us the way to our guesthouse and, after settling in and getting a feel for the island, I realized that Ali, wasn\u0027t a tout, he was just a really nice guy who enjoyed doing favors for tourists, just beware the Yoni beverage he offers. <a href="/2008/apr/05/little-island-sun/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_little_island, 'click', function() { - openWin(c_little_island,marker_little_island); - }); - - - var marker_return_to = new google.maps.Marker({ - position: new google.maps.LatLng(11.2543844991, -85.8734750628), - map: map, - shadow: shadow, - icon: image - }); - - var c_return_to = '<div class="infowin"><h4>Return to the Sea<\/h4><span class="date blok">April 2, 2008 (San Juan Del Sur, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/sanjuansunset.jpg" height="100" alt="Return to the Sea" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Southwestern Nicaragua is a very small strip of land with Lago Nicaragua to the east and the Pacific Ocean to the west. The main town in the area, Juan Del Sur, is nestled around a well protected harbor with a mediocre strip of sand. For the nice beaches you have to head up or down the coast to one of the many small inlets. <a href="/2008/apr/02/return-sea/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_return_to, 'click', function() { - openWin(c_return_to,marker_return_to); - }); - - - var marker_ring_the = new google.maps.Marker({ - position: new google.maps.LatLng(11.9320622659, -85.9581363081), - map: map, - shadow: shadow, - icon: image - }); - - var c_ring_the = '<div class="infowin"><h4>Ring The Bells<\/h4><span class="date blok">March 30, 2008 (Granada, Nicaragua)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/ringthbells.jpg" height="100" alt="Ring The Bells" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Church, which dates from the 1600s has the the narrowest, steepest, circular concrete staircase that I\u0027ve ever encountered. It had a low railing and circled up four stories worth of precipitous dropoffs before you hit solid ground. From the top was a views of Granada\u0027s endless sea of mottled pink, orange and brown hues \u002D\u002D terra cotta roof tiles stretching from the shores of Lago Nicaragua all the way back toward the hills. <a href="/2008/mar/30/ring-bells/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_ring_the, 'click', function() { - openWin(c_ring_the,marker_ring_the); - }); - - - var marker_fall = new google.maps.Marker({ - position: new google.maps.LatLng(33.9448641195, -83.3885693434), - map: map, - shadow: shadow, - icon: image - }); - - var c_fall = '<div class="infowin"><h4>Fall<\/h4><span class="date blok">November 14, 2007 (Athens, Georgia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/fall.jpg" height="100" alt="Fall" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The trees are in full technicolor swing. The land is slowly dying, and not just because it\u0027s Fall, we\u0027re also in the middle of a prolonged drought and this year the leaves are opting for a James Dean\u002Dstyle, leave\u002Da\u002Dgood\u002Dlooking\u002Dcorpse exit. If you\u0027re a leaf and you\u0027ve got to go, do it with class. <a href="/2007/nov/14/fall/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_fall, 'click', function() { - openWin(c_fall,marker_fall); - }); - - - var marker_on_the = new google.maps.Marker({ - position: new google.maps.LatLng(33.4619143859, -118.52130173), - map: map, - shadow: shadow, - icon: image - }); - - var c_on_the = '<div class="infowin"><h4>On The Other Ocean<\/h4><span class="date blok">July 23, 2007 (Catalina Island, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/sailing.jpg" height="100" alt="On The Other Ocean" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Consider what would happen if your house were tilted 30 degrees to the left, how this would complicate ordinary activities \u002D\u002D like say walking. Now throw in a bouncing motion that lifts the floor five or six feet up and down in a seesaw\u002Dlike motion on a perpendicular axis to the 30 degree tilt \u002D\u002D things become more like riding a seesaw that\u0027s attached to a merry\u002Dgo\u002Dround which is missing a few bolts. That\u0027s sailing. <a href="/2007/jul/23/other-ocean/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_on_the, 'click', function() { - openWin(c_on_the,marker_on_the); - }); - - - var marker_being_there = new google.maps.Marker({ - position: new google.maps.LatLng(33.6839251309, -78.9283561597), - map: map, - shadow: shadow, - icon: image - }); - - var c_being_there = '<div class="infowin"><h4>Being There<\/h4><span class="date blok">June 17, 2007 (Myrtle Beach Airport, South Carolina)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/myrtlebeachcrap.jpg" height="100" alt="Being There" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Myrtle Beach does not exist. Nearly everything in Myrtle Beach is a paltry derivative of some original form. For instance, most of the country has golf courses, in Myrtle Beach there are endless rows of putt\u002Dputt courses, where most towns attempt to draw in big name musical acts for their tourist venues, Myrtle Beach is content with impersonators. <a href="/2007/jun/17/being-there/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_being_there, 'click', function() { - openWin(c_being_there,marker_being_there); - }); - - - var marker_sailing_through = new google.maps.Marker({ - position: new google.maps.LatLng(32.8355703352, -79.8225617298), - map: map, - shadow: shadow, - icon: image - }); - - var c_sailing_through = '<div class="infowin"><h4>Sailing Through<\/h4><span class="date blok">June 14, 2007 (Charleston, South Carolina)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/charlestonships.jpg" height="100" alt="Sailing Through" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The rumors are true. I moved back to the south\u003B Athens GA to be exact. But I hate staying in one place for too long, so after a month or two in Athens I headed up to Charleston to visit a friend. The south is curious place. If you\u0027ve never been here I couldn\u0027t hope to explain it, but it\u0027s not so much a place as an approach. A way of getting somewhere more than anywhere specific. Perhaps even a wrong turn. \u000D\u000A <a href="/2007/jun/14/sailing-through/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_sailing_through, 'click', function() { - openWin(c_sailing_through,marker_sailing_through); - }); - - - var marker_goodbye_to = new google.maps.Marker({ - position: new google.maps.LatLng(34.0409072252, -118.47207783), - map: map, - shadow: shadow, - icon: image - }); - - var c_goodbye_to = '<div class="infowin"><h4>Goodbye to the Mother and the Cove<\/h4><span class="date blok">March 1, 2007 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/lacloud.jpg" height="100" alt="Goodbye to the Mother and the Cove" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>It\u0027s strange how you can plan something, go through all the motions of making it happen without ever really understanding what you\u0027re doing. I\u0027ve been doing this for the better part of three years now. I realized recently that I have no real idea how I came to be here. \u000D\u000A <a href="/2007/mar/01/goodbye-mother-and-cove/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_goodbye_to, 'click', function() { - openWin(c_goodbye_to,marker_goodbye_to); - }); - - - var marker_everything_all = new google.maps.Marker({ - position: new google.maps.LatLng(33.9753068641, -118.428904994), - map: map, - shadow: shadow, - icon: image - }); - - var c_everything_all = '<div class="infowin"><h4>Everything All The Time<\/h4><span class="date blok">February 3, 2007 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/end.jpg" height="100" alt="Everything All The Time" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I don\u0027t know if I\u0027m just overly paranoid but when I call up memories in the dark hours of the Beaujolais\u002Dsoaked pre\u002Ddawn, I see a collection of mildly amusing, occasionally painful series of embarrassments, misunderstandings and general wrong\u002Dplace, wrong\u002Dtime sort of moments. Which isn\u0027t to imply that my life is a British sitcom, just that I\u0027m not in a hurry to re\u002Dlive any of it. <a href="/2007/feb/03/everything-all-time/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_everything_all, 'click', function() { - openWin(c_everything_all,marker_everything_all); - }); - - - var marker_the_sun = new google.maps.Marker({ - position: new google.maps.LatLng(33.9751734061, -118.428872807), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_sun = '<div class="infowin"><h4>The Sun Came Up With No Conclusions<\/h4><span class="date blok">January 11, 2007 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/illuminatus.jpg" height="100" alt="The Sun Came Up With No Conclusions" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>\u0022And so it is that we, as men, do not exist until we do\u003B and then it is that we play with our world of existent things, and order and disorder them, and so it shall be that non\u002Dexistence shall take us back from existence and that nameless spirituality shall return to Void, like a tired child home from a very wild circus.\u0022 \u002D\u002D Robert Anton Wilson and Kerry Thornley. Good luck and Godspeed Mr. Wilson. <a href="/2007/jan/11/sun-came-no-conclusions/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_sun, 'click', function() { - openWin(c_the_sun,marker_the_sun); - }); - - - var marker_give_it = new google.maps.Marker({ - position: new google.maps.LatLng(33.9751956491, -118.42893718), - map: map, - shadow: shadow, - icon: image - }); - - var c_give_it = '<div class="infowin"><h4>Give It Up Or Turnit A Loose<\/h4><span class="date blok">December 25, 2006 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/jamesbrown.jpg" height="100" alt="Give It Up Or Turnit A Loose" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Traveling soul. Soul is not something out there or in you, it\u0027s the place where you meet the out there\u003B something very similar to what I think James Brown meant \u0026mdash\u003B a mixture of the secular and the spiritual, the profane and the sublime. <a href="/2006/dec/25/give-it-or-turnit-loose/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_give_it, 'click', function() { - openWin(c_give_it,marker_give_it); - }); - - - var marker_homeward = new google.maps.Marker({ - position: new google.maps.LatLng(33.9751600603, -118.42903374), - map: map, - shadow: shadow, - icon: image - }); - - var c_homeward = '<div class="infowin"><h4>Homeward<\/h4><span class="date blok">June 9, 2006 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/trappedmoth.jpg" height="100" alt="Homeward" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>New York, New York. John F Kennedy airport 1 am date unknown, sleepy looking customs guard stamps a passport without hardly looking at, without even checking to see where I had been. A light drizzle is falling outside and the subways extension to the terminal never looked so good. What is it like to be home? I don\u0027t know, I\u0027ll tell you when I get there. <a href="/2006/jun/09/homeward/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_homeward, 'click', function() { - openWin(c_homeward,marker_homeward); - }); - - - var marker_cadenza = new google.maps.Marker({ - position: new google.maps.LatLng(48.8634584438, 2.36108422246), - map: map, - shadow: shadow, - icon: image - }); - - var c_cadenza = '<div class="infowin"><h4>Cadenza<\/h4><span class="date blok">June 6, 2006 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/parisglow.jpg" height="100" alt="Cadenza" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Paris \u002D Outside it\u0027s raining. Beads of water form on the window in front of me. The glow of the unseen sun is fading behind midnight blue clouds and darkening sky. An old man in a butcher apron selling oysters under an awning smokes a cigarette and watches the mothers and children walking home with bags of groceries. <a href="/2006/jun/06/cadenza/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_cadenza, 'click', function() { - openWin(c_cadenza,marker_cadenza); - }); - - - var marker_i_dont = new google.maps.Marker({ - position: new google.maps.LatLng(48.2099677697, 16.3706481434), - map: map, - shadow: shadow, - icon: image - }); - - var c_i_dont = '<div class="infowin"><h4>I Don't Sleep I Dream<\/h4><span class="date blok">May 28, 2006 (Vienna, Austria)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/freudsoffice.jpg" height="100" alt="I Don't Sleep I Dream" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>How can Freud\u0027s former residence in Vienna lack a couch? The closest thing is up against the wall, behind a small writing desk in what was then the waiting room \u0026mdash\u003B a small divan where one might stare at the patternless ceiling until the patterns emerge as it were. “Tell me about it,” he began. <a href="/2006/may/28/i-dont-sleep-i-dream/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_i_dont, 'click', function() { - openWin(c_i_dont,marker_i_dont); - }); - - - var marker_unreflected = new google.maps.Marker({ - position: new google.maps.LatLng(48.2099677697, 16.3706481434), - map: map, - shadow: shadow, - icon: image - }); - - var c_unreflected = '<div class="infowin"><h4>Unreflected<\/h4><span class="date blok">May 27, 2006 (Vienna, Austria)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/selfportraitconvex.jpg" height="100" alt="Unreflected" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Kunsthistorisches Museum contains probably the best collection of art outside of France \u0026mdash\u003B Rubens, Rembrandt, Vermeer, Raphael, Velazquez, Bruegel and a certain Italian for whom I have a festering personal obsession, which shall be addressed shortly \u0026mdash\u003B and what\u0027s remarkable about this magnificent assemblage is that the vast majority of it was once the Hapsburg\u0027s private collection. <a href="/2006/may/27/unreflected/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_unreflected, 'click', function() { - openWin(c_unreflected,marker_unreflected); - }); - - - var marker_four_minutes = new google.maps.Marker({ - position: new google.maps.LatLng(50.0898463908, 14.418117998), - map: map, - shadow: shadow, - icon: image - }); - - var c_four_minutes = '<div class="infowin"><h4>Four Minutes Thirty-Three Seconds<\/h4><span class="date blok">May 26, 2006 (Prague, Czech Republic)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/wallofnames.jpg" height="100" alt="Four Minutes Thirty-Three Seconds" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Just north of Prague\u0027s old town square and east of the River Vltava is Josefov, the old Jewish quarter of Prague. The Pinkas Synagogue in Josefov is an unassuming pale, sand\u002Dcolored building with a slightly sunken entrance. Inside is a small alter and little else. The floor is bare\u003B there are no places for worshipers to sit. The synagogue is little more than walls. And on the walls inscribed in extremely small print are the names of the 77,297 Jewish citizens of Bohemia and Moravia who died in the Holocaust.\u000D\u000A <a href="/2006/may/26/four-minutes-thirty-three-seconds/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_four_minutes, 'click', function() { - openWin(c_four_minutes,marker_four_minutes); - }); - - - var marker_inside_and = new google.maps.Marker({ - position: new google.maps.LatLng(48.810530578, 14.3173527698), - map: map, - shadow: shadow, - icon: image - }); - - var c_inside_and = '<div class="infowin"><h4>Inside and Out<\/h4><span class="date blok">May 25, 2006 (Cesky Krumlov, Czech Republic)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/krumlovcastleatnight.jpg" height="100" alt="Inside and Out" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Chasing Egon Schiele: The attention to detail that makes the difference between a building and work of art was everywhere in Cesky Krumlov, from the delicate pink and red complements of a fine dovetailed corner, to the white plaster and oak beams of the Egon Schiele museum, which, despite geometric differences, looked not unlike the Globe Theatre in London. <a href="/2006/may/25/inside-and-out/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_inside_and, 'click', function() { - openWin(c_inside_and,marker_inside_and); - }); - - - var marker_the_king = new google.maps.Marker({ - position: new google.maps.LatLng(46.3652099826, 14.1099429111), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_king = '<div class="infowin"><h4>The King of Carrot Flowers Part Two<\/h4><span class="date blok">May 22, 2006 (Bled, Slovenia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/sloveniachurch.jpg" height="100" alt="The King of Carrot Flowers Part Two" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>There is a roughly 200km loop of road that leads northwest out of Bled, through a pass in the Julian Alps and then down the other side, twisting and winding back toward Bled by way of craggy canyons, small hamlets and crystalline rivers. We set out sometime after breakfast. <a href="/2006/may/22/king-carrot-flowers-part-two/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_king, 'click', function() { - openWin(c_the_king,marker_the_king); - }); - - - var marker_ghost = new google.maps.Marker({ - position: new google.maps.LatLng(46.0508598563, 14.5067489127), - map: map, - shadow: shadow, - icon: image - }); - - var c_ghost = '<div class="infowin"><h4>Ghost<\/h4><span class="date blok">May 19, 2006 (Ljubljana, Slovenia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/trogirnight.jpg" height="100" alt="Ghost" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Like Dubrovnik, Trogir is a walled city of roughly Venetian vintage, but Trogir\u0027s wall has largely crumbled away or been removed. Still, it has the gorgeous narrow cobblestone streets, arched doorways and towering forts that give all Dalmatian towns their Rapunzel\u002Dlike fairly tale quality. <a href="/2006/may/19/ghost/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_ghost, 'click', function() { - openWin(c_ghost,marker_ghost); - }); - - - var marker_feel_good = new google.maps.Marker({ - position: new google.maps.LatLng(42.6413383843, 18.1090521787), - map: map, - shadow: shadow, - icon: image - }); - - var c_feel_good = '<div class="infowin"><h4>Feel Good Lost<\/h4><span class="date blok">May 17, 2006 (Dubrovnik, Croatia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/dubrovnik.jpg" height="100" alt="Feel Good Lost" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Dubrovnik, Croatia was heavily shelled during the Bosnian conflict and roughly 65 percent of its buildings were hit, built for the most part you\u0027d never know it. Most of the buildings date from about 1468, though some were destroyed in the great earthquake of 1667, still, by and large, the city looks as it did in the fifteenth century. <a href="/2006/may/17/feel-good-lost/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_feel_good, 'click', function() { - openWin(c_feel_good,marker_feel_good); - }); - - - var marker_blue_milk = new google.maps.Marker({ - position: new google.maps.LatLng(42.6413383843, 18.1090521787), - map: map, - shadow: shadow, - icon: image - }); - - var c_blue_milk = '<div class="infowin"><h4>Blue Milk<\/h4><span class="date blok">May 15, 2006 (Lake Plitvice, Croatia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/plitvice.jpg" height="100" alt="Blue Milk" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>It\u0027s hard to understand, standing on the banks of such crystalline, cerulean lakes, whose dazzling colors come from the mineral rich silt runoff of glaciers, that the largest European conflict since world war two began here, at Like Plitvice Croatia. But indeed this is where the first shots were fired on Easter Sunday in 1991 and the first casualty was a park policeman. <a href="/2006/may/15/blue-milk/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_blue_milk, 'click', function() { - openWin(c_blue_milk,marker_blue_milk); - }); - - - var marker_refracted_light = new google.maps.Marker({ - position: new google.maps.LatLng(47.4838008623, 19.0621376011), - map: map, - shadow: shadow, - icon: image - }); - - var c_refracted_light = '<div class="infowin"><h4>Refracted Light and Grace<\/h4><span class="date blok">May 10, 2006 (Budapest, Hungary)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/castlehillbuda.jpg" height="100" alt="Refracted Light and Grace" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Evening, after dinner, outside on the balcony, smoking cigarettes and contemplating the nightscape of Buda\u0027s Castle Hill rising up out of its own golden reflection in the shimmering Danube waters. The drone of car horns in the distance and the electric tram squealing as it pulls out of the station below on the river a boat slowly churns upstream... <a href="/2006/may/10/refracted-light-and-grace/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_refracted_light, 'click', function() { - openWin(c_refracted_light,marker_refracted_light); - }); - - - var marker_london_calling = new google.maps.Marker({ - position: new google.maps.LatLng(51.5511920468, -0.14955997465), - map: map, - shadow: shadow, - icon: image - }); - - var c_london_calling = '<div class="infowin"><h4>London Calling<\/h4><span class="date blok">May 9, 2006 (London, United Kingdom)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/londonthames.jpg" height="100" alt="London Calling" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>London: The British don\u0027t want me \u002D\u002D no money, no proof I\u0027m leaving and no real reason for coming, good lord, I must be a vagabond, up to no good, surely. Eventually the customs agent relents and lets me in, a favor I repay by nearly burning down one of London\u0027s bigger parks. Seriously. <a href="/2006/may/09/london-calling/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_london_calling, 'click', function() { - openWin(c_london_calling,marker_london_calling); - }); - - - var marker_closing_time = new google.maps.Marker({ - position: new google.maps.LatLng(7.0586452367, 98.5398101669), - map: map, - shadow: shadow, - icon: image - }); - - var c_closing_time = '<div class="infowin"><h4>Closing Time<\/h4><span class="date blok">April 30, 2006 (Trang, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/thailandtrain.jpg" height="100" alt="Closing Time" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Headed back to Europe: I started to write a bit of reminiscence, trying to remember the highlights of my time in Asia before I return to the west, but about halfway through I kept thinking of a popular Buddhist saying \u0026mdash\u003B be here now. Most of these dispatches are written in past tense, but this time I want to simply be here now. This moment, on this train. This is the last time I\u0027ll post something from Southeast Asia. <a href="/2006/apr/30/closing-time/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_closing_time, 'click', function() { - openWin(c_closing_time,marker_closing_time); - }); - - - var marker_beginning_of = new google.maps.Marker({ - position: new google.maps.LatLng(7.40906927581, 99.207916246), - map: map, - shadow: shadow, - icon: image - }); - - var c_beginning_of = '<div class="infowin"><h4>Beginning of the End<\/h4><span class="date blok">April 21, 2006 (Koh Kradan, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/kokradan.jpg" height="100" alt="Beginning of the End" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I wasn\u0027t expecting much from Ko Kradan, but in the end I discovered a slice of Thailand the way it\u0027s often describe by wistful hippies who first came here twenty years ago. Tong and Ngu and the rest of the Thais working at Paradise Lost were the nicest people I met in Thailand and Wally was by far the most laid back farang I\u0027ve come across. I ended up staying on Ko Kradan for the remainder of my time in the south.\u000D\u000A <a href="/2006/apr/21/beginning-end/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_beginning_of, 'click', function() { - openWin(c_beginning_of,marker_beginning_of); - }); - - - var marker_going_down = new google.maps.Marker({ - position: new google.maps.LatLng(7.73582685702, 98.7787628036), - map: map, - shadow: shadow, - icon: image - }); - - var c_going_down = '<div class="infowin"><h4>Going Down South<\/h4><span class="date blok">April 10, 2006 (Koh Phi Phi, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/thailandleahkate.jpg" height="100" alt="Going Down South" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Phi Phi Island Resort, where some friends were staying, is nestled on the leeward shore of Koh Phi Phi Island and posts a private beach, beautiful reef, fancy swimming pools and rooms with real sheets. Unheard of. I sauntered in a day early, acted like I owned the place, rented snorkel gear, charged it to a random room number and spent the afternoon on the reef. If only I could have put it on the Underhill\u0027s credit card. <a href="/2006/apr/10/going-down-south/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_going_down, 'click', function() { - openWin(c_going_down,marker_going_down); - }); - - - var marker_the_book = new google.maps.Marker({ - position: new google.maps.LatLng(10.6262758656, 103.499450669), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_book = '<div class="infowin"><h4>The Book of Right On<\/h4><span class="date blok">March 30, 2006 (Sinoukville, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/goodbyes.jpg" height="100" alt="The Book of Right On" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The next day we continued on to Sinoukville which is Cambodia\u0027s attempt at a seaside resort. Combining the essential elements of Goa and Thailand, Sinoukville is a pleasant, if somewhat hippy\u002Doriented, travelers haven. We rented Honda Dreams and cruised down the coast to deserted white sand beaches, thatched huts serving noodles and rice, where we watched sunsets and dodged rain storms. <a href="/2006/mar/30/book-right/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_book, 'click', function() { - openWin(c_the_book,marker_the_book); - }); - - - var marker_midnight_in = new google.maps.Marker({ - position: new google.maps.LatLng(10.4382670171, 104.323253617), - map: map, - shadow: shadow, - icon: image - }); - - var c_midnight_in = '<div class="infowin"><h4>Midnight in a Perfect World<\/h4><span class="date blok">March 26, 2006 (Death Island, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/deathisland.jpg" height="100" alt="Midnight in a Perfect World" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Death Island, as Rob nicknamed it, was just what I needed. The first day we sat down for lunch and ordered crab\u003B a boy in his underwear proceeded to run out of the kitchen, swam out in the ocean and began unloading crabs from a trap into a bucket. It doesn\u0027t get much fresher than that. Throw in a nice beach, some cheap bungalows and you\u0027re away. <a href="/2006/mar/26/midnight-perfect-world/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_midnight_in, 'click', function() { - openWin(c_midnight_in,marker_midnight_in); - }); - - - var marker_angkor_wat = new google.maps.Marker({ - position: new google.maps.LatLng(13.4978081268, 103.892898545), - map: map, - shadow: shadow, - icon: image - }); - - var c_angkor_wat = '<div class="infowin"><h4>Angkor Wat<\/h4><span class="date blok">March 21, 2006 (Angkor Wat, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/angkorwat.jpg" height="100" alt="Angkor Wat" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Roughly half a million people a year visit Angkor Wat. The first evening we decided to see just how tourist\u002Dfilled Angkor was by heading to the most popular sunset temple, Phnom Bakheng, to watch the sunset. And there were a lot of tourists. Thousands of them. And that was just at one temple. Thus was hatched the plan: see Angkor in the heat of the day. Yes it will be hot. Hot hot hot. Fucking hot. But hopefully empty. <a href="/2006/mar/21/angkor-wat/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_angkor_wat, 'click', function() { - openWin(c_angkor_wat,marker_angkor_wat); - }); - - - var marker_wait_til = new google.maps.Marker({ - position: new google.maps.LatLng(13.3612287241, 103.861484513), - map: map, - shadow: shadow, - icon: image - }); - - var c_wait_til = '<div class="infowin"><h4>...Wait 'til it Blows<\/h4><span class="date blok">March 18, 2006 (Seam Reap, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/landmines.jpg" height="100" alt="...Wait 'til it Blows" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>One the things I may have failed to mention thus far in my Cambodia reportage is that this was/is one of the most heavily mined areas in the world. You might think that removing landmines involves sophisticated technology of the sort you see in BBC documentaries on Bosnia, but here in Cambodia landmine removal is most often handled by the technological marvel of southeast Asia \u0026mdash\u003B the bamboo stick. <a href="/2006/mar/18/wait-til-it-blows/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_wait_til, 'click', function() { - openWin(c_wait_til,marker_wait_til); - }); - - - var marker_beginning_to = new google.maps.Marker({ - position: new google.maps.LatLng(12.8211748485, 104.040527329), - map: map, - shadow: shadow, - icon: image - }); - - var c_beginning_to = '<div class="infowin"><h4>Beginning to See the Light<\/h4><span class="date blok">March 16, 2006 (Battambang, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/floatingvillage.jpg" height="100" alt="Beginning to See the Light" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Surprisingly, a floating village is not that different than a village on the land. There are the same stores, the computer repair shop, the grocers, the petrol station, the temple, the dance hall and all the other things that makeup a town. I could even say with some authority that the town is laid out in streets, watery pathways that form nearly perfect lines. <a href="/2006/mar/16/beginning-see-light/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_beginning_to, 'click', function() { - openWin(c_beginning_to,marker_beginning_to); - }); - - - var marker_blood_on = new google.maps.Marker({ - position: new google.maps.LatLng(11.5659755905, 104.927501664), - map: map, - shadow: shadow, - icon: image - }); - - var c_blood_on = '<div class="infowin"><h4>Blood on the Tracks<\/h4><span class="date blok">March 14, 2006 (Phenom Phen, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/killingfields.jpg" height="100" alt="Blood on the Tracks" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>As I mentioned in the last entry I came down with a bit of a fever for a few days. This was accompanied by what we in the group have come to term, for lack of a nicer, but equally descriptive phrase \u0026mdash\u003B pissing out the ass. It\u0027s not a pretty picture. Nor is it a pleasant experience, and consequently I don\u0027t have a real clear recollection of the journey from Ban Lung to Kratie or from Kratie out to Sen Monoron. <a href="/2006/mar/14/blood-tracks/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_blood_on, 'click', function() { - openWin(c_blood_on,marker_blood_on); - }); - - - var marker_ticket_to = new google.maps.Marker({ - position: new google.maps.LatLng(13.7345492998, 106.979413018), - map: map, - shadow: shadow, - icon: image - }); - - var c_ticket_to = '<div class="infowin"><h4>Ticket To Ride<\/h4><span class="date blok">March 7, 2006 (Ban Lung, Cambodia)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/hondadream.jpg" height="100" alt="Ticket To Ride" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I can\u0027t see. My eyebrows are orange with dust. I cannot see them, but I know they must be\u003B they were yesterday. Every now and then when her legs clench down on my hips or her fingernails dig into my shoulders, I remember Debi is behind me and I am more or less responsible for not killing both of us. <a href="/2006/mar/07/ticket-ride/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_ticket_to, 'click', function() { - openWin(c_ticket_to,marker_ticket_to); - }); - - - var marker_little_corner = new google.maps.Marker({ - position: new google.maps.LatLng(14.1309158427, 105.837821946), - map: map, - shadow: shadow, - icon: image - }); - - var c_little_corner = '<div class="infowin"><h4>Little Corner of the World<\/h4><span class="date blok">February 28, 2006 (Four Thousand Islands, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/siphondon.jpg" height="100" alt="Little Corner of the World" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>It\u0027s difficult to explain but the further south you go in Laos the more relaxed life becomes. Since life in the north is not exactly high stress, by the time we arrived in the four thousand Islands we had to check our pulse periodically to ensure that time was in fact still moving forward. <a href="/2006/feb/28/little-corner-world/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_little_corner, 'click', function() { - openWin(c_little_corner,marker_little_corner); - }); - - - var marker_can8217t_get = new google.maps.Marker({ - position: new google.maps.LatLng(14.8060855248, 106.836891159), - map: map, - shadow: shadow, - icon: image - }); - - var c_can8217t_get = '<div class="infowin"><h4>Can&#8217;t Get There From Here<\/h4><span class="date blok">February 24, 2006 (Attapeu, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/attapeulight.jpg" height="100" alt="Can&#8217;t Get There From Here" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The most magical light in Laos lives on the Bolevan Plateau. For some reason not many tourists seem to make it out to the Bolevan Plateau, in spite of the fact that the roads are quite good, transport runs regularly, the villages peaceful, even sleepy, little hamlets. In short, the Bolevan Plateau is wonderful, and not the least in part because no one else is there. <a href="/2006/feb/24/cant-get-there-here/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_can8217t_get, 'click', function() { - openWin(c_can8217t_get,marker_can8217t_get); - }); - - - var marker_safe_as = new google.maps.Marker({ - position: new google.maps.LatLng(14.6239495051, 106.575622544), - map: map, - shadow: shadow, - icon: image - }); - - var c_safe_as = '<div class="infowin"><h4>Safe as Milk<\/h4><span class="date blok">February 18, 2006 (Sekong, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/usbombs.jpg" height="100" alt="Safe as Milk" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>You would think, if you were the United States and you were illegally and unofficially bombing a foreign country you might not want to stamp \u0022US Bomb\u0022 on the side of your bombs, and yet there it was all over Laos: \u0022US Bomb.\u0022 Clearly somebody didn\u0027t think things all the way through, especially given that roughly one third of said bombs failed to explode. <a href="/2006/feb/18/safe-milk/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_safe_as, 'click', function() { - openWin(c_safe_as,marker_safe_as); - }); - - - var marker_everyday_the = new google.maps.Marker({ - position: new google.maps.LatLng(16.5604357571, 104.750261292), - map: map, - shadow: shadow, - icon: image - }); - - var c_everyday_the = '<div class="infowin"><h4>Everyday the Fourteenth<\/h4><span class="date blok">February 14, 2006 (Champasak, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/hinbunriver.jpg" height="100" alt="Everyday the Fourteenth" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>We piled four large bags, four daypacks and five people in a six meter dugout canoe. The boat was powered by the ever\u002Dpresent\u002Din\u002Dsoutheast\u002DAsia long tail motor which is essential a lawnmower engine with a three meter pole extending out of it to which a small propeller is attached \u0026mdash\u003B perfect for navigating shallow water. And by shallow I mean sometimes a mere inch between the hull and the riverbed. <a href="/2006/feb/14/everyday-fourteenth/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_everyday_the, 'click', function() { - openWin(c_everyday_the,marker_everyday_the); - }); - - - var marker_water_slides = new google.maps.Marker({ - position: new google.maps.LatLng(17.5131057154, 105.303955063), - map: map, - shadow: shadow, - icon: image - }); - - var c_water_slides = '<div class="infowin"><h4>Water Slides and Spirit Guides<\/h4><span class="date blok">February 10, 2006 (Champasak, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/konglorcave.jpg" height="100" alt="Water Slides and Spirit Guides" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The dramatic black karst limestone mountains ringing Ban Na Hin grew darker as the light faded. I was sitting alone on the back porch of our guesthouse watching the light slowly disappear from the bottoms of the clouds and wondering absently how many pages it would take to explain how I came to be in the tiny town of Ban Na Hin, or if such an explanation even really existed. <a href="/2006/feb/10/water-slides-and-spirit-guides/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_water_slides, 'click', function() { - openWin(c_water_slides,marker_water_slides); - }); - - - var marker_the_lovely = new google.maps.Marker({ - position: new google.maps.LatLng(18.9254486207, 102.437553392), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_lovely = '<div class="infowin"><h4>The Lovely Universe<\/h4><span class="date blok">February 4, 2006 (Vang Vieng, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/vangveing.jpg" height="100" alt="The Lovely Universe" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I would like to say that I have something memorable to write about Vang Vieng, but the truth is we mostly sat around doing very little, making new friends, drinking a beer around the fire and waiting out the Chinese new year celebrations, which meant none of us could get Cambodian visas until the following Monday. We were forced to relax beside the river for several more days than we intended. Yes friends, traveling is hard, but I do it for you. <a href="/2006/feb/04/lovely-universe/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_lovely, 'click', function() { - openWin(c_the_lovely,marker_the_lovely); - }); - - - var marker_i_used = new google.maps.Marker({ - position: new google.maps.LatLng(20.8536785547, 101.190948472), - map: map, - shadow: shadow, - icon: image - }); - - var c_i_used = '<div class="infowin"><h4>I Used to Fly Like Peter Pan<\/h4><span class="date blok">January 21, 2006 (Luang Nam Tha, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/gibbonexperience.jpg" height="100" alt="I Used to Fly Like Peter Pan" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The next time someone asks you, \u0026#8220\u003Bwould you like to live in a tree house and travel five hundred feet above the ground attached to a zip wire?\u0026#8221\u003B I highly suggest you say, \u0026#8220\u003Byes, where do a I sign up?\u0026#8221\u003B If you happen to be in Laos, try the Gibbon Experience. <a href="/2006/jan/21/i-used-fly-peter-pan/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_i_used, 'click', function() { - openWin(c_i_used,marker_i_used); - }); - - - var marker_hymn_of = new google.maps.Marker({ - position: new google.maps.LatLng(19.8274335101, 102.422790513), - map: map, - shadow: shadow, - icon: image - }); - - var c_hymn_of = '<div class="infowin"><h4>Hymn of the Big Wheel<\/h4><span class="date blok">January 19, 2006 (Luang Prabang, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/bluemilkwaterfall.jpg" height="100" alt="Hymn of the Big Wheel" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Jose Saramago writes in \u003Ccite\u003EThe Year of the Death of Ricardo Reis\u003C/cite\u003E that the gods \u0022journey like us in the river of things, differing from us only because we call them gods and sometimes believe in them.\u0022 Sitting in the middle of the river listening to the gurgle of water moving over stone and around trees I began to think that perhaps this is the sound of some lost language, a sound capable of creating mountains, valleys, estuaries, isthmuses and all the other forms around us, gurgling and sonorous but without clear meaning, shrouded in turquoise, a mystery through which we can move our sense of wonder intact. <a href="/2006/jan/19/hymn-big-wheel/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_hymn_of, 'click', function() { - openWin(c_hymn_of,marker_hymn_of); - }); - - - var marker_down_the = new google.maps.Marker({ - position: new google.maps.LatLng(19.8750644479, 102.131996141), - map: map, - shadow: shadow, - icon: image - }); - - var c_down_the = '<div class="infowin"><h4>Down the River<\/h4><span class="date blok">January 17, 2006 (Luang Prabang, Lao (PDR))<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/mekongslowboat.jpg" height="100" alt="Down the River" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Morning in Chiang Khong Thailand revealed itself as a foggy, and not a little mysterious, affair with the far shore of the Mekong, the Laos shore, almost completely hidden in a veil of mist. The first ferry crossed at eight and I was on it, looking to meet up with the slow boat to Luang Prabang. <a href="/2006/jan/17/down-river/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_down_the, 'click', function() { - openWin(c_down_the,marker_down_the); - }); - - - var marker_the_king = new google.maps.Marker({ - position: new google.maps.LatLng(19.3150313814, 98.8426208359), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_king = '<div class="infowin"><h4>The King of Carrot Flowers<\/h4><span class="date blok">January 17, 2006 (Doi Inthanan National Park, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/maesaorchids.jpg" height="100" alt="The King of Carrot Flowers" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The light outside the windows was still a pre\u002Ddawn inky blue when the freezing cold water hit my back. A cold shower at six thirty in the morning is infinitely more powerful, albeit not at long lasting, as a cup of coffee. After dropping my body temperature a few degrees and having no towel to dry off with, just a dirty shirt and ceaseless ceiling fan, a cup of tea seemed like a good idea so I stopped in at the restaurant downstairs and, after a cup of hot water with some Jasmine leaves swirling at the bottom of it, I climbed on my rental motorbike and set out for Doi Inthanan National Park. <a href="/2006/jan/17/king-carrot-flowers/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_king, 'click', function() { - openWin(c_the_king,marker_the_king); - }); - - - var marker_you_and = new google.maps.Marker({ - position: new google.maps.LatLng(18.7870423436, 98.9876746994), - map: map, - shadow: shadow, - icon: image - }); - - var c_you_and = '<div class="infowin"><h4>You and I Are Disappearing<\/h4><span class="date blok">January 11, 2006 (Chang Mai, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/changmaiumong.jpg" height="100" alt="You and I Are Disappearing" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The all night bus reached Chiang Mai well past dawn, the city already beginning to stir. I considered trying to nap, but in the end decided to explore the town. What better way to see Buddhist temples than in the dreamy fog of sleeplessness? Chiang Mai has over three hundred wats within the somewhat sprawling city limits, most of them reasonably modern and, in my opinion, not worth visiting. I narrowed the field to three, which I figured was a nice round one percent. \u000D\u000A <a href="/2006/jan/11/you-and-i-are-disappearing/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_you_and, 'click', function() { - openWin(c_you_and,marker_you_and); - }); - - - var marker_buddha_on = new google.maps.Marker({ - position: new google.maps.LatLng(13.7261281265, 100.547304139), - map: map, - shadow: shadow, - icon: image - }); - - var c_buddha_on = '<div class="infowin"><h4>Buddha on the Bounty<\/h4><span class="date blok">January 5, 2006 (Bangkok, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/jimthompsonhouse.jpg" height="100" alt="Buddha on the Bounty" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The house Jim Thompson left behind in Bangkok is gorgeous, but the real charm is the garden and its orchids. I wandered around the gardens which really aren\u0027t that large for some time and then found a bench near a collection of orchids, where I sat for the better part of an hour, occasionally taking a photograph or two, but mostly thinking about how human orchids are. <a href="/2006/jan/05/buddha-bounty/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_buddha_on, 'click', function() { - openWin(c_buddha_on,marker_buddha_on); - }); - - - var marker_brink_of = new google.maps.Marker({ - position: new google.maps.LatLng(13.7509217796, 100.543141351), - map: map, - shadow: shadow, - icon: image - }); - - var c_brink_of = '<div class="infowin"><h4>Brink of the Clouds<\/h4><span class="date blok">January 3, 2006 (Bangkok, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/bangkokfrombaiyoke.jpg" height="100" alt="Brink of the Clouds" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>\u0022The city is a cathedral\u0022 writes James Salter, \u0022its scent is dreams.\u0022 Salter may have been referring to New York, but his words ring true in Bangkok. And the best place to feel it at night is on the river or from the top of the Baiyoke Sky Hotel \u0026mdash\u003B where a circular, revolving observation deck offers 360\u0026deg\u003B views of the Bangkok nightscape. <a href="/2006/jan/03/brink-clouds/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_brink_of, 'click', function() { - openWin(c_brink_of,marker_brink_of); - }); - - - var marker_are_you = new google.maps.Marker({ - position: new google.maps.LatLng(13.7617909731, 100.493445382), - map: map, - shadow: shadow, - icon: image - }); - - var c_are_you = '<div class="infowin"><h4>Are You Amplified to Rock?<\/h4><span class="date blok">January 1, 2006 (Bangkok, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/bangkokriver.jpg" height="100" alt="Are You Amplified to Rock?" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>It\u0027s a new year, are you amplified to rock? Ready, set, go. <a href="/2006/jan/01/are-you-amplified-rock/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_are_you, 'click', function() { - openWin(c_are_you,marker_are_you); - }); - - - var marker_merry_christmas = new google.maps.Marker({ - position: new google.maps.LatLng(13.7617909731, 100.493445382), - map: map, - shadow: shadow, - icon: image - }); - - var c_merry_christmas = '<div class="infowin"><h4>Merry Christmas 2005<\/h4><span class="date blok">December 25, 2005 (Bangkok, Thailand)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/bangkokfort.jpg" height="100" alt="Merry Christmas 2005" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Seasons Greeting from luxagraf. I\u0027m in Bangkok, Thailand at the moment. I am taking a short break from traveling to do a little working so I don\u0027t have much to report. I\u0027ve seen the two big temples down in the Khaosan Rd area, but otherwise I\u0027ve been trying to live an ordinary life in Bangkok, if such a thing is possible. <a href="/2005/dec/25/merry-christmas-2005/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_merry_christmas, 'click', function() { - openWin(c_merry_christmas,marker_merry_christmas); - }); - - - var marker_sunset_over = new google.maps.Marker({ - position: new google.maps.LatLng(28.2104827779, 83.9582061651), - map: map, - shadow: shadow, - icon: image - }); - - var c_sunset_over = '<div class="infowin"><h4>Sunset Over the Himalayas<\/h4><span class="date blok">December 17, 2005 (Pokhara, Nepal)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/pokharaboat.jpg" height="100" alt="Sunset Over the Himalayas" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>After about forty\u002Dfive minutes of paddling I reached a point where the views of the Annapurna range were, in the words of an Englishman I met in Katmandu, \u0022gob smacking gorgeous.\u0022 I put down the paddle and moved to the center of the boat where the benches were wider and, using my bag a cushion, lay back against the gunwale and hung my feet over the opposite side so that they just skimmed the surface of the chilly water. <a href="/2005/dec/17/sunset-over-himalayas/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_sunset_over, 'click', function() { - openWin(c_sunset_over,marker_sunset_over); - }); - - - var marker_pashupatinath = new google.maps.Marker({ - position: new google.maps.LatLng(27.7105731557, 85.3485345722), - map: map, - shadow: shadow, - icon: image - }); - - var c_pashupatinath = '<div class="infowin"><h4>Pashupatinath<\/h4><span class="date blok">December 15, 2005 (Pashupatinath, Nepal)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/nepalburninggahts.jpg" height="100" alt="Pashupatinath" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Nestled on a hillside beside the Bagmati River, Pashupatinath is one of the holiest sites in the world for Hindus, second only to Varanasi in India. Pashupatinath consists of a large temple which is open only to Hindus, surrounded by a number of smaller shrines and then down on the banks of the Bagmati are the burning ghats where bodies are cremated. <a href="/2005/dec/15/pashupatinath/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_pashupatinath, 'click', function() { - openWin(c_pashupatinath,marker_pashupatinath); - }); - - - var marker_durbar_square = new google.maps.Marker({ - position: new google.maps.LatLng(27.7033636906, 85.3173780323), - map: map, - shadow: shadow, - icon: image - }); - - var c_durbar_square = '<div class="infowin"><h4>Durbar Square Kathmandu<\/h4><span class="date blok">December 15, 2005 (Kathmandu, Nepal)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/durbarsquare.jpg" height="100" alt="Durbar Square Kathmandu" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>After saturating myself with the streets of Thamel I went on a longer excursion down to Durbar Square to see the various pagodas, temples and the old palace. The palace itself no longer houses the King, but is still used for coronations and ceremonies and Durbar Square is still very much the hub of Katmandu. <a href="/2005/dec/15/durbar-square-kathmandu/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_durbar_square, 'click', function() { - openWin(c_durbar_square,marker_durbar_square); - }); - - - var marker_goodbye_india = new google.maps.Marker({ - position: new google.maps.LatLng(28.6418241967, 77.2109269988), - map: map, - shadow: shadow, - icon: image - }); - - var c_goodbye_india = '<div class="infowin"><h4>Goodbye India<\/h4><span class="date blok">December 10, 2005 (Delhi, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/indiadelhi.jpg" height="100" alt="Goodbye India" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I have taken almost 750 photos and traveled nearly 4000 km (2500 miles) in India, the vast majority of it by train. I have seen everything from depressing squalor to majestic palaces and yet I still feel as if I have hardly scratched the surface. I can\u0027t think of another and certainly have never been to a country with the kind of geographic and ethnic diversity of India. <a href="/2005/dec/10/goodbye-india/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_goodbye_india, 'click', function() { - openWin(c_goodbye_india,marker_goodbye_india); - }); - - - var marker_the_taj = new google.maps.Marker({ - position: new google.maps.LatLng(27.1728040126, 78.0417680632), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_taj = '<div class="infowin"><h4>The Taj Express<\/h4><span class="date blok">December 9, 2005 (Agra, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/tajmahal.jpg" height="100" alt="The Taj Express" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Taj Mahal is one of the Seven Wonders of the World, and, given the level of hype I was fully prepared to be underwhelmed, but I was wrong. I have never in my life seen anything so extravagant, elegant and colossal. The Taj Mahal seems mythically, spiritually, as well as architecturally, to have risen from nowhere, without equal or context. <a href="/2005/dec/09/taj-express/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_taj, 'click', function() { - openWin(c_the_taj,marker_the_taj); - }); - - - var marker_on_a = new google.maps.Marker({ - position: new google.maps.LatLng(27.0040787606, 70.8906555077), - map: map, - shadow: shadow, - icon: image - }); - - var c_on_a = '<div class="infowin"><h4>On a Camel With No Name<\/h4><span class="date blok">December 5, 2005 (Thar Desert, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/cameltrek.jpg" height="100" alt="On a Camel With No Name" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Thar Desert is a bewitching if stark place. It reminded me of areas of the Great Basin between Las Vegas and St. George, Utah. Twigging mesquite\u002Dlike trees, bluish gray bushes resembling creosote, a very large bush that resembled a Palo Verde tree and grew in impenetrable clumps, and, strangely, only one species of cactus and not a whole lot of them. <a href="/2005/dec/05/camel-no-name/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_on_a, 'click', function() { - openWin(c_on_a,marker_on_a); - }); - - - var marker_the_majestic = new google.maps.Marker({ - position: new google.maps.LatLng(26.2974163535, 73.0176687139), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_majestic = '<div class="infowin"><h4>The Majestic Fort<\/h4><span class="date blok">December 2, 2005 (Jodhpur, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/jodhpurfort.jpg" height="100" alt="The Majestic Fort" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The next day I hopped in a rickshaw and headed up to tour Meherangarh, or the Majestic Fort as it\u0027s known in English. As its English name indicates, it is indeed perched majestically atop the only hill around, and seems not so much built on a hill as to have naturally risen out the very rocks that form the mesa on which it rests. The outer wall encloses some of the sturdiest and most impressive ramparts I\u0027ve seen in India or anywhere else. <a href="/2005/dec/02/majestic-fort/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_majestic, 'click', function() { - openWin(c_the_majestic,marker_the_majestic); - }); - - - var marker_around_udaipur = new google.maps.Marker({ - position: new google.maps.LatLng(24.6676103687, 73.7848663227), - map: map, - shadow: shadow, - icon: image - }); - - var c_around_udaipur = '<div class="infowin"><h4>Around Udaipur<\/h4><span class="date blok">November 30, 2005 (Udiapur, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/shiplogram.jpg" height="100" alt="Around Udaipur" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Just out of Udaipur is a government sponsored \u0022artist colony\u0022 for various cultures from the five nearby states, Rajasthan, Gujarat, Karnataka, Goa and Madhya Pradesh. On one hand Shilpogram is a wonderful idea on the part of the government, but on the other hand the \u0022artists colony\u0022 is slightly creepy. Amidst displays of typical tribal life there were artists and craftsmen and women hawking their wares along with dancers and musicians performing traditional songs. The whole thing had the feel of a living museum, or, for the creepy angle \u0026mdash\u003B human zoo. <a href="/2005/nov/30/around-udaipur/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_around_udaipur, 'click', function() { - openWin(c_around_udaipur,marker_around_udaipur); - }); - - - var marker_the_monsoon = new google.maps.Marker({ - position: new google.maps.LatLng(24.6619943759, 73.6880493061), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_monsoon = '<div class="infowin"><h4>The Monsoon Palace<\/h4><span class="date blok">November 29, 2005 (Udiapur, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/monsoonpalace.jpg" height="100" alt="The Monsoon Palace" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>We started out in the early evening quickly leaving behind Udaipur and its increasing urban sprawl. The road to the Monsoon Palace passes through the Sajjan Garh Nature Preserve and there was a sudden and dramatic drop in temperature, but then the road climbed out of the hollow and the temperature jumped back up to comfortable as we began to climb the mountain in a series of hairpin switchbacks. As the sun slowly slunk behind the mountain range to the west the balconies and balustrades of the Monsoon Palace took on an increasingly orange hue. <a href="/2005/nov/29/monsoon-palace/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_monsoon, 'click', function() { - openWin(c_the_monsoon,marker_the_monsoon); - }); - - - var marker_the_city = new google.maps.Marker({ - position: new google.maps.LatLng(24.5913048792, 73.6931991475), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_city = '<div class="infowin"><h4>The City Palace<\/h4><span class="date blok">November 28, 2005 (Udiapur, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/citypalaceudaipur.jpg" height="100" alt="The City Palace" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I spent some time sitting in the inner gardens of the City Place, listening to rustling trees and the various guides bringing small groups of western and Indian tourists through the garden. In the center of the hanging gardens was the kings, extremely oversized bath, which reminded me of children\u0027s book that I once gave to a friend\u0027s daughter\u003B it was a massively oversized and lavishly illustrated book that told the story of a king who refused to get out of the bath and instead made his ministers, advisors, cooks and even his wife conduct business by getting in the bath with him. <a href="/2005/nov/28/city-palace/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_city, 'click', function() { - openWin(c_the_city,marker_the_city); - }); - - - var marker_living_in = new google.maps.Marker({ - position: new google.maps.LatLng(23.0096752856, 72.5623798269), - map: map, - shadow: shadow, - icon: image - }); - - var c_living_in = '<div class="infowin"><h4>Living in Airport Terminals<\/h4><span class="date blok">November 27, 2005 (Ahmedabad, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/ceilingfanindia.jpg" height="100" alt="Living in Airport Terminals" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Airport terminals are fast becoming my favorite part of traveling. When you stop and observe them closely as I have been forced to do on this trip, terminals are actually quite beautiful weird places. Terminals inhabit a unique space in the architecture of humanity, perhaps the strangest of all spaces we have created\u003B a space that is itself only a boundary that delineates the border between what was and what will be without leaving any space at all for what is. <a href="/2005/nov/27/living-airport-terminals/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_living_in, 'click', function() { - openWin(c_living_in,marker_living_in); - }); - - - var marker_anjuna_market = new google.maps.Marker({ - position: new google.maps.LatLng(15.5812894729, 73.7388610737), - map: map, - shadow: shadow, - icon: image - }); - - var c_anjuna_market = '<div class="infowin"><h4>Anjuna Market<\/h4><span class="date blok">November 23, 2005 (Anjuna Beach, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/anjunabeachmarket.jpg" height="100" alt="Anjuna Market" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Earlier today I caught a bus up to the Anjuna Flea Market and can now tell you for certain that old hippies do not die, they simply move to Goa. The flea market was quite a spectacle\u003B riots of color at every turn and more silver jewelry than you could shake a stick at. <a href="/2005/nov/23/anjuna-market/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_anjuna_market, 'click', function() { - openWin(c_anjuna_market,marker_anjuna_market); - }); - - - var marker_fish_story = new google.maps.Marker({ - position: new google.maps.LatLng(15.2772302271, 73.9154147999), - map: map, - shadow: shadow, - icon: image - }); - - var c_fish_story = '<div class="infowin"><h4>Fish Story<\/h4><span class="date blok">November 19, 2005 (Colva Beach, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/colvabeach.jpg" height="100" alt="Fish Story" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The Arabian Sea is warm and the sand sucks at your feet when you walk, schools of tiny fish dart and disappear into each receding wave. In the morning the water is nearly glassy and the beach slopes off so slowly one can walk out at least 200 meters and be only waist deep. <a href="/2005/nov/19/fish-story/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_fish_story, 'click', function() { - openWin(c_fish_story,marker_fish_story); - }); - - - var marker_the_backwaters = new google.maps.Marker({ - position: new google.maps.LatLng(9.95802997096, 76.253356923), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_backwaters = '<div class="infowin"><h4>The Backwaters of Kerala<\/h4><span class="date blok">November 14, 2005 (Kerala Backwaters, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/keralabackwater.jpg" height="100" alt="The Backwaters of Kerala" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The guide showed us Tamarind trees, coconut palms, lemon trees, vanilla vine, plantain trees and countless other shrubs and bushes whose names I have already forgotten. The most fascinating was a plant that produces a fruit something like a miniature mango that contains cyanide and which, as our guide informed us, is cultivated mainly to commit suicide with \u0026mdash\u003B as if it was no big deal and everyone is at least occasionally tempted to each the killer mango. <a href="/2005/nov/14/backwaters-kerala/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_backwaters, 'click', function() { - openWin(c_the_backwaters,marker_the_backwaters); - }); - - - var marker_vasco_de = new google.maps.Marker({ - position: new google.maps.LatLng(9.96437023104, 76.2409114732), - map: map, - shadow: shadow, - icon: image - }); - - var c_vasco_de = '<div class="infowin"><h4>Vasco de Gama Exhumed<\/h4><span class="date blok">November 10, 2005 (Fort Kochi, India)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/fortcochin.jpg" height="100" alt="Vasco de Gama Exhumed" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Fort Cochin is curious collision of cultures \u0026mdash\u003B Chinese, India and even Portuguese. Many of the obviously older buildings are of a distinctly Iberian\u002Dstyle \u0026mdash\u003B moss covered, adobe\u002Dcolored arches abound. There is graveyard just down the road with a tombstone that bears the name Vasco de Gama, who died and was buried here for fourteen years before being moved to Lisbon (there we go again, more Europeans digging up and moving the dead). <a href="/2005/nov/10/vasco-de-gama-exhumed/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_vasco_de, 'click', function() { - openWin(c_vasco_de,marker_vasco_de); - }); - - - var marker_riots_iraqi = new google.maps.Marker({ - position: new google.maps.LatLng(48.863514908, 2.36107349363), - map: map, - shadow: shadow, - icon: image - }); - - var c_riots_iraqi = '<div class="infowin"><h4>Riots, Iraqi Restaurants, Goodbye Seine<\/h4><span class="date blok">November 8, 2005 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/republique.jpg" height="100" alt="Riots, Iraqi Restaurants, Goodbye Seine" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Well it\u0027s my last night here in Paris and I\u0027ve chosen to return to the best restaurant we\u0027ve been to so far, an Iraqi restaurant in a Marais. I am using all my willpower right now to avoid having a political outburst re the quality of Iraqi food versus the intelligence of George Bush etc etc. I\u0027m traveling\u003B I don\u0027t want to get into politics except to say that my dislike for the current El Presidente was no small factor in my decision to go abroad. <a href="/2005/nov/08/riots-iraqi-restaurants-goodbye-seine/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_riots_iraqi, 'click', function() { - openWin(c_riots_iraqi,marker_riots_iraqi); - }); - - - var marker_bury_your = new google.maps.Marker({ - position: new google.maps.LatLng(48.8862365662, 2.34375715223), - map: map, - shadow: shadow, - icon: image - }); - - var c_bury_your = '<div class="infowin"><h4>Bury Your Dead<\/h4><span class="date blok">November 6, 2005 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/pariscatacombs.jpg" height="100" alt="Bury Your Dead" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I would like to say that the catacombs of Paris had some spectacular effect on me seeing that I strolled through human remains, skulls and femurs mainly, \u0022decoratively arranged,\u0022 but the truth is, after you get over the initial shock of seeing a skull, well, it turns out you can get adjusted to just about anything. Maybe that in and off itself is the scary part. <a href="/2005/nov/06/bury-your-dead/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_bury_your, 'click', function() { - openWin(c_bury_your,marker_bury_your); - }); - - - var marker_the_houses = new google.maps.Marker({ - position: new google.maps.LatLng(48.8640936621, 2.36156702009), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_houses = '<div class="infowin"><h4>The Houses We Live In<\/h4><span class="date blok">November 1, 2005 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/pariscityscape.jpg" height="100" alt="The Houses We Live In" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I\u0027ve been thinking the last couple of days about something Bill\u0027s dad said to me before I left. I\u0027m paraphrasing here since I don\u0027t remember the exact phrasing he used, but something to the effect of \u0022people are essentially the same everywhere, they just build their houses differently.\u0022 Indeed, Parisian architecture is completely unlike anything in America. Perhaps more than any other single element, architecture reflects culture and the ideas of the people that make up culture. <a href="/2005/nov/01/houses-we-live/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_houses, 'click', function() { - openWin(c_the_houses,marker_the_houses); - }); - - - var marker_sainte_chapelle = new google.maps.Marker({ - position: new google.maps.LatLng(48.8555669485, 2.34525918928), - map: map, - shadow: shadow, - icon: image - }); - - var c_sainte_chapelle = '<div class="infowin"><h4>Sainte Chapelle<\/h4><span class="date blok">October 28, 2005 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/saintechapelle.jpg" height="100" alt="Sainte Chapelle" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Sainte Chapelle was interesting to see after the modern, conceptual art stuff at the Pompidou, rather than simple stained glass, Sainte Chapelle felt quite conceptual. In a sense the entire Bible (i.e. all history from that perspective) is unfolding simultaneously, quite a so\u002Dcalled post\u002Dmodern idea if you think about it. And yet it was conceived and executed over 800 years ago. Kind of kicks a lot pretentious modern art in its collective ass. <a href="/2005/oct/28/sainte-chapelle/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_sainte_chapelle, 'click', function() { - openWin(c_sainte_chapelle,marker_sainte_chapelle); - }); - - - var marker_living_in = new google.maps.Marker({ - position: new google.maps.LatLng(48.8641642414, 2.36178159681), - map: map, - shadow: shadow, - icon: image - }); - - var c_living_in = '<div class="infowin"><h4>Living in a Railway Car<\/h4><span class="date blok">October 24, 2005 (Paris, France)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/letour.jpg" height="100" alt="Living in a Railway Car" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>This French apartment is more like a railway sleeper car than apartment proper. Maybe fifteen feet long and only three feet wide at the ceiling. More like five feet wide at the floor, but, because it\u0027s an attic, the outer wall slopes in and you lose two feet by the time you get to the ceiling. It\u0027s narrow enough that you can\u0027t pass another body when you walk to length of it. <a href="/2005/oct/24/living-railway-car/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_living_in, 'click', function() { - openWin(c_living_in,marker_living_in); - }); - - - var marker_twenty_more = new google.maps.Marker({ - position: new google.maps.LatLng(33.6333266453, -117.903020366), - map: map, - shadow: shadow, - icon: image - }); - - var c_twenty_more = '<div class="infowin"><h4>Twenty More Minutes to Go<\/h4><span class="date blok">October 20, 2005 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/nightswings.jpg" height="100" alt="Twenty More Minutes to Go" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Well it\u0027s the night before I leave. I just got done pacing around the driveway of my parents house smoking cigarettes\u0026#8230\u003B nervously? Excitedly? Restlessly? A bit of all of those I suppose. I walk across the street, over the drainage ditch and head for the swing set at the park. Right now I\u0027m swinging in a park in Costa Mesa California. Tomorrow France. Weird. [Photo to the right, \u003Ca href\u003D\u0022http://www.flickr.com/photos/scarin/53961434/\u0022\u003Evia Flickr\u003C/a\u003E] <a href="/2005/oct/20/twenty-more-minutes-go/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_twenty_more, 'click', function() { - openWin(c_twenty_more,marker_twenty_more); - }); - - - var marker_travel_tips = new google.maps.Marker({ - position: new google.maps.LatLng(33.6320939072, -117.901239379), - map: map, - shadow: shadow, - icon: image - }); - - var c_travel_tips = '<div class="infowin"><h4>Travel Tips and Resources<\/h4><span class="date blok">October 19, 2005 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/travelgear.jpg" height="100" alt="Travel Tips and Resources" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>An overview of the things you might want to bring on an extended trip, as well as some tips and recommendations on things like visas and vaccinations. The part that was most helpful for me was learning what I \u003Cem\u003Edidn\u0027t\u003C/em\u003E need to bring \u0026mdash\u003B as it turns out, quite a bit. Nowadays my pack is much smaller and lighter. <a href="/2005/oct/19/tips-and-resources/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_travel_tips, 'click', function() { - openWin(c_travel_tips,marker_travel_tips); - }); - - - var marker_the_new = new google.maps.Marker({ - position: new google.maps.LatLng(33.6321475049, -117.901067717), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_new = '<div class="infowin"><h4>The New Luddites<\/h4><span class="date blok">October 8, 2005 (Los Angeles, California)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/books.jpg" height="100" alt="The New Luddites" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>An older, non\u002Dtravel piece about Google\u0027s plan to scan all the world\u0027s books and Luddite\u002Dlike response from many authors. Let\u0027s see, someone wants to make your book easier to find, searchable and indexable and you\u0027re opposed to it? You\u0027re a fucking idiot. <a href="/2005/oct/08/new-luddites/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_new, 'click', function() { - openWin(c_the_new,marker_the_new); - }); - - - var marker_one_nation = new google.maps.Marker({ - position: new google.maps.LatLng(42.3225404908, -72.6280403036), - map: map, - shadow: shadow, - icon: image - }); - - var c_one_nation = '<div class="infowin"><h4>One Nation Under a Groove<\/h4><span class="date blok">March 25, 2005 (Northampton, Massachusetts)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/ipod.jpg" height="100" alt="One Nation Under a Groove" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>The sky is falling! The iPod! It\u0027s ruining our culture! Or, uh, maybe it\u0027s just like the Walkman, but better. And since, so far as I can tell, the world did not collapse with the introduction of the Walkman and headphones, it probably isn\u0027t going to fall apart just because the storage format for our music has changed. [Photo to the right via \u003Ca href\u003D\u0022http://flickr.com/photos/rogpool/2960735485/\u0022\u003EFlickr\u003C/a\u003E] <a href="/2005/mar/25/one-nation-under-groove/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_one_nation, 'click', function() { - openWin(c_one_nation,marker_one_nation); - }); - - - var marker_farewell_mr = new google.maps.Marker({ - position: new google.maps.LatLng(42.3226356812, -72.6279544729), - map: map, - shadow: shadow, - icon: image - }); - - var c_farewell_mr = '<div class="infowin"><h4>Farewell Mr. Hunter S Thompson<\/h4><span class="date blok">February 24, 2005 (Northampton, Massachusetts)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/thompson.jpg" height="100" alt="Farewell Mr. Hunter S Thompson" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Hunter S. Thompson departs on a journey to the western lands. Thompson\u0027s \u003Cem\u003EFear and Loathing in Las Vegas\u003C/em\u003E delivered the penultimate eulogy for the dreams of the 1960\u0027s, one that mourned, but also tried to lay the empty idealism to rest. <a href="/2005/feb/24/farewell-mr-hunter-s-thompson/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_farewell_mr, 'click', function() { - openWin(c_farewell_mr,marker_farewell_mr); - }); - - - var marker_the_art = new google.maps.Marker({ - position: new google.maps.LatLng(42.3224770304, -72.628340711), - map: map, - shadow: shadow, - icon: image - }); - - var c_the_art = '<div class="infowin"><h4>The Art of the Essay<\/h4><span class="date blok">October 10, 2004 (Northampton, Massachusetts)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/essay.jpg" height="100" alt="The Art of the Essay" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>I generally ignore internet debates, they never go anywhere, so why bother. But we all have our weak points and when programmer Paul Graham posted what might be the dumbest essay on writing that\u0027s ever been written, I just couldn\u0027t help myuself. <a href="/2004/oct/10/art-essay/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_the_art, 'click', function() { - openWin(c_the_art,marker_the_art); - }); - - - var marker_farewell_mr = new google.maps.Marker({ - position: new google.maps.LatLng(42.3225087606, -72.6280403036), - map: map, - shadow: shadow, - icon: image - }); - - var c_farewell_mr = '<div class="infowin"><h4>Farewell Mr. Cash<\/h4><span class="date blok">September 12, 2003 (Northampton, Massachusetts)<\/span><p><img src="http://127.0.0.1:8000/images/post-thumbs/2008/cash.jpg" height="100" alt="Farewell Mr. Cash" style="float: left; border: #000 10px solid; margin-right: 8px; margin-bottom: 4px; height: 100px;" \/>Johnny Cash heads for the western lands. <a href="/2003/sep/12/farewell-mr-cash/">Read it »<\/a><\/p><\/div>'; - - google.maps.event.addListener(marker_farewell_mr, 'click', function() { - openWin(c_farewell_mr,marker_farewell_mr); - }); - - - // create an empty info window instance, set max width - var infowindow = new google.maps.InfoWindow({ - content: ' ', - maxWidth: 400 - }); - //function to handle click event and display single info window - function openWin(content, marker) { - infowindow.close(); - infowindow.setContent(content); - infowindow.open(map,marker); - }; - - -} |