├── .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 | 
3 |
4 | 
5 | 
6 | 
7 | 
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 | 
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 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/img/geoverview.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Geo o verview
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 += "Type " + type + " ";
40 | r += "Features " + fig + " ";
41 | r +=
42 | "Count " +
43 | nbfeat +
44 | " ";
45 | r +=
46 | "Bounding box " +
47 | turfbbox(geojson)
48 | .map((d) => Math.round(d * 100) / 100)
49 | .join(", ") +
50 | " ";
51 | r += "
";
52 | r += "Attribute data (completeness)";
53 | r += `
`;
54 | keys.forEach(
55 | (d) =>
56 | (r +=
57 | "" +
58 | d +
59 | " " +
62 | pct.get(d) +
63 | " /" +
64 | geojson.features.length +
65 | "
" +
66 | " ")
67 | );
68 |
69 | r += "
";
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 += "Type " + type + " ";
277 |
278 | if (type == "Point") {
279 | r +=
280 | "Latitude " +
281 | e.features[0].geometry.coordinates[1] +
282 | " ";
283 | r +=
284 | "Longitude " +
285 | e.features[0].geometry.coordinates[0] +
286 | " ";
287 | }
288 |
289 | if (type == "MultiPolygon") {
290 | r +=
291 | "Nb of polygons " +
292 | e.features[0].geometry.coordinates.length +
293 | " ";
294 | }
295 |
296 | if (type == "MultiLineString") {
297 | r +=
298 | "Nb of lines " +
299 | e.features[0].geometry.coordinates.length +
300 | " ";
301 | }
302 |
303 | if (type == "MultiPoint") {
304 | r +=
305 | "Nb of points " +
306 | e.features[0].geometry.coordinates.length +
307 | " ";
308 | }
309 |
310 | if (type != "Point") {
311 | const bb = turfbbox(e.features[0]);
312 | r +=
313 | "Longitude min " +
314 | Math.round(bb[0] * 100) / 100 +
315 | " ";
316 | r +=
317 | "Longitude max " +
318 | Math.round(bb[2] * 100) / 100 +
319 | " ";
320 | r +=
321 | "Latitude min " +
322 | Math.round(bb[1] * 100) / 100 +
323 | " ";
324 | r +=
325 | "Latitude max " +
326 | Math.round(bb[3] * 100) / 100 +
327 | " ";
328 | }
329 |
330 | if (type.indexOf("Polygon") != -1) {
331 | r +=
332 | "Computed area " +
333 | Math.round(turfarea(e.features[0].geometry) / 10000) / 100 +
334 | " km²" +
335 | " ";
336 | }
337 |
338 | if (type.indexOf("LineString") != -1) {
339 | r +=
340 | "Computed length " +
341 | Math.round(turflength(e.features[0].geometry) * 100) / 100 +
342 | " km" +
343 | " ";
344 | }
345 |
346 | r += "
";
347 | r += "Attribute data";
348 | r += "";
349 | keys.forEach(
350 | (d) =>
351 | (r +=
352 | "" +
353 | d +
354 | " " +
355 | e.features[0].properties[d] +
356 | " ")
357 | );
358 | r += "
";
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 |
--------------------------------------------------------------------------------