Smathermather's Weblog

Remote Sensing, GIS, Ecology, and Oddball Techniques

Posts Tagged ‘Triggers’

PostgreSQL and Fuzzy Hashes– a natural combo?

Posted by smathermather on May 20, 2011

A while back, I had the idea of creating a hash of geometry in order to create a unique constraint on geometry to avoid duplicate geometries in a PostGIS table. It works. It’s slow, but it works. I haven’t used it since.

What I really wanted to do though was be able to detect if geometries were similar as well (not just detect identical geometries), to test if geometries are homologous.  Wouldn’t it be nice to know how similar two shapes are?

A couple of nights ago, I was introduced to a technique that may just do that– fuzzy hashes.  Now a hash is a clever way to convert any and everything (in a computer), regardless of size, into the same size number, say 128-byte integer which is unique to the particular input.  This has all sorts of uses, from identifying viruses, to indexing information, to ensuring that when you download something from the internet, you are getting what you thought you were downloading in an unmodified form.  The unique property of a hash is that, make a small change to the input, and the output hash is completely different from the hash of unchanged input.  If you want to know if two things are similar, a hash won’t help.  In fact, hashes of bad files are used in forensics, and work-arounds to hash-based forensics can be as simple as changing a single byte in a file.

Enter fuzzy hashes.  In short, fuzzy hashes break up a file into logical pieces or blocks, hash each block, and then similarity between files can be determined block-by-block.  This is useful for file forensics, anti-virus, determining the origins of nefarious code, detecting plagiarism etc.  In long, well… just read this.  Also, check out ssdeep.

The question is, can we use this the geographic sphere.  How would this be useful in, e.g. PostGIS?

Let’s give it a try:

Line geometry-- original

If we convert our purple line above to text in PostGIS:

SELECT ST_AsEWKT(the_geom) FROM trail_test;

we get something like this:

"SRID=3734;MULTILINESTRING((2179985.47870642 567446.852705703 1030.80067171231 -1.79769313486232e+308,2179982.97870642 567445.252427514 1030.79452346621 -1.79769313486232e+308,2179980.47870642 567443.652149326 1030.74616711558 -1.79769313486232e+308,2179977.97870642 567442.051871137 1030.61640858078 -1.79769313486232e+308,2179975.47870642 567440.451592949 1030.50568889286

-1.79769313486232e+308,2179794.15034441 566393.137591605 1051.18052872389 -1.79769313486232e+308,2179794.04164876 566390.637591605 1051.46559305229 -1.79769313486232e+308,2179793.93295311 566388.137591605 1051.74478920181 -1.79769313486232e+308,2179793.82425746 566385.637591605 1052.01504940301 -1.79769313486232e+308,2179793.73503707 566383.585522703 1052.23441454273 -1.79769313486232e+308))"

Which fuzzy hashed looks like this:

# ssdeep -b temp
ssdeep,1.0--blocksize:hash:hash,filename
384:RpLQqsmoBbzjvPW7KEhoAyoGchYqpmeYOB7LCfCMJrOevT1WLwR7UyYIU:7Eq5oBbzjvPWuAyoGcSqIUNLCfCMJ6eq,"temp"

If we change some vertices and rerun, but this time change the ssdeep command to tell us similarity:

Original Geometry

Modified Geometry


Create a hash of the first geometry and write it to file:
# ssdeep -b temp > temphash

then to compare the hash of the second file:
# ssdeep -bm temphash temp1
temp1 matches temp (99)

Not surprisingly, they are 99% similar.
Now this is a painful way to do this comparison. However, imagine a function in the PostGIS suite wherein you could do a geometry-by-geometry similarity comparison for tables. Imagine combining that analysis with a snap-to-grid function to further generalize it’s applicability.

Any thoughts on uses? I know I’d use it to determine whether two data sources were related, i.e. were modified from the same parent, but I’m guessing there are some other more subtle applications.

Posted in PostGIS | Tagged: , , , , | 2 Comments »

pgagent — when triggers are too much

Posted by smathermather on July 8, 2010

This will be one of my shorter posts. pgadmin III allows for the development of scheduled tasks that run on your PostgreSQL database. As my PostGIS triggers have gotten more complicated, and since they run per transaction to keep ACID compliance, a big mess can be made by just updating a dataset (this is what vacuum is good for), and the time for inserts can be astronomical. Take, e.g. my previous post. Each record insert, because of the trigger, takes about 30 seconds to complete. That’s way to slow, especially if I want updates to happen during the business day. Enter pgagent.

pgagent has been well covered by other’s blogs, so I’ll post two links that were helpful to me for administering it in Windows:

