Line data Source code
1 : /*
2 : * SPDX-License-Identifier: MPL-2.0
3 : *
4 : * This Source Code Form is subject to the terms of the Mozilla Public
5 : * License, v. 2.0. If a copy of the MPL was not distributed with this
6 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 : *
8 : * Copyright 2024 MonetDB Foundation;
9 : * Copyright August 2008 - 2023 MonetDB B.V.;
10 : * Copyright 1997 - July 2008 CWI.
11 : */
12 :
13 : /*
14 : * Authors: K. Kyzirakos, D. Savva
15 : * This module contains primitives for accessing geospatial data
16 : * stored in ESRI Shapefile documents.
17 : */
18 :
19 : #include "monetdb_config.h"
20 : #include <string.h>
21 : #include "sql_mvc.h"
22 : #include "sql.h"
23 : #ifndef __clang_major__ /* stupid include file gdal/cpl_port.h */
24 : #define __clang_major__ 0
25 : #endif
26 : #include "shp.h"
27 : #include <cpl_conv.h> /* for CPLFree */
28 : #include "sql_execute.h"
29 : #include "mal_exception.h"
30 :
31 : #include "libgeom.h"
32 :
33 0 : GDALWConnection * GDALWConnect(char * source) {
34 0 : GDALWConnection * conn = NULL;
35 0 : OGRFeatureDefnH featureDefn;
36 0 : int fieldCount, i;
37 0 : OGRRegisterAll();
38 0 : conn = malloc(sizeof(GDALWConnection));
39 0 : if (conn == NULL) {
40 0 : TRC_ERROR(SHP, "Could not allocate memory\n");
41 0 : return NULL;
42 : }
43 0 : conn->handler = OGROpen(source, 0 , &(conn->driver));
44 0 : if (conn->handler == NULL) {
45 0 : free(conn);
46 0 : return NULL;
47 : }
48 :
49 0 : conn->layer = OGR_DS_GetLayer(conn->handler, 0);
50 0 : if (conn->layer == NULL) {
51 0 : OGRReleaseDataSource(conn->handler);
52 0 : free(conn);
53 0 : return NULL;
54 : }
55 :
56 0 : conn->layername = (const char *) OGR_L_GetName(conn->layer);
57 :
58 0 : featureDefn = OGR_L_GetLayerDefn(conn->layer);
59 0 : fieldCount = OGR_FD_GetFieldCount(featureDefn);
60 0 : conn->numFieldDefinitions = fieldCount;
61 0 : conn->fieldDefinitions = malloc(fieldCount * sizeof(OGRFieldDefnH));
62 0 : if (conn->fieldDefinitions == NULL) {
63 0 : OGRReleaseDataSource(conn->handler);
64 0 : free(conn);
65 0 : TRC_ERROR(SHP, "Could not allocate memory\n");
66 0 : return NULL;
67 : }
68 0 : for (i=0 ; i<fieldCount ; i++) {
69 0 : conn->fieldDefinitions[i] = OGR_FD_GetFieldDefn(featureDefn, i);
70 : }
71 :
72 : return conn;
73 : }
74 :
75 0 : void GDALWClose(GDALWConnection * conn) {
76 0 : free(conn->fieldDefinitions);
77 0 : OGRReleaseDataSource(conn->handler);
78 0 : free(conn);
79 0 : }
80 :
81 0 : GDALWSimpleFieldDef * GDALWGetSimpleFieldDefinitions(GDALWConnection conn) {
82 0 : int i;
83 0 : GDALWSimpleFieldDef * columns;
84 0 : OGRFieldDefnH fieldDefn;
85 : /*if (conn.layer == NULL || conn.handler == NULL || conn.driver == NULL) {
86 : printf("Could not extract columns, initialize a connection first.\n");
87 : exit(-1);
88 : }*/
89 0 : columns = malloc(conn.numFieldDefinitions * sizeof(GDALWSimpleFieldDef));
90 0 : if (columns == NULL) {
91 0 : TRC_ERROR(SHP, "Could not allocate memory\n");
92 0 : return NULL;
93 : }
94 0 : for (i=0 ; i<conn.numFieldDefinitions ; i++) {
95 0 : fieldDefn = conn.fieldDefinitions[i];
96 0 : columns[i].fieldName = OGR_Fld_GetNameRef(fieldDefn);
97 0 : columns[i].fieldType = OGR_GetFieldTypeName(OGR_Fld_GetType(fieldDefn));
98 : }
99 :
100 : return columns;
101 : }
102 :
103 0 : void GDALWPrintRecords(GDALWConnection conn) {
104 0 : char * wkt;
105 0 : int i;
106 0 : OGRFeatureH feature;
107 0 : OGRGeometryH geometry;
108 0 : OGRFeatureDefnH featureDefn;
109 0 : featureDefn = OGR_L_GetLayerDefn(conn.layer);
110 0 : OGR_L_ResetReading(conn.layer);
111 0 : while( (feature = OGR_L_GetNextFeature(conn.layer)) != NULL ) {
112 0 : for(i = 0; i < OGR_FD_GetFieldCount(featureDefn); i++ ) {
113 0 : OGRFieldDefnH hFieldDefn = OGR_FD_GetFieldDefn( featureDefn, i );
114 0 : if( OGR_Fld_GetType(hFieldDefn) == OFTInteger )
115 0 : printf( "%d,", OGR_F_GetFieldAsInteger( feature, i ) );
116 0 : else if( OGR_Fld_GetType(hFieldDefn) == OFTReal )
117 0 : printf( "%.3f,", OGR_F_GetFieldAsDouble( feature, i) );
118 : else
119 0 : printf( "%s,", OGR_F_GetFieldAsString( feature, i) );
120 :
121 : }
122 0 : geometry = OGR_F_GetGeometryRef(feature);
123 0 : OGR_G_ExportToWkt(geometry, &wkt);
124 0 : printf("%s", wkt);
125 0 : printf("\n");
126 0 : CPLFree(wkt);
127 0 : OGR_F_Destroy(feature);
128 : }
129 0 : }
130 :
131 0 : GDALWSpatialInfo GDALWGetSpatialInfo(GDALWConnection conn) {
132 0 : GDALWSpatialInfo spatialInfo;
133 0 : OGRSpatialReferenceH spatialRef = OGR_L_GetSpatialRef(conn.layer);
134 0 : char * proj4, * srsText, * srid;
135 :
136 0 : OSRExportToProj4(spatialRef, &proj4);
137 0 : OSRExportToWkt(spatialRef, &srsText);
138 0 : srid = (char *) OSRGetAttrValue(spatialRef, "AUTHORITY", 1);
139 0 : if (srid == NULL) {
140 : spatialInfo.epsg = 4326;
141 : }
142 : else {
143 0 : spatialInfo.epsg = atoi(OSRGetAttrValue(spatialRef, "AUTHORITY", 1));
144 : }
145 0 : spatialInfo.authName = OSRGetAttrValue(spatialRef, "AUTHORITY", 0);
146 0 : if (spatialInfo.authName == NULL) {
147 0 : spatialInfo.authName = "EPSG";
148 : }
149 0 : spatialInfo.proj4Text = proj4;
150 0 : spatialInfo.srsText = srsText;
151 :
152 0 : return spatialInfo;
153 : }
154 :
155 : //Using SQL query
156 0 : str createSHPtable(Client cntxt, str schemaname, str tablename, GDALWConnection shp_conn, GDALWSimpleFieldDef *field_definitions) {
157 0 : unsigned int size = BUFSIZ;
158 0 : char *buf = NULL, *temp_buf = GDKmalloc(BUFSIZ * sizeof(char));
159 0 : char *nameToLowerCase = NULL;
160 0 : str msg = MAL_SUCCEED;
161 :
162 0 : if (field_definitions == NULL)
163 : {
164 : /* Can't find shapefile field definitions */
165 0 : return createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
166 : }
167 :
168 : /* Create the table that will store the data of the shape file (Allows integers, floats and strings) */
169 0 : temp_buf[0] = '\0';
170 0 : for (int i = 0; i < shp_conn.numFieldDefinitions; i++)
171 : {
172 : /*If the next column definition doesn't fit in the buffer, resize the buffers to double
173 : Compare current buffer size with current lenght + field name lenght + 11 (lenght of string column definition)*/
174 0 : if (size <= (11 + strlen(field_definitions[i].fieldName) + strlen(temp_buf)))
175 : {
176 0 : size = 2 * size;
177 0 : temp_buf = GDKrealloc(temp_buf, size);
178 : }
179 0 : nameToLowerCase = toLower(field_definitions[i].fieldName);
180 0 : if (strcmp(field_definitions[i].fieldType, "Integer") == 0)
181 : {
182 0 : sprintf(temp_buf + strlen(temp_buf), "\"%s\" INT, ", nameToLowerCase);
183 : }
184 0 : else if (strcmp(field_definitions[i].fieldType, "Real") == 0)
185 : {
186 0 : sprintf(temp_buf + strlen(temp_buf), "\"%s\" FLOAT, ", nameToLowerCase);
187 : }
188 : else
189 0 : sprintf(temp_buf + strlen(temp_buf), "\"%s\" STRING, ", nameToLowerCase);
190 0 : GDKfree(nameToLowerCase);
191 : }
192 :
193 : //Each shapefile table has one geom column
194 0 : sprintf(temp_buf + strlen(temp_buf), "geom GEOMETRY ");
195 :
196 : //Concat the schema name with the table name
197 0 : size_t schemaTableSize = strlen(schemaname) + strlen(tablename) + 3;
198 0 : char *schemaTable = GDKmalloc(schemaTableSize);
199 0 : snprintf(schemaTable, schemaTableSize - 1, "%s.%s", schemaname, tablename);
200 :
201 : //Build the CREATE TABLE command
202 0 : buf = GDKmalloc((size + schemaTableSize) * sizeof(char));
203 0 : snprintf(buf, size + schemaTableSize, CRTTBL, schemaTable, temp_buf);
204 : //And execute it
205 0 : msg = SQLstatementIntern(cntxt, buf, "shp.load", TRUE, FALSE, NULL);
206 :
207 0 : GDKfree(buf);
208 0 : GDKfree(temp_buf);
209 0 : GDKfree(schemaTable);
210 0 : return msg;
211 : }
212 :
213 0 : str loadSHPtable(mvc *m, sql_schema *sch, str schemaname, str tablename, GDALWConnection shp_conn, GDALWSimpleFieldDef *field_definitions, GDALWSpatialInfo spatial_info) {
214 0 : sql_table *data_table = NULL;
215 0 : sql_column **cols;
216 0 : BAT **colsBAT;
217 0 : int colsNum = 2; //we will have at least the gid column and a geometry column
218 0 : int rowsNum = 0; //the number of rows in the shape file that will be imported
219 0 : int gidNum = 0;
220 0 : str msg = MAL_SUCCEED;
221 0 : char *nameToLowerCase = NULL;
222 0 : int i;
223 :
224 0 : BUN offset;
225 0 : BAT *pos = NULL;
226 0 : sqlstore *store;
227 :
228 : /* SHP-level descriptor */
229 0 : OGRFieldDefnH hFieldDefn;
230 0 : OGRFeatureH feature;
231 0 : OGRFeatureDefnH featureDefn;
232 :
233 : /* Count the number of lines in the shape file */
234 0 : if ((rowsNum = OGR_L_GetFeatureCount(shp_conn.layer, false)) == -1) {
235 0 : OGR_L_ResetReading(shp_conn.layer);
236 0 : rowsNum = 0;
237 0 : while ((feature = OGR_L_GetNextFeature(shp_conn.layer)) != NULL) {
238 0 : rowsNum++;
239 0 : OGR_F_Destroy(feature);
240 : }
241 : }
242 :
243 : /* bind the columns of the data table that was just created
244 : * and create a BAT for each of the columns */
245 0 : if (!(data_table = mvc_bind_table(m, sch, tablename))) {
246 : /* Previously create output table is missing */
247 0 : msg = createException(MAL, "shp.load", SQLSTATE(42SO2) "Table '%s.%s' missing", schemaname, tablename);
248 0 : return msg;
249 : }
250 0 : colsNum += shp_conn.numFieldDefinitions;
251 0 : if (!(cols = (sql_column **)GDKmalloc(sizeof(sql_column *) * colsNum))) {
252 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
253 0 : return msg;
254 : }
255 0 : if (!(colsBAT = (BAT **)GDKzalloc(sizeof(BAT *) * colsNum))) {
256 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
257 0 : GDKfree(cols);
258 0 : return msg;
259 : }
260 :
261 : /* Bind shapefile attributes to columns */
262 0 : for (i = 0; i < colsNum - 2; i++) {
263 0 : cols[i] = NULL;
264 : /* bind the column */
265 0 : nameToLowerCase = toLower(field_definitions[i].fieldName);
266 0 : cols[i] = mvc_bind_column(m, data_table, nameToLowerCase);
267 0 : GDKfree(nameToLowerCase);
268 0 : if (cols[i] == NULL) {
269 0 : msg = createException(MAL, "shp.load", SQLSTATE(42SO2) "Column '%s.%s(%s)' missing", schemaname, tablename, toLower(field_definitions[i].fieldName));
270 0 : goto unfree;
271 : }
272 : /*create the BAT */
273 0 : if (strcmp(field_definitions[i].fieldType, "Integer") == 0) {
274 0 : if (!(colsBAT[i] = COLnew(0, TYPE_int, rowsNum, PERSISTENT))) {
275 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
276 0 : goto unfree;
277 : }
278 : }
279 0 : else if (strcmp(field_definitions[i].fieldType, "Real") == 0) {
280 0 : if (!(colsBAT[i] = COLnew(0, TYPE_dbl, rowsNum, PERSISTENT))) {
281 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
282 0 : goto unfree;
283 : }
284 : }
285 : else {
286 0 : if (!(colsBAT[i] = COLnew(0, TYPE_str, rowsNum, PERSISTENT))) {
287 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
288 0 : goto unfree;
289 : }
290 : }
291 : }
292 : /* Bind GID and GEOM columns */
293 0 : if (!(cols[colsNum - 2] = mvc_bind_column(m, data_table, "gid"))) {
294 0 : msg = createException(MAL, "shp.load", SQLSTATE(42SO2) "Column '%s.%s(gid)' missing", schemaname, tablename);
295 0 : goto unfree;
296 : }
297 0 : if (!(colsBAT[colsNum - 2] = COLnew(0, TYPE_int, rowsNum, PERSISTENT))) {
298 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
299 0 : goto unfree;
300 : }
301 0 : if (!(cols[colsNum - 1] = mvc_bind_column(m, data_table, "geom"))) {
302 0 : msg = createException(MAL, "shp.load", SQLSTATE(42SO2) "Column '%s.%s(geom)' missing", schemaname, tablename);
303 0 : goto unfree;
304 : }
305 0 : if (!(colsBAT[colsNum - 1] = COLnew(0, ATOMindex("wkb"), rowsNum, PERSISTENT))) {
306 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
307 0 : goto unfree;
308 : }
309 :
310 : /* Import the data */
311 0 : featureDefn = OGR_L_GetLayerDefn(shp_conn.layer);
312 0 : OGR_L_ResetReading(shp_conn.layer);
313 : /* Import shapefile attributes */
314 0 : while ((feature = OGR_L_GetNextFeature(shp_conn.layer)) != NULL) {
315 0 : wkb *geomWKB;
316 0 : int len;
317 0 : int gidTemp = ++gidNum;
318 0 : gdk_return rc;
319 :
320 0 : OGRGeometryH geometry = OGR_F_GetGeometryRef(feature);
321 :
322 0 : for (i = 0; i < colsNum - 2; i++) {
323 0 : hFieldDefn = OGR_FD_GetFieldDefn(featureDefn, i);
324 0 : if (OGR_Fld_GetType(hFieldDefn) == OFTInteger) {
325 0 : int val = OGR_F_GetFieldAsInteger(feature, i);
326 0 : rc = BUNappend(colsBAT[i], &val, false);
327 : }
328 0 : else if (OGR_Fld_GetType(hFieldDefn) == OFTReal) {
329 0 : double val = OGR_F_GetFieldAsDouble(feature, i);
330 0 : rc = BUNappend(colsBAT[i], &val, false);
331 : }
332 : else {
333 0 : rc = BUNappend(colsBAT[i], OGR_F_GetFieldAsString(feature, i), false);
334 : }
335 0 : if (rc != GDK_SUCCEED) {
336 : /* Append to column failed */
337 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
338 0 : goto unfree;
339 : }
340 : }
341 :
342 : /* Import GID and GEOM columns */
343 0 : if (BUNappend(colsBAT[colsNum - 2], &gidTemp, false) != GDK_SUCCEED) {
344 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
345 0 : goto unfree;
346 : }
347 :
348 0 : len = OGR_G_WkbSize(geometry);
349 0 : if (!(geomWKB = GDKmalloc(sizeof(wkb) + len))) {
350 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
351 0 : OGR_F_Destroy(feature);
352 0 : goto unfree;
353 : }
354 0 : geomWKB->len = len;
355 : /* Set SRID */
356 0 : geomWKB->srid = spatial_info.epsg;
357 0 : OGR_G_ExportToWkb(geometry, wkbNDR, (unsigned char *)geomWKB->data);
358 0 : rc = BUNappend(colsBAT[colsNum - 1], geomWKB, false);
359 :
360 0 : GDKfree(geomWKB);
361 0 : OGR_F_Destroy(feature);
362 0 : if (rc != GDK_SUCCEED)
363 0 : goto unfree;
364 : }
365 : /* finalise the BATs */
366 0 : store = m->session->tr->store;
367 0 : if (store->storage_api.claim_tab(m->session->tr, data_table, BATcount(colsBAT[0]), &offset, &pos) != LOG_OK) {
368 0 : msg = createException(MAL, "shp.load", SQLSTATE(HY013) MAL_MALLOC_FAIL);
369 0 : goto unfree;
370 : }
371 :
372 0 : for (i = 0; i < colsNum; i++) {
373 0 : if (store->storage_api.append_col(m->session->tr, cols[i], offset, pos, colsBAT[i], BATcount(colsBAT[i]), TYPE_bat) != LOG_OK) {
374 0 : bat_destroy(pos);
375 0 : msg = createException(MAL, "shp.load", SQLSTATE(38000) "Geos append column failed");
376 0 : goto unfree;
377 : }
378 : }
379 0 : GDKfree(colsBAT);
380 0 : GDKfree(cols);
381 0 : bat_destroy(pos);
382 0 : return msg;
383 0 : unfree:
384 0 : for (i = 0; i < colsNum; i++)
385 0 : if (colsBAT[i])
386 0 : BBPunfix(colsBAT[i]->batCacheid);
387 0 : free(field_definitions);
388 0 : GDKfree(cols);
389 0 : GDKfree(colsBAT);
390 0 : return msg;
391 : }
392 :
393 0 : static str SHPloadInternal (Client cntxt, MalBlkPtr mb, str filename, str schemaname, str tablename) {
394 0 : str msg = MAL_SUCCEED;
395 0 : mvc *m = NULL;
396 0 : sql_schema *sch = NULL;
397 :
398 0 : if ((msg = getSQLContext(cntxt, mb, &m, NULL)) != MAL_SUCCEED)
399 : return msg;
400 0 : if ((msg = checkSQLContext(cntxt)) != MAL_SUCCEED)
401 : return msg;
402 :
403 : /* SHP-level descriptor */
404 0 : GDALWConnection shp_conn;
405 0 : GDALWConnection *shp_conn_ptr = NULL;
406 0 : GDALWSimpleFieldDef *field_definitions;
407 0 : GDALWSpatialInfo spatial_info;
408 :
409 0 : if (!(sch = mvc_bind_schema(m, schemaname)))
410 : /* Can't find schema */
411 0 : return createException(MAL, "shp.load", SQLSTATE(38000) "Schema %s missing\n", schemaname);
412 :
413 0 : if ((tablename != NULL) && (tablename[0] == '\0'))
414 : /* Output table name is NULL */
415 0 : return createException(MAL, "shp.load", SQLSTATE(38000) "Missing output table name %s\n", tablename);
416 :
417 0 : if ((shp_conn_ptr = GDALWConnect((char *)filename)) == NULL)
418 : /* Can't find shapefile */
419 0 : return createException(MAL, "shp.load", SQLSTATE(38000) "Missing shape file %s\n", filename);
420 :
421 : /* Get info about fields and spatial attributes of shapefile*/
422 0 : shp_conn = *shp_conn_ptr;
423 0 : spatial_info = GDALWGetSpatialInfo(shp_conn);
424 0 : field_definitions = GDALWGetSimpleFieldDefinitions(shp_conn);
425 :
426 : /* Convert schema and table name to lower case*/
427 0 : schemaname = toLower(schemaname);
428 0 : tablename = toLower(tablename);
429 :
430 : /* Create table for outputting shapefile data */
431 0 : if ((msg = createSHPtable(cntxt, schemaname, tablename, shp_conn, field_definitions)) != MAL_SUCCEED) {
432 : /* Create table failed */
433 0 : GDKfree(schemaname);
434 0 : GDKfree(tablename);
435 0 : free(field_definitions);
436 0 : GDALWClose(shp_conn_ptr);
437 0 : return msg;
438 : }
439 :
440 : //TODO If createSHPtable works and loadSHPtable doesn't, we have to clean the created table
441 : /* Load shapefile data into table */
442 0 : msg = loadSHPtable(m, sch, schemaname, tablename, shp_conn, field_definitions, spatial_info);
443 :
444 : /* Frees */
445 0 : GDKfree(schemaname);
446 0 : GDKfree(tablename);
447 0 : free(field_definitions);
448 0 : GDALWClose(shp_conn_ptr);
449 :
450 0 : return msg;
451 : }
452 :
453 : /* TODO: Use Shapefile table to avoid loading the same file more than once, or allow the user to load as many times as he wants? */
454 : /* Attach and load single shp file given its file name and output table name */
455 0 : str SHPloadSchema(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci)
456 : {
457 : /* Shapefile name (argument 1) */
458 0 : str filename = *(str *)getArgReference(stk, pci, 1);
459 : /* Output schema name (argument 2) */
460 0 : str schemaname = *(str *)getArgReference(stk, pci, 2);
461 : /* Output table name (argument 3) */
462 0 : str tablename = *(str *)getArgReference(stk, pci, 3);
463 0 : return SHPloadInternal(cntxt,mb,filename,schemaname,tablename);
464 : }
465 :
466 0 : str SHPload(Client cntxt, MalBlkPtr mb, MalStkPtr stk, InstrPtr pci) {
467 : /* Shapefile name (argument 1) */
468 0 : str filename = *(str *)getArgReference(stk, pci, 1);
469 : /* Output table name (argument 3) */
470 0 : str tablename = *(str *)getArgReference(stk, pci, 2);
471 0 : return SHPloadInternal(cntxt,mb,filename,"sys",tablename);
472 : }
473 :
474 : #include "mel.h"
475 : static mel_func shp_init_funcs[] = {
476 : pattern("shp", "load", SHPloadSchema, false, "Import an ESRI Shapefile into a new table, on a certain schema", args(1, 4, arg("", void), arg("filename", str), arg("schemaname", str), arg("tablename", str))),
477 : pattern("shp", "load", SHPload, false, "Import an ESRI Shapefile into a new table, on the sys schema", args(1, 3, arg("", void), arg("filename", str), arg("tablename", str))),
478 : {.imp = NULL}};
479 : #include "mal_import.h"
480 : #ifdef _MSC_VER
481 : #undef read
482 : #pragma section(".CRT$XCU", read)
483 : #endif
484 324 : LIB_STARTUP_FUNC(init_shp_mal)
485 : {
486 324 : mal_module("shp", NULL, shp_init_funcs);
487 324 : }
|