First Look at a Geospatial File: What to Check Before You Trust It
tool
geometry
bilingual
Upload a GeoJSON or a Shapefile and get an instant summary — feature count, geometry type, CRS, bounds, attributes, invalid/empty/duplicate geometries — in Python and R on the same map.
Published
September 5, 2026
Before you validate, fix, or check topology, it helps to know what you’re actually holding. A feature count in the thousands changes what checks are even practical. A CRS you didn’t expect explains a map that looks wrong before you’ve touched a single geometry. A handful of exact duplicates might be an import artifact, not a data-quality problem worth chasing.
This isn’t a repair tool and it isn’t a topology report — it’s the five-second first look that tells you which of those tools you actually need next.
Just want the summary, without the rest of the reading? Use the standalone tool — same engine, less prose.
1. Upload your file
A .geojson, or a shapefile: either as a single .zip, or as the individual .shp/.dbf/.shx files (and optionally .prj) selected together.
mutable uploadedFiles =null
// A native <input type="file"> (WebGeoDS.Upload.createControl(), see// shared/upload.js) — see its doc comment there for why, over an// Observable Inputs `viewof`-bound widget.uploadControl = WebGeoDS.Upload.createControl({label:"Upload",onChange: (files) => { mutable uploadedFiles = files; }})
{const result =await WebGeoDS.Upload.load(uploadedFiles); mutable uploadStatus = result.message;}
2. Why start with a summary?
Eight numbers, each answering a question you’d otherwise have to dig for:
Features — how many rows. Determines whether an O(n²) check (like the Topology Checker’s pairwise overlap/gap detection) is even practical to run.
Geometry type — Point, Line, Polygon, or a mix. Some checks only apply to one family; a “mixed” result is itself useful information.
CRS — the coordinate reference system the file claims to be in. Not verified against the actual coordinates here — that mismatch detection is a distinct, more involved check, planned as its own tool.
Bounds — the bounding box, in the file’s own CRS units. A bounding box in the thousands or millions instead of the -180..180 / -90..90 range is usually the first hint of a CRS problem, before any dedicated check runs.
Attributes — how many non-geometry columns, and their names. How much there is to a feature beyond its shape.
Invalid — geometries that fail the Simple Features validity rules. See Geometry validity for what that actually means and how to fix it.
Empty — geometries with no coordinates at all. Easy to miss in a raw file, and something naive code (yours or a library’s) can choke on silently.
Duplicates — exact repeats (identical coordinates), detected by comparing binary geometry representations, not a full topological equality check. Fast enough for a large file; the Topology Checker has the slower, more thorough version if you need it.
// Same coloring convention as geometry-validity.qmd's VALIDITY_PAINT// — green valid, red invalid.INSPECT_PAINT = ({"fill-color": ["case", ["==", ["get","valid"],true],"#2ea44f","#e05252" ],"fill-opacity":0.55})
// The summary cards' own state, separate from reading pyInspect/// rInspect directly: those are the CodeCells' cached values, which// Reset below has no way to clear (there's nothing to re-run) —// without this, clicking Reset would empty the map but leave both// summary cards showing stale numbers. Set alongside each map update// below, cleared alongside the map reset further down.mutable pySummary =null
mutable rSummary =null
pyInspect = WebGeoDS.getCellValue("inspect-py", Generators){if (pyInspect) {// mapFeatures (WGS84), falling back to features: the default// inspect-py code returns both, but the CRS-mismatch example in// section 6 deliberately returns only "features", still in Web// Mercator — that's the whole point of that example (see its own// callout below), so it must NOT be silently reprojected here.const mapData = pyInspect.mapFeatures?? pyInspect.features;await sharedMap.setGeoJSON("inspect-py", mapData, { paint: INSPECT_PAINT });// Zooms to whatever was just inspected — same as the standalone// tool. Without this the CRS-mismatch example in section 6 would// load fine but stay invisible: the map would just sit at its// fixed initial view over Italy instead of jumping to wherever the// Web Mercator coordinates actually land, missing the whole point// of that example.await sharedMap.fitToData(mapData); mutable pySummary = pyInspect.summary; }}
The CRS string is the one place they can genuinely disagree in formatting, not just vocabulary: st_crs()$input returns whatever string the file’s own CRS definition resolved to (often "EPSG:4326" for a clean case, but sometimes a full WKT string for a less common one), while GeoPandas’ .crs.to_string() goes through PyPROJ’s own formatting — same underlying CRS, not always the same displayed text. Worth knowing before assuming a mismatch between the two summary cards above means an actual data problem.
The reusable cell pair above already loads a small dataset with one exact duplicate and one invalid geometry whenever nothing has been uploaded — click ▶ Run on both languages above to see it. This button swaps in a dataset with a deliberately unusual CRS instead, to see the bounds stat do its job:
pyCrsExampleCode =`import geopandas as gpdfrom shapely.geometry import shape# The same square as the default example, but reprojected to Web# Mercator (EPSG:3857) — coordinates in the hundreds of thousands to# millions instead of -180..180, exactly the tell described in# section 2 above.gdf = gpd.GeoDataFrame( {"name": ["square"]}, geometry=[shape({"type": "Polygon", "coordinates": [[[12.40, 41.80], [12.50, 41.80], [12.50, 41.90], [12.40, 41.90], [12.40, 41.80]]]})], crs="EPSG:4326").to_crs("EPSG:3857")if "name" not in gdf.columns: gdf["name"] = [f"geometry {i+1}" for i in range(len(gdf))]gdf["valid"] = gdf.geometry.apply(lambda g: g is not None and g.is_valid)gdf["empty"] = gdf.geometry.apply(lambda g: g is None or g.is_empty)gdf["duplicate"] = gdf.geometry.to_wkb().duplicated()for col in gdf.columns: if col != "geometry": gdf[col] = gdf[col].fillna("")attribute_columns = [c for c in gdf.columns if c not in ("geometry", "valid", "empty", "duplicate")]geometry_types = sorted(set(gdf.geometry.geom_type))bounds = [round(float(b), 6) for b in gdf.total_bounds]crs = gdf.crs.to_string() if gdf.crs is not None else None{ "features": gdf.__geo_interface__, "summary": { "total": len(gdf), "geometryTypes": geometry_types, "crs": crs, "bounds": bounds, "attributeNames": attribute_columns, "invalid": int((~gdf["valid"]).sum()), "empty": int(gdf["empty"].sum()), "duplicates": int(gdf["duplicate"].sum()) }}`rCrsExampleCode =`wkt <- "POLYGON((12.40 41.80, 12.50 41.80, 12.50 41.90, 12.40 41.90, 12.40 41.80))"data <- sf::st_sf(name = "square", geometry = sf::st_as_sfc(wkt, crs = 4326))data <- sf::st_transform(data, 3857)data$valid <- sf::st_is_valid(data)data$empty <- sf::st_is_empty(data)data$duplicate <- duplicated(sf::st_as_text(sf::st_geometry(data)))attribute_names <- setdiff(names(data), c("geometry", "valid", "empty", "duplicate"))geometry_types <- sort(unique(as.character(sf::st_geometry_type(data))))bnd <- sf::st_bbox(data)result <- list( features = jsonlite::fromJSON(geojsonsf::sf_geojson(data), simplifyVector = FALSE), summary = list( total = jsonlite::unbox(nrow(data)), geometryTypes = geometry_types, crs = jsonlite::unbox(sf::st_crs(data)$input), bounds = unname(round(as.numeric(bnd), 6)), attributeNames = attribute_names, invalid = jsonlite::unbox(sum(!data$valid)), empty = jsonlite::unbox(sum(data$empty)), duplicates = jsonlite::unbox(sum(data$duplicate)) ))jsonlite::fromJSON(jsonlite::toJSON(result, null = "null"), simplifyVector = FALSE)`
loadCrsExampleButton =loadExampleButton("📋 Load a CRS-mismatch example", pyCrsExampleCode, rCrsExampleCode)
Note
Web Mercator coordinates plotted at the same zoom level as geographic coordinates land far outside any sensible map view — that’s the “bounds in the hundreds of thousands” tell in practice, not just in the abstract. Actually correcting a CRS mismatch — reprojecting a file to match what you expect — is a separate, dedicated tool, planned as the CRS Inspector & Converter.
Everything valid, but the dataset still looks wrong? → Topology errors covers overlaps, gaps, slivers, duplicates, and dangles — relationships between features, not properties of any one of them — or use the standalone Topology Checker.
Just needed the numbers? The standalone Inspector has the same summary, no reading required.