http://www.mkyong.com/database/how-to-install-pgagent-on-windows-postgresql-job-scheduler/

http://www.postgresonline.com/journal/index.php?/archives/19-Setting-up-PgAgent-and-Doing-Scheduled-Backups.html

Posted in Database, PostGIS, SQL | Tagged: , , , , | 1 Comment »

Multi-ring buffers in PostGIS

Posted by smathermather on May 26, 2010


I maintain a lot of versions of our exterior property boundary layer for our park system for different purposes, e.g. this post.  What I’d like to include in this automatically managed dataset is a multi-ring buffer version of the dataset for the purpose of a careful mask with fade (also useful for showing coastal vignettes— oops, ESRI link), e.g.:

So, first we buffer:

And discover we have to union to get the result we want (dissolve in ESRI terms):

Now, I want rings, so I subtract an outer and inter buffer with a ST_Difference:

Perform this a few more times:

And drop it into my trigger statement so I never have to worry about it again:

DROP TRIGGER dissolver ON base.reservation_boundaries_public_private_cm;

CREATE OR REPLACE FUNCTION dissolve_bounds() RETURNS TRIGGER AS $dissolver$

BEGIN
IF(TG_OP='INSERT') THEN

TRUNCATE base.reservation_boundaries_public_private_cm_dissolved;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved
SELECT res, ST_Union(the_geom)
AS the_geom
FROM base.reservation_boundaries_public_private_cm
GROUP BY res;

TRUNCATE base.reservation_boundaries_cm_dissolved;

INSERT INTO base.reservation_boundaries_cm_dissolved;
SELECT res, ST_Union(the_geom)
AS the_geom
FROM base.reservation_boundaries_public_private_cm
WHERE access = 1
GROUP BY res;

TRUNCATE base.reservation_boundaries_cm;

INSERT INTO base.reservation_boundaries_cm
SELECT res, the_geom
FROM base.reservation_boundaries_public_private_cm
WHERE access = 1;

TRUNCATE base.reservation_boundaries_private_cm;

INSERT INTO base.reservation_boundaries_private_cm
SELECT res, the_geom
FROM base.reservation_boundaries_public_private_cm
WHERE access = 0;

TRUNCATE base.reservation_boundaries_public_private_cm_dissolved_mask;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask
SELECT
ST_Difference(
ST_GeomFromText('POLYGON((2000000 860000, 2500000 860000, 2500000 460000, 2000000 460000,2000000 860000))', 102722),
ST_Union(ST_Buffer(a.the_geom, 0))
)
FROM base.reservation_boundaries_public_private_cm AS a;

TRUNCATE base.reservation_boundaries_public_private_cm_dissolved_mask_gradien;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 0, 50,
ST_Difference(
ST_GeomFromText('POLYGON((2000000 860000, 2500000 860000, 2500000 460000, 2000000 460000,2000000 860000))', 102722),
ST_Union(ST_Buffer(a.the_geom, 300))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 300, 50,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 300)),
ST_Union(ST_Buffer(a.the_geom, 275))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 275, 54,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 275)),
ST_Union(ST_Buffer(a.the_geom, 250))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 250, 58,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 250)),
ST_Union(ST_Buffer(a.the_geom, 225))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 225, 62,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 225)),
ST_Union(ST_Buffer(a.the_geom, 200))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 200, 66,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 200)),
ST_Union(ST_Buffer(a.the_geom, 175))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 175, 70,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 175)),
ST_Union(ST_Buffer(a.the_geom, 150))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 150, 74,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 150)),
ST_Union(ST_Buffer(a.the_geom, 125))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 125, 78,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 125)),
ST_Union(ST_Buffer(a.the_geom, 100))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 100, 82,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 100)),
ST_Union(ST_Buffer(a.the_geom, 75))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 75, 86,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 75)),
ST_Union(ST_Buffer(a.the_geom, 50))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 50, 90,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 50)),
ST_Union(ST_Buffer(a.the_geom, 25))
)
FROM base.reservation_boundaries_public_private_cm AS a;

INSERT INTO base.reservation_boundaries_public_private_cm_dissolved_mask_gradien
SELECT 25, 94,
ST_Difference(
ST_Union(ST_Buffer(a.the_geom, 25)),
ST_Union(ST_Buffer(a.the_geom, 0))
)
FROM base.reservation_boundaries_public_private_cm AS a;

END IF;
RETURN NEW;
END;

$dissolver$ LANGUAGE plpgsql;

