2009 (old posts, page 10)

GeoJSON Data URIs

In a previous post I described how I was getting the geographic context for a web page from an alternate JSON representation. It was pointed out that this requires an extra request for what could be a fairly small bit of data. Via Sam Ruby, I see that I can head off the extra request and yet keep my links with a Data URI from RFC 2397. Instead of

<link type="application/json" href="http://example.com/where"/>

where

$ curl http://example.com/where
{"type": "Point", "coordinates": [0.0, 0.0]}

One can encode location directly in a URI using escaped GeoJSON:

<link href="data:application/json,%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D"/>

Andrew Turner came up with something like this in an old GeoJSON discussion, but none of us thought of Data URIs.

A round trip through the encoding specified in RFC 2397 is simple with Python:

>>> from urllib import quote, unquote
>>> from json import dumps, loads # or simplejson
>>> where = {'type': 'Point', 'coordinates': (0.0, 0.0)}
>>> json = dumps(where)
>>> json
'{"type": "Point", "coordinates": [0.0, 0.0]}'
>>> uri = 'data:application/json,' + quote(json)
>>> uri
'data:application/json,%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D'
>>> data = uri.split(',')[-1]
>>> data
'%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D'
>>> json = unquote(data)
>>> json
'{"type": "Point", "coordinates": [0.0, 0.0]}'
>>> where = loads(json)
>>> where
{'type': 'Point', 'coordinates': [0.0, 0.0]}

and a Data URI is similarly easy to handle with Javascript:

>>> uri = 'data:application/json,%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D'
"data:application/json,%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D"
>>> data = uri.split(',').pop()
"%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D"
>>> json = unescape(data)
"{"type": "Point", "coordinates": [0.0, 0.0]}"

The missing piece is a link relation type that specifies what such a Data URI means in the context of the document. Something like rel="where" [where-link-relation-type]:

<link rel="where" href="data:application/json,..."/>
%7B%22type%22%3A%20%22Point%22%2C%20%22coordinates%22%3A%20%5B0.0%2C%200.0%5D%7D"/>

Atom threading

Reading about Salmon this morning and its use of Atom Threading (RFC 4685) reminded me that before anybody outside of Google had ever heard about Wave, let alone place-locating Wave bots (see slide 15) that would geocode and annotate places that you mention, I'd suggested that you might arrange for an app to walk your posts and leave geographic annotation in comments using the same Atom extension. Salmon might make the arrangement unnecessary: geotagging might just come knocking.

Lessons of standardization

Stefan Tilkov tipped me off to Adam Bosworth's lessons learned in creating standards:

  1. Keep the standard as simple and stupid as possible.
  2. The data being exchanged should be human readable and easy to understand.
  3. Standards work best when they are focused.
  4. Standards should have precise encodings.
  5. Always have real implementations that are actually being used as part of design of any standard.
  6. Put in hysteresis for the unexpected.
  7. Make the spec itself free, public on the web, and include lots of simple examples on the web site.

Bosworth goes into each of these in detail, I've only reproduced the first sentence.

I feel like we achieved these well for GeoJSON. It's a simple, readable, precise, and stupid format. For example, coordinates are represented in [x, y] pairs instead of arrays of x and arrays of y like you'd implement for software optimized for performance. Stupid in that sense, but much more transparent. The lack of feature attribute schemas and feature classes also seems pretty stupid to some people, but that's fine.

GeoJSON focuses on serialization of basic GIS features. It doesn't create any new protocols. It doesn't require any new abstract models of the world. The spec is plain HTML with links so you can refer to sections when you throw the book at someone, short and sweet, has clear examples, and no bizarre click-through license agreement. Not everything in GeoJSON had a real implementation before publication, but most elements of it had several independent implementations.

Update (2009-11-10): Dale Lutz has blogged this too.

Python GIS features

How do you access the attributes and geometry of features in your favorite Python and GIS environment? Let's imagine we're interested in the state_name attribute from a US States data set and compare. I'll employ the iterable adapters I introduced in a previous post to simplify the code.

ESRI:

for row in ESRICollection(rows):
    state_name = row.state_name
    geom = row.shape

That's not bad at all, although there's potential for name collisions.

FME:

