├── .gitignore ├── LICENSE ├── README.md ├── docs └── index.html ├── img └── geoverview.svg ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── figuration.js ├── index.js ├── info.js ├── topo2geo.js └── view.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js folders 2 | node_modules/ 3 | 4 | # dist 5 | dist/ 6 | 7 | # History files 8 | .Rhistory 9 | 10 | # VS Code folders 11 | .vscode/* 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nicolas LAMBERT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![logo](img/geoverview.svg) 3 | 4 | ![npm](https://img.shields.io/npm/v/geoverview) 5 | ![jsdeliver](https://img.shields.io/jsdelivr/npm/hw/geoverview) 6 | ![license](https://img.shields.io/badge/license-MIT-success) 7 | ![code size](https://img.shields.io/github/languages/code-size/neocarto/geoverview) 8 | 9 | Based on [maplibre-gl](https://maplibre.org/), **geoverview** is a tool for giving a quick and easy geographic **overview** of any **geo**json (and the information it contains). Geoverview is particularly suitable for working within [Observable](https://observablehq.com/@neocartocnrs/geoverview). 10 | 11 | ![geoverview](./img/geoverview.png) 12 | 13 | ## How to use? 14 | 15 | It is very simple, geoverview contains only one function. In Observable, it is used in the following way. You need 3 cells: 16 | 17 | ```js 18 | // Load geoverview 19 | view = require("geoverview@1.2.1").then((f) => f.view) 20 | ``` 21 | 22 | ```js 23 | // add a geojson (or topojson) file 24 | data = FileAttachment("something.geojson").json() 25 | ``` 26 | 27 | ```js 28 | // and view 29 | view(data) 30 | ``` 31 | 32 | Automatically, the map and your geojson will be displayed. So simple... 33 | 34 | ## Demo 35 | 36 | Live demo on this [page](https://neocarto.github.io/geoverview) or this Observable [notebook](https://observablehq.com/@neocartocnrs/geoverview). 37 | 38 | ## Options 39 | 40 | You can add some options like this: 41 | 42 | ```js 43 | view(data, {width:800, renderWorldCopies:false}) 44 | ``` 45 | 46 | Option list: 47 | 48 | - **width**: width of the map (default: 1000) 49 | - **height**: height of the map (default: 550) 50 | - **col**: Color of the displayed geojson (default: "#be82c2") 51 | - **fillOpacity**: fill opacity (default: 0.5) 52 | - **lineWidth**: line thickness (default 1 if point or polygon, 3 if line) 53 | - **colOver**: color when an object is hovered (default: "#ffd505") 54 | - **renderWorldCopies**: If true , multiple copies of the world will be rendered side by side beyond -180 and 180 degrees longitude (default: true) 55 | - **style**: basemap style: "night", "fulldark", "voyager","positron","icgc","osmbright","hibrid" (default: voyager) 56 | 57 | ## Things to fix/improve 58 | 59 | - [ ] Add an example that works outside of Observable ([#1](https://github.com/neocarto/geoverview/issues/1)) 60 | - [ ] In [Quarto](https://quarto.org/), the rendering of the infoboxes is not good. Css problem? ([#2](https://github.com/neocarto/geoverview/issues/2)) 61 | 62 | See all [issues](https://github.com/neocarto/geoverview/issues). 63 | 64 | ## Contribute 65 | 66 | If you want to improve geoverview, feel free to post [issues](https://github.com/neocarto/geoverview/issues) (bugs, suggestions...) and suggest [pull requests](https://github.com/neocarto/geoverview/pulls). 67 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |

Hello geoverview.js