CREATE TRIGGER dissolver
AFTER INSERT ON base.reservation_boundaries_public_private_cm
FOR EACH STATEMENT EXECUTE PROCEDURE dissolve_bounds();

Posted in Database, PostGIS, SQL | Tagged: , , , | 1 Comment »

Trigger Speeeeed, or triggers and data maintenance in PostGIS

Posted by smathermather on April 21, 2010

I have completed 4 out of 6 of my triggers for maintaining boundary data in PostGIS.  This is a follow-up to this post.  If you haven’t read my PostGIS trigger post, follow this link and come back.  I’ll wait.  Go on.

Back?  OK.  So I’ve implemented my property boundary triggers.  I need about 6-7 different versions of our boundary for cartographic reasons– some which are dissolved (ST_Union) based on one set of attributes, some based on another, etc..  Needless to say, I just want to modify one version, and have the other versions automagically be created.  This is where PostGIS functions and triggers come in.  Anything you can conceive of doing with the hundreds of PostGIS spatial functions you could run within a trigger.

First, as always, testing.  Each time I update the record (can’t figure out how to do triggers once per transaction in PostgreSQL, so this runs with each record insert) I want to take my boundary and create a dissolved copy of it in another table.  Naively, I tried to remove and recreate the table first:

DROP TRIGGER dissolver ON base.reservation_boundaries_public_private_cm;

CREATE OR REPLACE FUNCTION dissolve_bounds() RETURNS TRIGGER AS $dissolver$

BEGIN
 IF(TG_OP='INSERT') THEN

 DROP TABLE IF EXISTS base.reservation_boundaries_public_private_cm_dissolved;

 CREATE TABLE base.reservation_boundaries_public_private_cm_dissolved
 AS
 SELECT res, ST_Union(the_geom)
 AS the_geom
 FROM base.reservation_boundaries_public_private_cm
 GROUP BY res;

 ALTER TABLE base.reservation_boundaries_public_private_cm_dissolved ADD COLUMN gid BIGSERIAL;
 ALTER TABLE base.reservation_boundaries_public_private_cm_dissolved ADD PRIMARY KEY (gid);
 ALTER TABLE base.reservation_boundaries_public_private_cm_dissolved ADD CONSTRAINT enforce_dims_the_geom CHECK (ndims(the_geom) = 2);
 ALTER TABLE base.reservation_boundaries_public_private_cm_dissolved ADD CONSTRAINT enforce_srid_the_geom CHECK (srid(the_geom) = 102722);
 GRANT SELECT ON TABLE base.reservation_boundaries_public_private_cm_dissolved TO low_level_access;
 COMMENT ON TABLE base.reservation_boundaries_public_private_cm_dissolved IS 'Base boundary layer, with public and private boundaries, dissolved (CM)';

 END IF;
 RETURN NEW;
END;

$dissolver$ LANGUAGE plpgsql;

CREATE TRIGGER dissolver
 AFTER INSERT ON base.reservation_boundaries_public_private_cm
 FOR EACH STATEMENT EXECUTE PROCEDURE dissolve_bounds();

It works, but it is a little slow.  It takes about 50 seconds to complete the transaction.  So, when I wrote this I was a fundamental SQL nube.  Now, still nubish, I realize that dropping and recreating the table is quite inefficient.  It’s much better to remove the contents of the table, and insert new records (here is just the modified code between IF() THEN and END IF;).

 DELETE FROM base.reservation_boundaries_public_private_cm_dissolved;

 INSERT INTO base.reservation_boundaries_public_private_cm_dissolved
  SELECT res, ST_Union(the_geom)
   AS the_geom
   FROM base.reservation_boundaries_public_private_cm
   GROUP BY res;

Moderately faster– runs in about 30 seconds.

To create all the tables, I have a few functions run in the trigger.  Remarkably, this doesn’t take much longer than just modifying one table within the trigger, or about 30 seconds:

DROP TRIGGER dissolver ON base.reservation_boundaries_public_private_cm;

CREATE OR REPLACE FUNCTION dissolve_bounds() RETURNS TRIGGER AS $dissolver$

BEGIN
 IF(TG_OP='INSERT') THEN

 DELETE FROM base.reservation_boundaries_public_private_cm_dissolved;

 INSERT INTO base.reservation_boundaries_public_private_cm_dissolved
  SELECT res, ST_Union(the_geom)
   AS the_geom
   FROM base.reservation_boundaries_public_private_cm
   GROUP BY res;

 DELETE FROM base.reservation_boundaries_cm_dissolved;

 INSERT INTO base.reservation_boundaries_cm_dissolved;
 SELECT res, ST_Union(the_geom)
 AS the_geom
 FROM base.reservation_boundaries_public_private_cm
 WHERE access = 1
 GROUP BY res;

 DELETE FROM base.reservation_boundaries_cm;

 INSERT INTO base.reservation_boundaries_cm
 SELECT res, the_geom
 FROM base.reservation_boundaries_public_private_cm
 WHERE access = 1;

 DELETE FROM base.reservation_boundaries_private_cm;

 INSERT INTO base.reservation_boundaries_private_cm
 SELECT res, the_geom
 FROM base.reservation_boundaries_public_private_cm
 WHERE access = 0;

 END IF;
 RETURN NEW;