for f in FMECollection(features):
    state_name = f.getAttribute('state_name')
    geom_type = f.getGeometryType()
    geom_coords = f.getCoordinates()

Evil or not, getters and setters aren't needed in Python.

OGR (earlier than 1.5):

for f in OGRCollection(layer):
    state_name = f.GetFieldAsString('state_name')
    geom = f.GetGeometryRef()

    # Don't forget to free the memory allocated for the feature!
    f.Destroy()

Destroy!

OGR (current, also known as osgeo.ogr):

for f in layer:
    state_name = f.state_name
    geom = f.geometry()

Howard Butler has made osgeo.ogr much more peaceful.

QGIS:

for f in QGISCollection(provider):
    attribs = f.attributeMap()
    state_name = attribs['state_name'].toString()
    geom = f.geometry()

A mapping of attributes is design that I like, but it could be more user friendly.

5 different environments, 5 different ways.

Ideally, feature attributes (not to be confused with Python object attributes) are accessed through a mapping attribute to prevent collisions in the feature's own namespace, and the feature geometry is simply another attribute:

for f in features:
    # A dict has all kinds of nice built-in features, like
    attribute_items = f.properties.items() # [('state_name', 'Utah'), ...]
    assert 'state_name' in f.properties # True

    state_name = f.properties['state_name']
    geom = f.geometry

Looks a bit like GeoJSON? Yes, it does.

Why learn to program?

Why would a geographer need to learn to program? I don't see that the motivation is very different from than that of a historian. To paraphrase from William J. Turkel and Alan MacEachern's The Programming Historian:

We think that at least some [analysts] really will need to learn how to program. Think of it like learning how to cook. You may prefer fresh pasta to boxed macaroni and cheese, but if you don't want to be stuck eating the latter, you have to learn to cook or pay someone else to do it for you. Learning how to program is like learning to cook in another way: it can be a very gradual process. One day you're sitting there eating your macaroni and cheese and you decide to liven it up with a bit of Tabasco, Dijon mustard or Worcestershire sauce. Bingo! Soon you're putting grated cheddar in, too. You discover that the ingredients that you bought for one dish can be remixed to make another. You begin to linger in the spice aisle at the grocery store. People start buying you cookware. You get to the point where you're willing and able to experiment with recipes. Although few people become master chefs, many learn to cook well enough to meet their own needs.

If you don't program, your [business] process will always be at the mercy of those who do.

I've substituted analyst for historian, and business for research. There's nothing like a cooking analogy, is there?

Comments

Re: Why learn to program?

Author: Kirk

mmmm, is that spaghetti code I smell ?

Re: Why learn to program?

Author: Mick

Awesome paraphrase. Spot on, for those that have the passion to and wisdom to program. I recall the charge in the eighties. But the admin of whatever organization you're really cooking for has to contribute to the bill at the grocery store. And they often see the coal being shoveled into the oven to make SOS as good enough efficency. Too much organizational communication is somehow equivalent to too many cooks in the kitchen. They're paying for new kitchen tools that are not exploited, except to support the way their fore-fathers cooked. I guess if the heat's too hot, consider getting out of the kitchen. Support the cooks with the underemployed passion to be chefs. They're the ones shoveling coal and daydreaming of how much more efficient things could be, and how many satisfied customers they'd see. Consider the guy hanging in the dungeon in Monty Python's Life of Brian; "A great culture, the Romans", (obviously paraphrased). He dreamed of building a GIS in a target-rich environment, and kept smiling.

Re: Why learn to program?

Author: gilles

Funny, I teach programming basics to GIS students and the very first program example I gave them was actually a cooking recipe. It is quite useful to explain them what is an algorithm and that there's nothing to be afraid of.

Tomorrow is the last session of this course, I'll mention your cooking analogy.

Re: Why learn to program?

Author: Sean

I'm curious, Gilles: How soon do you introduce proper programming techniques? I've been arguing in my blog that we shouldn't teach beginners to make spaghetti code and getting some strong contrary opinions.

Iterators, again

If your favorite Python GIS environment doesn't provide iterators, you can easily implement adapters like these.

ESRI (ArcGIS 9.3):

class ESRICollection(object):

    def __init__(self, context):
        self.context = context

    def __iter__(self):
        row = self.context.next()
        while row:
            yield row
            row = self.context.next()
        raise StopIteration