2 |
3 |
4 | -------------------------------------------------------------------------------- /img/geoverview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Geooverview 308 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geoverview", 3 | "version": "1.2.1", 4 | "description": "Based on Maplibre, geoverview is a tool to display very easily any geojson (and the information it contains) on a map.", 5 | "main": "src/index.js", 6 | "module": "src/index.js", 7 | "jsdelivr": "dist/index.min.js", 8 | "unpkg": "dist/index.min.js", 9 | "exports": { 10 | "umd": "./dist/index.min.js", 11 | "default": "./src/index.js" 12 | }, 13 | "files": [ 14 | "src", 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "rollup --config", 19 | "prepare": "npm run build" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/neocarto/geoverview.git" 24 | }, 25 | "keywords": [ 26 | "geojson", 27 | "map", 28 | "cartogrphy", 29 | "summary", 30 | "overview", 31 | "maplibre" 32 | ], 33 | "author": "Nicolas Lambert", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/neocarto/geoverview/issues" 37 | }, 38 | "homepage": "https://github.com/neocarto/geoverview#readme", 39 | "devDependencies": { 40 | "@rollup/plugin-babel": "^5.3.1", 41 | "@rollup/plugin-commonjs": "^22.0.0", 42 | "@rollup/plugin-node-resolve": "^13.3.0", 43 | "rollup-plugin-copy": "^3.4.0", 44 | "rollup-plugin-delete": "^2.0.0", 45 | "rollup-plugin-terser": "^7.0.2" 46 | }, 47 | "dependencies": { 48 | "@turf/area": "^6.5.0", 49 | "@turf/bbox": "^6.5.0", 50 | "@turf/length": "^6.5.0", 51 | "maplibre-gl": "^2.1.9", 52 | "topojson-client": "^3.1.0", 53 | "topojson-server": "^3.0.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import de nos plugins 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import noderesolve from "@rollup/plugin-node-resolve"; 4 | import babel from "@rollup/plugin-babel"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import copy from "rollup-plugin-copy"; 7 | import del from "rollup-plugin-delete"; 8 | 9 | export default { 10 | input: "src/index.js", 11 | output: { 12 | format: "umd", 13 | file: "dist/index.min.js", 14 | name: "geoverview", 15 | }, 16 | plugins: [ 17 | commonjs(), // prise en charge de require 18 | noderesolve(), // prise en charge des modules depuis node_modules 19 | babel({ babelHelpers: "bundled" }), // transpilation 20 | terser(), // minification 21 | del({ 22 | targets: "/var/www/html/npm_test/geoverview/index.min.js", 23 | force: true, 24 | }), 25 | copy({ 26 | targets: [ 27 | { src: "dist/index.min.js", dest: "/var/www/html/npm_test/geoverview" }, 28 | ], 29 | }), 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /src/figuration.js: -------------------------------------------------------------------------------- 1 | // ************************************************************ 2 | // figuration() check if the geometry is point, linear or zonal 3 | // ************************************************************ 4 | 5 | export function figuration(geojson) { 6 | let figuration = ["z", "l", "p"]; 7 | let types = geojson.features.map((d) => d.geometry.type); 8 | types = Array.from(new Set(types)); 9 | let poly = 10 | types.indexOf("Polygon") !== -1 || types.indexOf("MultiPolygon") !== -1 11 | ? figuration[0] 12 | : ""; 13 | let line = 14 | types.indexOf("LineString") !== -1 || 15 | types.indexOf("MultiLineString") !== -1 16 | ? figuration[1] 17 | : ""; 18 | let point = 19 | types.indexOf("Point") !== -1 || types.indexOf("MultiPoint") !== -1 20 | ? figuration[2] 21 | : ""; 22 | let tmp = poly + line + point; 23 | let result = tmp.length == 1 ? tmp : "c"; 24 | return [result, types.sort().join(", ")]; 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { view } from "./view.js"; 2 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // info() build a html table giving global information on the dataset 3 | // ****************************************************************** 4 | 5 | import { figuration } from "./figuration.js"; 6 | import { topo2geo } from "./topo2geo.js"; 7 | import turfbbox from "@turf/bbox"; 8 | 9 | export function info(geojson, unique) { 10 | const type = geojson.type; 11 | geojson = topo2geo(geojson); 12 | 13 | let result = []; 14 | const attr = geojson.features.map((d) => d.properties); 15 | attr.forEach((d) => result.push(Object.keys(d).length)); 16 | const keys = Object.keys(attr[result.indexOf(Math.max(...result))]); 17 | 18 | let pct = []; 19 | keys.forEach((k) => 20 | pct.push([ 21 | k, 22 | geojson.features 23 | .map((d) => d.properties[k]) 24 | .filter((d) => d != NaN) 25 | .filter((d) => d != "") 26 | .filter((d) => d != null) 27 | .filter((d) => d != undefined).length, 28 | ]) 29 | ); 30 | 31 | pct = new Map(pct); 32 | 33 | const nbfeat = geojson.features.length; 34 | const fig = figuration(geojson)[1]; 35 | 36 | let r = "
"; 37 | r += "Geometries in brief"; 38 | r += ""; 39 | r += ""; 40 | r += ""; 41 | r += 42 | ""; 45 | r += 46 | ""; 51 | r += "
Type" + type + "
Features" + fig + "
Count" + 43 | nbfeat + 44 | "
Bounding box" + 47 | turfbbox(geojson) 48 | .map((d) => Math.round(d * 100) / 100) 49 | .join(", ") + 50 | "
"; 52 | r += "Attribute data (completeness)"; 53 | r += ``; 54 | keys.forEach( 55 | (d) => 56 | (r += 57 | "") 67 | ); 68 | 69 | r += "
" + 58 | d + 59 | "
" + 62 | pct.get(d) + 63 | " /" + 64 | geojson.features.length + 65 | "
" + 66 | "
"; 70 | 71 | return r; 72 | } 73 | -------------------------------------------------------------------------------- /src/topo2geo.js: -------------------------------------------------------------------------------- 1 | // ****************************************************************** 2 | // topo2geo() allows to transform a topojson into geojson, if needed 3 | // ****************************************************************** 4 | 5 | import * as topojsonserver from "topojson-server"; 6 | import * as topojsonclient from "topojson-client"; 7 | const topojson = Object.assign({}, topojsonserver, topojsonclient); 8 | 9 | export function topo2geo(json) { 10 | if (json.type == "Topology") { 11 | return topojson.feature(json, Object.keys(json.objects)[0]); 12 | } 13 | return json; 14 | } 15 | -------------------------------------------------------------------------------- /src/view.js: -------------------------------------------------------------------------------- 1 | // **************************************************************** 2 | // view() is the main function. It returns the map to be displayed. 3 | // **************************************************************** 4 | 5 | // imports 6 | 7 | import turfbbox from "@turf/bbox"; 8 | import turfarea from "@turf/area"; 9 | import turflength from "@turf/length"; 10 | import maplibregl from "maplibre-gl"; 11 | 12 | // helpers 13 | 14 | import { info } from "./info.js"; 15 | import { figuration } from "./figuration.js"; 16 | import { topo2geo } from "./topo2geo.js"; 17 | 18 | export function* view(geojson, options = {}) { 19 | if (geojson) { 20 | const geojson_raw = geojson; 21 | geojson = topo2geo(geojson); 22 | const width = options.width != undefined ? options.width : 1000; 23 | const col = options.col != undefined ? options.col : "#be82c2"; 24 | const height = options.height != undefined ? options.height : 550; 25 | const radius = options.radius != undefined ? options.radius : 5; 26 | const fillOpacity = 27 | options.fillOpacity != undefined ? options.fillOpacity : 0.5; 28 | const renderWorldCopies = 29 | options.renderWorldCopies != undefined ? options.renderWorldCopies : true; 30 | const colOver = options.colOver != undefined ? options.colOver : "#ffd505"; 31 | const lineWidth = options.lineWidth; 32 | const basemap = options.style != undefined ? options.style : "voyager"; 33 | 34 | // basemaps 35 | const mapstyle = new Map([ 36 | ["night", "https://geoserveis.icgc.cat/contextmaps/night.json"], 37 | ["fulldark", "https://geoserveis.icgc.cat/contextmaps/fulldark.json"], 38 | [ 39 | "voyager", 40 | "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json", 41 | ], 42 | ["positron", "https://geoserveis.icgc.cat/contextmaps/positron.json"], 43 | ["icgc", "https://geoserveis.icgc.cat/contextmaps/icgc.json"], 44 | ["osmbright", "https://geoserveis.icgc.cat/contextmaps/osm-bright.json"], 45 | ["hibrid", "https://geoserveis.icgc.cat/contextmaps/hibrid.json"], 46 | ]); 47 | 48 | // Unique id to allow you to put several maps on one page 49 | const unique = Math.floor((1 + Math.random()) * 0x1000000000000) 50 | .toString(16) 51 | .substring(1); 52 | 53 | // css 54 | 55 | const css = "https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.css"; 56 | document.head.innerHTML += ``; 57 | const style = ` 58 | 59 | 143 | `; 144 | document.head.innerHTML += style; 145 | 146 | geojson.features.map((d, i) => (d.id = i + 1)); 147 | let hovereId = null; 148 | 149 | // bbox 150 | 151 | const fig = figuration(geojson); 152 | const turfbb = turfbbox(geojson); 153 | const bb = [ 154 | [turfbb[0], turfbb[1]], 155 | [turfbb[2], turfbb[3]], 156 | ]; 157 | 158 | // fields 159 | 160 | let result = []; 161 | const attr = geojson.features.map((d) => d.properties); 162 | attr.forEach((d) => result.push(Object.keys(d).length)); 163 | const keys = Object.keys(attr[result.indexOf(Math.max(...result))]); 164 | 165 | // map container 166 | 167 | let container = document.createElement("div"); 168 | container.setAttribute("style", `width:${width}px;height:${height}px`); 169 | container.innerHTML = ``; 174 | 175 | yield container; 176 | const map = (container.value = new maplibregl.Map({ 177 | container, 178 | style: mapstyle.get(basemap), 179 | scrollZoom: true, 180 | bounds: bb, 181 | attributionControl: false, 182 | renderWorldCopies: renderWorldCopies, 183 | })); 184 | 185 | map.on("load", function () { 186 | map.addSource("mygeojson", { 187 | type: "geojson", 188 | data: geojson, 189 | }); 190 | 191 | // If polygons 192 | 193 | if (fig[0] == "z") { 194 | map.addLayer({ 195 | id: "mygeojson", 196 | type: "fill", 197 | source: "mygeojson", 198 | paint: { 199 | "fill-color": [ 200 | "case", 201 | ["boolean", ["feature-state", "hover"], false], 202 | colOver, 203 | col, 204 | ], 205 | "fill-opacity": fillOpacity, 206 | }, 207 | }); 208 | 209 | map.addLayer({ 210 | id: "mygeojson-stroke", 211 | type: "line", 212 | source: "mygeojson", 213 | layout: { 214 | "line-join": "round", 215 | "line-cap": "round", 216 | }, 217 | paint: { 218 | "line-color": col, 219 | "line-width": lineWidth != undefined ? lineWidth : 1, 220 | }, 221 | }); 222 | } 223 | 224 | // If lines 225 | 226 | if (fig[0] == "l") { 227 | map.addLayer({ 228 | id: "mygeojson", 229 | type: "line", 230 | source: "mygeojson", 231 | layout: { 232 | "line-join": "round", 233 | "line-cap": "round", 234 | }, 235 | paint: { 236 | "line-color": [ 237 | "case", 238 | ["boolean", ["feature-state", "hover"], false], 239 | colOver, 240 | col, 241 | ], 242 | "line-width": lineWidth != undefined ? lineWidth : 3, 243 | }, 244 | }); 245 | } 246 | 247 | // If points 248 | 249 | if (fig[0] == "p") { 250 | map.addLayer({ 251 | id: "mygeojson", 252 | type: "circle", 253 | source: "mygeojson", 254 | paint: { 255 | "circle-color": [ 256 | "case", 257 | ["boolean", ["feature-state", "hover"], false], 258 | colOver, 259 | col, 260 | ], 261 | "circle-stroke-color": col, 262 | "circle-opacity": fillOpacity, 263 | "circle-stroke-width": lineWidth != undefined ? lineWidth : 1, 264 | "circle-radius": radius, 265 | }, 266 | }); 267 | } 268 | 269 | // Popup 270 | 271 | map.on("click", "mygeojson", function (e) { 272 | let type = e.features[0].geometry.type; 273 | let r = ""; 274 | r += "Geometries"; 275 | r += ""; 276 | r += ""; 277 | 278 | if (type == "Point") { 279 | r += 280 | ""; 283 | r += 284 | ""; 287 | } 288 | 289 | if (type == "MultiPolygon") { 290 | r += 291 | ""; 294 | } 295 | 296 | if (type == "MultiLineString") { 297 | r += 298 | ""; 301 | } 302 | 303 | if (type == "MultiPoint") { 304 | r += 305 | ""; 308 | } 309 | 310 | if (type != "Point") { 311 | const bb = turfbbox(e.features[0]); 312 | r += 313 | ""; 316 | r += 317 | ""; 320 | r += 321 | ""; 324 | r += 325 | ""; 328 | } 329 | 330 | if (type.indexOf("Polygon") != -1) { 331 | r += 332 | ""; 336 | } 337 | 338 | if (type.indexOf("LineString") != -1) { 339 | r += 340 | ""; 344 | } 345 | 346 | r += "
Type" + type + "
Latitude" + 281 | e.features[0].geometry.coordinates[1] + 282 | "
Longitude" + 285 | e.features[0].geometry.coordinates[0] + 286 | "
Nb of polygons" + 292 | e.features[0].geometry.coordinates.length + 293 | "
Nb of lines" + 299 | e.features[0].geometry.coordinates.length + 300 | "
Nb of points" + 306 | e.features[0].geometry.coordinates.length + 307 | "
Longitude min" + 314 | Math.round(bb[0] * 100) / 100 + 315 | "
Longitude max" + 318 | Math.round(bb[2] * 100) / 100 + 319 | "
Latitude min" + 322 | Math.round(bb[1] * 100) / 100 + 323 | "
Latitude max" + 326 | Math.round(bb[3] * 100) / 100 + 327 | "
Computed area" + 333 | Math.round(turfarea(e.features[0].geometry) / 10000) / 100 + 334 | " km²" + 335 | "
Computed length" + 341 | Math.round(turflength(e.features[0].geometry) * 100) / 100 + 342 | " km" + 343 | "
"; 347 | r += "Attribute data"; 348 | r += ""; 349 | keys.forEach( 350 | (d) => 351 | (r += 352 | "") 357 | ); 358 | r += "
" + 353 | d + 354 | "" + 355 | e.features[0].properties[d] + 356 | "
"; 359 | new maplibregl.Popup({ 360 | closeOnClick: true, 361 | closeOnMove: true, 362 | }) 363 | .setLngLat(e.lngLat) 364 | .setHTML(r) 365 | .addTo(map); 366 | }); 367 | 368 | // Pointer 369 | 370 | map.on("mouseenter", "mygeojson", function () { 371 | map.getCanvas().style.cursor = "pointer"; 372 | }); 373 | map.on("mouseleave", "mygeojson", function () { 374 | map.getCanvas().style.cursor = ""; 375 | }); 376 | 377 | // fit bounds 378 | 379 | map.fitBounds(bb, { 380 | padding: { top: 15, bottom: 15, left: 15, right: 15 }, 381 | }); 382 | 383 | // end onload 384 | }); 385 | 386 | // Over Effect 387 | 388 | map.on("mousemove", "mygeojson", function (e) { 389 | if (e.features.length > 0) { 390 | if (hovereId) { 391 | map.setFeatureState( 392 | { source: "mygeojson", id: hovereId }, 393 | { hover: false } 394 | ); 395 | } 396 | hovereId = e.features[0].id; 397 | map.setFeatureState( 398 | { source: "mygeojson", id: hovereId }, 399 | { hover: true } 400 | ); 401 | } 402 | }); 403 | 404 | map.on("mouseleave", "mygeojson", function () { 405 | if (hovereId) { 406 | map.setFeatureState( 407 | { source: "mygeojson", id: hovereId }, 408 | { hover: false } 409 | ); 410 | } 411 | hovereId = null; 412 | }); 413 | 414 | // Toogle Slider 415 | 416 | function toggleSidebar(id) { 417 | var elem = document.getElementById(id); 418 | var classes = elem.className.split(" "); 419 | var collapsed = classes.indexOf(`collapsed${unique}`) !== -1; 420 | 421 | var padding = {}; 422 | 423 | if (collapsed) { 424 | classes.splice(classes.indexOf(`collapsed${unique}`), 1); 425 | 426 | padding[id] = 300; 427 | map.easeTo({ 428 | padding: padding, 429 | duration: 1000, 430 | }); 431 | } else { 432 | padding[id] = 0; 433 | classes.push(`collapsed${unique}`); 434 | 435 | map.easeTo({ 436 | padding: padding, 437 | duration: 1000, 438 | }); 439 | } 440 | 441 | elem.className = classes.join(" "); 442 | } 443 | 444 | let slide = document.querySelector(`#toogle${unique}, #slidebar${unique}`); 445 | slide.addEventListener("click", function () { 446 | toggleSidebar(`slidebar${unique}`); 447 | }); 448 | 449 | //console.log(style); 450 | console.log(unique); 451 | 452 | // fullScreen & navigation 453 | 454 | map.addControl(new maplibregl.FullscreenControl()); 455 | map.addControl(new maplibregl.NavigationControl(), "top-right"); 456 | map.addControl( 457 | new maplibregl.AttributionControl({ 458 | customAttribution: `Made with geoverview.js`, 459 | compact: false, 460 | }) 461 | ); 462 | } 463 | } 464 | --------------------------------------------------------------------------------