END;

$dissolver$ LANGUAGE plpgsql;

CREATE TRIGGER dissolver
 AFTER INSERT ON base.reservation_boundaries_public_private_cm
 FOR EACH STATEMENT EXECUTE PROCEDURE dissolve_bounds();

Now just a few more things to stick in there for masking versions of my boundary, and a script to export this to a shapefile to synchronize with my file system, PostGIS gets to maintain by boundaries, not me or an intern.  Between you and me, my interns are good and very reliable, but PostGIS is much more reliable than I am.

Posted in Database, Database Optimization, PostGIS, SQL | Tagged: , , , , | 3 Comments »

Unique constraint on geometry continued

Posted by smathermather on October 22, 2009

An actual example of using the hashed geometry to uniquely constrain geometry (nothing really new here, just an example of using what we learned in the last post with a new problem):

I’ve got a contours dataset from the county where I work that is in 20 different shapefiles ranging in size from 500MB to 2GB.  I want to put them into a PostGIS database in a single table, so that I can use PostGIS to slice and dice them into reasonable chunks for use by our engineers in AutoDesk products, by ArcGIS users in several divisions, and to improve spatial indexing when dump them into one table in our spatial database as well (ala Regina Obe/Boston GIS’s Map Dicing).  This is a really detailed LiDAR-based contour dataset which used breaklines to make a really detailed and accurate product– which sadly is unusable in its current form because it takes too long to load any given section.

But, today, no slicing and dicing just yet.  I loaded all the data and discovered duplicate geometries.  It seems that some of these blocks that the data come in have overlaps.  I don’t have a screen shot of it, but when I viewed the data in uDig, you can see the duplicate geometries because the transparency of the contour lines in the duplicated areas is less transparent, i.e. the lines are darker against a white background.  I could search for and remove duplicates, but that seems heavy handed.  I’m going to recreate the table with a unique constraint applied to the hashed geometry (and hope for no hash collisions).

First we create the table:

CREATE TABLE base.cuy_contours_2
(
 gid serial NOT NULL,
 elevation smallint,
 shape_leng real,
 the_geom geometry,
 hash character(32),
 CONSTRAINT cuy_contours_2_pkey PRIMARY KEY (gid),
 CONSTRAINT geom_uniq UNIQUE (hash),
 CONSTRAINT enforce_dims_the_geom CHECK (ndims(the_geom) = 4),
 CONSTRAINT enforce_geotype_the_geom CHECK (geometrytype(the_geom) = 'LINESTRING'::text OR the_geom IS NULL),
 CONSTRAINT enforce_srid_the_geom CHECK (srid(the_geom) = 9102722)
)
WITH (OIDS=FALSE);
ALTER TABLE base.cuy_contours_2 OWNER TO postgres;

CREATE INDEX cuy_contours_2_the_geom_idx
 ON base.cuy_contours_2
 USING gist
 (the_geom);

Now we create our trigger that creates a hashed version of the geometry, “the_geom”, in a column called “hash”.

DROP TRIGGER hasher on base.cuy_contours_2;
CREATE OR REPLACE FUNCTION hash_geom() RETURNS TRIGGER AS $hasher$
BEGIN
 IF(TG_OP='INSERT') THEN

 UPDATE base.cuy_contours_2 SET hash = MD5(ST_AsBinary(the_geom));

 END IF;
 RETURN NEW;
END;
$hasher$ LANGUAGE plpgsql;

CREATE TRIGGER hasher AFTER INSERT ON base.cuy_contours_2 FOR EACH STATEMENT EXECUTE PROCEDURE hash_geom();

And finally we add a unique constraint:

ALTER TABLE base.cuy_contours_2 ADD CONSTRAINT geom_uniq UNIQUE (hash);

Now, sadly, I have to use windows where I work, so the following is Windows Command language to load all my shapefiles into the PostGIS database.  That said, I think the command, in this rare case, is more elegant than similar code in, say, BASH scripting:

for %f in (*.shp) do shp2pgsql -s 102722 -a -S -N skip %f base.cuy_contours_2 | psql -h bobs_server -d CM -U postgres

using the shp2pgsql -a flag for append, -S to enforce simple geometries, -N skip to ensure that we keep all records with non-null geometries to compensate for potential errors in the input dataset).

Posted in PostGIS | Tagged: , , , | Leave a Comment »

PostGIS Gripe—Limits to Postgre’s B-tree indexing—Followup

Posted by smathermather on August 18, 2009

In an earlier post, I griped about the limits to Postgres B-tree indexing, in that for large PostGIS geometries, I couldn’t create a unique constraint for PostGIS geometry.  Abe Gillespie (blame me not him for any mistakes  in what follows), suggested that I hash the geometry and create an index on that.  In a follow-up e-mail, he suggested a series of tests in a trigger statement, first test on the bbox, then on the hashed geometry, then finally on the geometry itself upon insert, to determine if the geometry is unique or not.

geometry_decision_tree

Well, I’ve completed the first suggestion, but not yet the second.  Here’s my trigger statement:

DROP TRIGGER dissolver on public.reservation_boundaries_public_private_cm;
CREATE OR REPLACE FUNCTION dissolve_bounds() RETURNS TRIGGER AS $dissolver$
BEGIN
   IF(TG_OP='INSERT') THEN

     UPDATE reservation_boundaries_public_private_cm SET hash = MD5(ST_AsBinary(the_geom));

   END IF;
 RETURN NEW;
END;
$dissolver$ LANGUAGE plpgsql;

CREATE TRIGGER dissolver BEFORE AFTER INSERT ON public.reservation_boundaries_public_private_cm FOR EACH STATEMENT EXECUTE PROCEDURE dissolve_bounds();

So, each time I insert a new geometry, the field “hash” is updated with an MD5 hashed version of the geometry.  Now we want to test against this so we add a constraint:

ALTER TABLE public.reservation_boundaries_public_private_cm ADD CONSTRAINT geom_uniq UNIQUE (hash);

And it works a charm. The only problem is that if I were to get a hash collision, I would reject a record and not mean to. I’m not sure the statistical chances of this, but implementing the above flow chart would prevent such a possibility.  If my reading is right, the chances of a hash collision is nearly 1 in 2^128, so maybe I won’t worry this year… .

Posted in PostGIS | Tagged: , , , | Leave a Comment »

PostGIS Triggers

Posted by smathermather on August 12, 2009

Yup, I’m going for hits with the above title.  So, I’ve been playing with triggers in PostGIS (PostgreSQL) trying to streamline a process that I go through quarterly.  We have a geographic boundary layer for our public entity:

boundary_1

The green area is the area with public access.  The purple without.  I want to maintain a version of the boundary with this very simple distinction, and another version of it in which the boundary is dissolved.  Triggers seem ideal here.  When I update the first, I get a version of the second (see below)boundary_2

The command to do this is as follows:

CREATE TABLE public.reservation_boundaries_public_private_cm_dissolved
 AS
 SELECT res, ST_Union(the_geom) AS the_geom
  FROM public.reservation_boundaries_public_private_cm
   GROUP BY res;

So far, so good (sorry about the awful table names).  Now it needs to run each time there is an insert on the original table.  Here is what I came up with:

DROP TRIGGER dissolver ON public.reservation_boundaries_public_private_cm;

CREATE OR REPLACE FUNCTION dissolve_bounds() RETURNS TRIGGER AS $dissolver$

BEGIN
 IF(TG_OP='INSERT') THEN

 DROP TABLE IF EXISTS public.reservation_boundaries_public_private_cm_dissolved;

 CREATE TABLE public.reservation_boundaries_public_private_cm_dissolved
 AS
 SELECT res, ST_Union(the_geom)
   AS the_geom
   FROM public.reservation_boundaries_public_private_cm
   GROUP BY res;

 END IF;
 RETURN NEW;
END;

$dissolver$ LANGUAGE plpgsql;

CREATE TRIGGER dissolver
  AFTER INSERT ON public.reservation_boundaries_public_private_cm
  FOR EACH STATEMENT EXECUTE PROCEDURE dissolve_bounds();

My only complaint here is speed– it takes about 45 seconds (instead of close to 0.3 seconds) now to insert all my records.  I hoped that I could speed it up with the addition of

FOR EACH STATEMENT EXECUTE PROCEDURE

instead of

FOR EACH RECORD EXECUTE PROCEDURE

but the difference in speed is undetectable.  Any suggestions on how to speed this puppy up?

Posted in PostGIS | Tagged: , , , | 3 Comments »