More or less as done here. Usage:

>>> for f in ESRICollection(rows):
...     # work with f

QGIS:

class QGISCollection(object):

    def __init__(self, context):
        self.context = context

    def __iter__(self):
        feat = QgsFeature()
        while provider.getNextFeature(feat):
            yield feat
            feat = QgsFeature()
        raise StopIteration

Returning new objects from the iterator rather than returning the same object with new values heads off unpleasant surprises. Usage:

>>> for f in QGISCollection(provider):
...     # work with f

OGR (earlier than 1.5):

class OGRCollection(object):

    def __init__(self, context):
        self.context = context

    def __iter__(self):
      feature = layer.GetNextFeature()
      while feature:
          yield feature
          feature = layer.GetNextFeature()
      raise StopIteration

Usage:

>>> for f in OGRCollection(layer):
...     # work with f

Notice a big difference between yield and return: the former gives a value, but execution continues after the statement rather than breaking as with the latter.

Comments

Re: Iterators, again

Author: Eric Wolf

Thanks Sean! I've been wondering how to overload iteration in Python. Haven't bothered to find out on my own. Should make stupid ESRI cursor code a little cleaner.

Iterators

How do you get a GIS feature from a Python collection/layer/provider/thingy? Let's look at 4 different popular GIS scripting environments.

ESRI (ArcGIS 9.3):

row = rows.next()
while row:
    # work with row
    ...
    row = rows.next()

FME:

self.my_collection_count = len(self.my_feature_collection)
for i in range(self.my_collection_count):
    self.my_feature_part = self.my_feature_collection[i]

    # work with self.my_feature_part
    ...

Note: don't use instance attributes like that. Use local variables.

QGIS:

feat = QgsFeature()
while provider.getNextFeature(feat):
    # work with feat
    ...

The oddball.

OGR (earlier than 1.5):

feature = layer.GetNextFeature()
while feature:
    # work with feature
    ...
    feature.Destroy()
    feature = layer.GetNextFeature()

Destroy!

4 different environments, 4 different ways, and none of them the natural Python way. There's one obviously right way to do it for Python, and that's the way that it's done in ogr.py versions >= 1.5, and how it's done in WorldMill. GeoDjango, too.

for f in layer:
    # work with f
    ...

where layer is among other things a generator that provides the iterator protocol just like Python strings, lists, and files do. It has a next method that yields a value, or raises a StopIteration exception when there are no more values. The advantages:

  • Clarity: it's agonizingly clear. More clear to a non-programmer, in my opinion, than the other alternatives. For each feature in the set: do something. And then forget about the feature and move on to the next.
  • Less error-prone: even a non-programmer can't screw up that one line of code any worse than to get a standard, understandable Python NameError, TypeError, or SyntaxError.
  • Standardization: core Python modules such as itertools and many other useful add-on packages reward you for using the iterator protocol.

Comments

Re: Iterators

Author: few people

I think ESRI has done away with the while: row = rows.next() stuff at 9.3.1: http://webhelp.esri.com/arcgisdesktop/9.3/index.cfm?TopicName=FeatureSets_and_RecordSets

Re: Iterators

Author: Mateusz Loskot

Sean, brilliant post! Finally, a voice clarity, correcness, robustness, discoverability, transparency, genericity, expressiveness, flexibility, consistency, meaning...

Quality Matters

Any of the interested parties are considering it in the same manner?

I'm afraid, that's the end of story.

Re: Iterators

Author: Michael Weisman

Just a quick clarification, FME does support iterators. The following snippet will iterate over the parts of an aggregate feature and extract the coordinates for each part:

for f in feature_collection:
  coords = f.getCoordinates()
  for m in coords:
    x = m[0]
    y = m[1]

Re: Iterators

Author: Mateusz Loskot

And a bit of clarification from me, I was referring to the Open Source projects mentioned above. Unfortunately, SWIG is in the house over there.

Re: Iterators

Author: Sean

Great, Michael. I simply didn't find examples of iterators employed by FME users. I haven't seen examples of programmers using Howard Butler's neater ogr.py API either.

QGIS

Author: Martin Dobias

Thanks for the nice article. Recently I've been thinking about adding a more pythonic way of accessing features in PyQGIS... currently it's just a crude wrapper around c++ api. So thanks for the inspiration :)

Re: Iterators

Author: Howard Butler

ogr.py is not mine. ogr.py is Frank's API ;), but I think I added support for this at GDAL 1.6. I guess we haven't advertised it so well.

for feature in layer:
    # COLUMN_NAME must be normalized to all caps due to some
    # data sources not caring.
    print feature.COLUMN_NAME

Python idioms for GIS Education

I'd like to see GIS students taught to program in Python using Python idioms, not Avenue idioms. I hate to pick on Utah State's GIS Programming with Python just because of its popularity, but it contains some good introductory code that can be easily tuned up to teach even better Python GIS programming skills. For example, let's look at Lesson 5: Writing/Reading Text Files, Python Lists & Dictionaries, part 5a:

# Author:  John Lowry
# Date: Dec. 21, 2007
# Purpose: Lesson 5a: Use split and Write to a text file
#############################################################

#Import modules
import string

# Open a new text file to write to
outFile = open(r"C:\john\TeachingGIS\WILD6900_ArcGISPython\Lesson5_results\write_example.txt", "w")

# Make a string variable of featureclass names
fcString = "nests1990.shp,nests1995.shp,nests2000.shp"

# Make a list variable from the string variable using the split method, then print
fcList = fcString.split(",")

# Write each item in the list to a separate line the output file
outFile.write(fcList[0]+ "\n")
outFile.write(fcList[1]+ "\n")
outFile.write(fcList[2]+ "\n")

# Close the files
outFile.close()

There are 3 defects in that code:

  • It imports the string module but never uses anything from it. You should almost always use string object methods anyway.
  • It presumes knowledge of the number of items in the comma-separated string, specifically that it is at least 3.
  • It needlessly concatenates newlines to items before writing.

The equivalent code, with none of those defects, looks like this:

outFile = open('/tmp/out.txt', 'w')
for item in fcString.split(','):
    outFile.write(item)
    outFile.write('\n')
outFile.close()

In Python 2.6 you can use a with statement to make it even more compact. The file closes itself at the end of the block. And you can use file.writelines to write the data and newline in one go. It's more efficient to pass it a tuple than to pass it a list.

with open('/tmp/out.txt', 'w') as f:
    for item in fcString.split(','):
        f.writelines((item, '\n'))

In the next part of the lesson, 5b, we see:

# Author:  John Lowry
# Date: Dec. 21, 2007
# Purpose: Lesson 5:  Reading a and writing a textfile
#############################################################

# Open the text file in read mode
inFile = open(r"C:\john\TeachingGIS\WILD6900_ArcGISPython\Lesson5\nests2005_coords.csv", "r")

# Open a new text file to write to
outFile = open(r"C:\john\TeachingGIS\WILD6900_ArcGISPython\Lesson5_results\nests2005_format.txt", "w")

# Read entire file and print one line at a time
for line in inFile.readlines():
    nestList = line.split(",")
    id = nestList[0]
    cnd = nestList[1]
    x = nestList[2]
    y = nestList[3]
    outFile.write("Siteid: " + id + "\n")
    outFile.write("Condition: " + cnd + "\n")
    outFile.write("X Coordinate: " + x + "\n")
    outFile.write("Y Coordinate: " + y + "\n")
    outFile.write("\n")

# Close the files
inFile.close()
outFile.close()

The more effective version of the looping block is this:

for line in inFile:
    outFile.write(
      'Siteid: %s\nCondition: %s\nX Coordinate: %s\nY Coordinate: %s\n\n' \
      % tuple(line.split(','))
      )

String formatting is more efficient than string concatenation (or not -- see the update below) and you can avoid needless variable assignments by using the split results directly.

I blogged before about how smelly the ArcGIS scripting cursor syntax was. I hear it's better now, but you can still see the old style in the USU course code.

Update (2009-10-21): Here's my benchmark script. I'm isolating just the inner part of the loop and focusing just on the extra assignments and file writes.

import timeit

# Sample input line
line = '1,good,433207.8362,4518107.044'

# A file-like object
class MockFile(object):
    def write(self, line):
        pass

outFile = MockFile()

# GIS Style programming. Assignment to intermediate variables
# and each written separately.

s1 = """\
nestList = line.split(',')
id = nestList[0]
cnd = nestList[1]
x = nestList[2]
y = nestList[3]
outFile.write(id)
outFile.write(cnd)
outFile.write(x)
outFile.write(y)
"""

t1 = timeit.Timer(
    stmt=s1,
    setup='from __main__ import line, outFile'
    )
print "GIS style"
print "%.2f usec/pass" % t1.timeit()
print

# Idiomatic Python. No intermediate variables and all written as
# a group

s2 = """\
outFile.write(''.join(line.split(',')))
"""

t2 = timeit.Timer(
    stmt=s2,
    setup='from __main__ import line, outFile'
    )
print "Idiomatic Python"
print "%.2f usec/pass" % t2.timeit()
print

The results:

$ python benchmarks.py
GIS style
2.07 usec/pass

Idiomatic Python
1.29 usec/pass

Someone else has looked at string performance more closely than I, and it looks like I'm wrong. On my Python 2.6, too, concatenation wins over formatting. Use of join is faster than concatenation for lists longer than 1000 items or so.

Comments

Re: Python idioms for GIS Education

Author: Eric Wolf

Sean - you obviously haven't tried teaching Python to Geographers. I find myself explaining over and over again how

with

works to grad students who've been writing Python code regularly for their advisors. And as for breaking out the

write

statements, my friend who regularly TA's the graduate Python class at CU-Boulder and I had a contest to see which of us could write the program for a lab in the fewest number of lines. The end result: combining program functionality into fewer lines of code destroys the readability and maintainability of the code. In the example above, it's pretty clear that the output spans multiple lines because the code spans multiple lines. But your rewrite requires that the student remember that /n means "start a new line".

And as for dropping the ".readlines()", I'd rather see the student specify the action directly rather than assuming it "just happens". For people who work in multiple languages, over use of "magic variables" or "magic functions" only makes the code harder to understand.

So here's the big question: Are your code rewrites significantly more efficient than the original in terms of execution?

Re: Python idioms for GIS Education

Author: Regina

Eric,

Honestly -- you can't say the first example is better than the second. I don't care what language you program in, the first code snippet is fraught with flaws.

Coming from a SQL/VB.NET/PHP/C# mindset, that Sean's example is much more understandable and clearly better.

Now as for the second example, I see your point that someone new to Python may be a bit at a disadvantage understanding this -- just like anyone seeing SQL for th first time would be disadvantaged seeing a join statement. That doesn't mean I should stop writing join statements since that is the strength of the language.

To some extent I would argue though that a fundamental reason aside from libraries available for it, are the succintness of its idioms. If you are going to write Python code, write Python code like a Python programmer. Anything else would just be insulting.

Re: Python idioms for GIS Education

Author: Sean

If you write slow code, you get a slow program. If you make a lot of needless assignments, you get a big, slow, program. It's not just about succinctness of the source. The Python idioms are usually backed up by optimized code and execute faster than pseudocode. How much faster? I'll benchmark it.

My first two examples are much more readable and maintainable than the original. That's a long string template in the last, granted, but there are constructions that can make it more readable. Python's triple-quoted text, for example:

template = '''\
Siteid: %s
Condition: %s
X Coordinate: %s
Y Coordinate: %s
'''

for line in inFile:
    outFile.write(template % tuple(line.split(',')))

I've taught Python to geographers a few times. I even remember being thanked and told that the lesson helped. Most of them were open source nuts and hackers at heart, it's true.

Re: Python idioms for GIS Education

Author: Clemens

Does your second suggestion really work? I'm still on Python 2.5 and I need to convert the result of the split to tuple in order to avoid "TypeError: not enough arguments for format string".

Re: Python idioms for GIS Education

Author: Sean

Indeed. Thanks, Clemens. I've corrected it.

Re: Python idioms for GIS Education

Author: Sean

I've previously encountered the opinion that non-idiomatic code is better for GIS, but I think it's misguided. Write Ruby like it is intended to be written. Write Python like it is intended to be written. One such reference for Python I've found is David Goodger's Code Like a Pythonista: Idiomatic Python. I'd love to see this near the top of Python GIS programming course reading lists.

It's not as if, to use a software-carpentry analogy, USU students are being taught to drive nails with the claw end of the hammer. The course is pretty good. But it's not quite teaching students how to drive nails with the least number of blows.

Re: Python idioms for GIS Education

Author: Chris

Valid points, Sean.

But I thought I'd point out that most of the students that take this course know absolutely nothing about programming. It's hard enough getting basic concepts across -- I wouldn't want to try to explain the string formatting to them when some of them are lucky to even get stuff to print out right. Also, John (the guy who wrote this code) is not a programmer. But he likes tinkering with code occasionally and thought it would be fun to teach a class. Which was fine by me, because it meant I got to teach the open source part of the course. And I'm sure there are problems with my code, too...

Re: Python idioms for GIS Education

Author: Eric Wolf

As Chris said, the students encountered in geography programs (including grad students) commonly know absolutely nothing about programming. Extending this comment:

1. These students made it to college (even grad school), they aren't stupid... but

2. They arrived at that level without knowing how to program.

So, why is it that intelligent people can reach such a high level of education without learning to program? Is it because they weren't exposed to programming in elementary school (maybe)? Or is it that these people aren't drawn programming (likely)?

In the case of people not having exposure, some of these people pick up programming quite quickly. They seem to be successes and are the type of student who appreciate learning how to optimize code (for speed, clarity, etc). But the other case of people, they don't need to learn to optimize code. They have an approach to problem solving that works - but doesn't necessarily fit with programming. They likely need to write a few scripts to automate geoprocessing for their problems. This second class of students are well-understood and targeted by ESRI. Hence Avenue!

On the positive side, these students aren't going to develop large programs in Python. So a little code bloat isn't going to bother them. They are also comparing their scripts to how well the same methods run in the ArcToolbox. The speed bar is set pretty low.

I really appreciate your examples. I would include them in a text book for Geography students - but I would present the clunky Avenue-style code first. For the students who do want to make their code more efficient, they can look at the refined code. But I sure as heck wouldn't start off trying to teach geography students how to right optimal Python code.

Unwords

I've learned that in German, there's a name for a disastrous string of characters, such as "REST endpoint", that insults human dignity: unwort. Yes, "REST endpoint" is two words in English, but it would be one in German. There's even an "Unword of the Year" website devoted to these entities (in German only). The level of negative meaning in "REST API" is almost as high.

Via @xrotwang.

Comments

Re: Unwords

Author: James Fee

OK, so what is RESTful in French?

Re: Unwords

Author: Sean

In French, the mec in the stock photo recliner would be having a sieste tranquille. Or, il glande. Depends on your frame of reference. French systems architects write "REST". What would a French standards body write? I don't know. Maybe "transport d'état aux representations"?

My point is that "REST endpoint" and "REST API" are unterms with negative meaning. Protracted use will probably result in brain damage.

BTW, loved your geodata site scraping anecdote. That takes me back.

Old Europe exhibit at ISAW

http://farm3.static.flickr.com/2432/4025718700_8fe6b90044_o.jpg

Check this out if you're in New York City this winter. It looks fascinating:

The Lost World of Old Europe brings to the United States for the first time more than 160 objects recovered by archaeologists from the graves, towns, and villages of Old Europe, a cycle of related cultures that achieved a precocious peak of sophistication and creativity in what is now southeastern Europe between 5000 and 4000 BC, and then mysteriously collapsed by 3500 BC. Long before Egypt or Mesopotamia rose to an equivalent level of achievement, Old Europe was among the most sophisticated places that humans inhabited. Some of its towns grew to city-like sizes. Potters developed striking designs, and the ubiquitous goddess figurines found in houses and shrines have triggered intense debates about womens roles in Old European society. Old European copper-smiths were, in their day, the most advanced metal artisans in the world. Their intense interest in acquiring copper, gold, Aegean shells, and other rare valuables created networks of negotiation that reached surprisingly far, permitting some of their chiefs to be buried with pounds of gold and copper in funerals without parallel in the Near East or Egypt at the time. The exhibition, arranged through loan agreements with 20 museums in three countries (Romania, The Republic of Bulgaria and the Republic of Moldova), brings the exuberant art, enigmatic goddess cults, and precocious metal ornaments and weapons of Old Europe to American audiences.