Coding soon

Coding soon

GeoJSON

geojson html
+
<div id="geojson-wrap"></div>
geojson css
+
#geojson-wrap {
	position: relative;
	height: 50vh;
}

.snippet-content:fullscreen #geojson-wrap {
	height: 100vh;
}

:root {
	--back: #ffffff;
	--over: #cacaca;
	--fill: #f4f4f4;
	--text: #000000;
}

@media (prefers-color-scheme: dark) {

	:root {
		--back: #1a1a1a;
		--over: #2D2D2D;
		--fill: #292929;
		--text: #FFFFFF;
	}

}

#map {
	width: 100%;
	height: 100%;
	display: block;
	touch-action: none;
}

.dept {
	/* opacity: 0; */
	fill: var(--fill);
	/* stroke: var(--strk); */
	stroke-width: 0.5;
	transition: fill 0.2s ease-out;
	touch-action: none;
}


.dept:hover {
	/* handled by JS */
	/* fill: #c5e1f7; */
}

@media (prefers-color-scheme: dark) {

	.dept:hover {
		/* handled by JS */
		/* fill: #1e3a4f; */
	}

}

#info {
	position: absolute;
	bottom: 5px;
	left: 5px;
	padding: 5px;
	background-color: var(--over);
	border-radius: 4px;
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
	width: 128px;
	pointer-events: none;
	opacity: 0;
	visibility: hidden;
	transition: visibility 0.2s ease, opacity 0.2s ease;
}

#info.visible {
	opacity: 1;
	visibility: visible;
}

#cnt {
	user-select: none;
	-webkit-user-select: none;
	white-space: pre;
	text-overflow: ellipsis;
	overflow: hidden;
}

#thmb {
	width: 100%;
	height: 128px;
	margin-top: 4px;
	display: block;
	background-color: var(--back);
	border-radius: 4px;
}

@media (prefers-color-scheme: dark) {
	#info {
		box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
	}

}
GeoJSON js
+
class FranceMap {

	constructor(wrap) {

		this.mapSize = 1000;
		this.preSize = 1000;

		this.bounds = {
			minLon: -5.1, maxLon: 9.6,
			minLat: 41.3, maxLat: 51.1
		};

		this.xOffset 
		= this.yOffset 
		= this.scale 
		= this.centerLonScale 
		= 0;

		this.infoPanel 
		= this.infoPanelContent 
		= this.previewSvg 
		= this.currentPreviewPath 
		= null;

		this.cols = null;
		this.mapSvg = this.create("svg:svg",
			{
				id: "map",
				viewBox: "0 0 " + this.mapSize + " " + this.mapSize
			});

		this.mapSvg.addEventListener("pointerdown",
			evt => {

				evt.preventDefault();
				evt.target.releasePointerCapture(evt.pointerId);
		
			});

		wrap.appendChild(this.mapSvg);
		this.setupBounds();
		this.setupInfoPanel(wrap);
		this.loadGeoJSON();

		this.darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
		this.darkModeMediaQuery.addEventListener("change",
			() => 
				this.updatePreviewColors());
	
	}

	setupBounds() {

		const width = this.bounds.maxLon - this.bounds.minLon;
		const height = this.bounds.maxLat - this.bounds.minLat;
		const centerLat = (this.bounds.maxLat + this.bounds.minLat) / 2;
		
		this.centerLonScale = Math.cos(centerLat * Math.PI / 180);
		
		const scaleX = this.mapSize / (width * this.centerLonScale);
		const scaleY = this.mapSize / height;

		this.scale = Math.min(scaleX,
			scaleY);

		const mapWidth = width * this.scale * this.centerLonScale;
		const mapHeight = height * this.scale;
		
		this.xOffset = (this.mapSize - mapWidth) / 2;
		this.yOffset = (this.mapSize - mapHeight) / 2;
	
	}

	projectCoordinate([lon, lat]) {

		const x = (lon - this.bounds.minLon) * this.scale * this.centerLonScale;
		const y = (this.bounds.maxLat - lat) * this.scale;

		return [x + this.xOffset, y + this.yOffset];
	
	}

	generatePath(feature) {

		const coordinates = feature["geometry"]["coordinates"];

		return feature["geometry"]["type"] === "Polygon" 
			? this.polygonToPath([coordinates])
			: this.polygonToPath(coordinates);
	
	}

	polygonToPath(polygons) {

		return polygons.reduce((path, polygon) => {

			const [outerRing, ...innerRings] = polygon;
			const points = outerRing.map(coord => 
				this.projectCoordinate(coord)
				.join(","));
			
			path += `M${points[0]}L${points.slice(1)
			.join("L")}Z`;
			
			innerRings.forEach(ring => {

				const holePoints = ring.map(coord => 
					this.projectCoordinate(coord)
					.join(","));

				path += `M${holePoints[0]}L${holePoints.slice(1)
				.join("L")}Z`;
			
			});
			
			return path;
		
		},
		"");
	
	}

	setupInfoPanel(wrap) {

		this.infoPanel = this.create("div",
			{ id: "info" });
		this.infoPanelContent = this.create("div",
			{ id: "cnt" });
		this.previewSvg = this.create("svg:svg",
			{
				id: "thmb",
				viewBox: "0 0 " + this.preSize + " " + this.preSize
			});
		
		this.infoPanel.append(this.infoPanelContent,
			this.previewSvg);
		wrap.appendChild(this.infoPanel);
	
	}

	async loadGeoJSON() {

		try {

			// const geofile = "https://github.com/gregoiredavid/france-geojson/blob/master/departements-version-simplifiee.geojson";
			const geofile = "media/geojson/departements-version-simplifiee.geojson";

			const response = await fetch(geofile);
			const features = (await response.json())["features"];

			this.renderMap(features);
		
		}
		catch(err) {

			console.log("geojson error",
				err);
		
		}
	
	}

	getDepartmentCenter(feature) {

		const geom = feature["geometry"];
		const coordinates = geom["coordinates"];
		let points = [];
		
		const geometryType = geom["type"];
		
		if(geometryType === "Polygon") {

			points = coordinates[0]; // Use outer ring
		
		}
		else if(geometryType === "MultiPolygon") {

			// Use the largest polygon's outer ring
			points = coordinates.reduce((largest, polygon) => {

				return polygon[0].length > largest.length ? polygon[0] : largest;
			
			},
			coordinates[0][0]);
		
		}
		
		// Calculate average of points
		const sum = points.reduce(
			([sumX, sumY], [x, y]) => 
				[sumX + x, sumY + y],
			[0, 0]
		);
		
		return [
			sum[0] / points.length,
			sum[1] / points.length
		];
	
	}

	renderMap(features) {

		const deptsWithPos = features
		.map(feature => {

			const [lon, lat] = this.getDepartmentCenter(feature);
			const [x, y] = this.projectCoordinate([lon, lat]);

			return { feature, y, path: this.generatePath(feature) };
		
		})
		.sort((a, b) => 
			a.y - b.y);

		const fragment = document.createDocumentFragment();

		deptsWithPos.forEach((parsed, idx) => {

			const props = parsed.feature["properties"];

			const path = this.create("svg:path",
				{
					"d": parsed.path,
					"class": "dept",
					"data-name": props["nom"],
					"data-code": props["code"]
				});
			
			// fix mobile devices over & out bug
			path.addEventListener("pointerdown",
				evt => {

					evt.preventDefault();
					evt.target.releasePointerCapture(evt.pointerId);
			
				});
			path.addEventListener("pointerover",
				() => 
					this.showDepartmentInfo(parsed.feature));
			path.addEventListener("pointerout",
				() => 
					this.hideDepartmentInfo());

			setTimeout(
				() => {

					path.style.opacity = 1;
					path.style.stroke = "#666666";
				
				}, 
				512 + Math.round(parsed.y) * 2
				// 256 + Math.round(Math.random() * 2048)
			);
			
			fragment.appendChild(path);
		
		});

		this.mapSvg.appendChild(fragment);
	
	}

	updatePreviewColors() {

		if(!this.currentPreviewPath) 
			return;

		const isDarkMode = this.darkModeMediaQuery.matches;

		this.setAttributes(this.currentPreviewPath,
			{
				fill: isDarkMode ? "#2d2d2d" : "#f4f4f4",
				stroke: isDarkMode ? "#666666" : "#333333"
			});
	
	}

	showDepartmentInfo(feature) {

		while(this.previewSvg.firstChild) 
			this.previewSvg.removeChild(this.previewSvg.firstChild);

		this.currentPreviewPath = this.create("svg:path",
			{
				"d": this.generatePath(feature)
			});
		
		this.updatePreviewColors();
		this.previewSvg.appendChild(this.currentPreviewPath);
		
		const bbox = this.currentPreviewPath.getBBox();
		const padding = bbox.width * 0.05;
		const bboxArea = bbox.width * bbox.height;
		
		this.setAttributes(this.previewSvg,
			{
				viewBox: `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`
			});
		
		this.setAttributes(this.currentPreviewPath,
			{
				"stroke-width": String(5 * Math.sqrt(bboxArea) / this.preSize)
			});

		const props = feature["properties"];

		this.infoPanelContent.textContent = `${props["code"]} ${props["nom"]}`;
		this.infoPanel.classList.add("visible");
	
	}

	hideDepartmentInfo() {

		this.infoPanel.classList.remove("visible");
	
	}

	create(tag, attributes = {}, children = []) {

		const element = tag.includes("svg:") 
			? document.createElementNS("http://www.w3.org/2000/svg",
				tag.replace("svg:",
					""))
			: document.createElement(tag);

		Object.entries(attributes)
		.forEach(([key, value]) => 
			element.setAttribute(key,
				value));

		children.forEach(child => 
			element.appendChild(child));
		return element;
	
	}

	setAttributes(element, attributes) {

		Object.entries(attributes)
		.forEach(([key, value]) => 
			element.setAttribute(key,
				value));
	
	}

}

new FranceMap(document.querySelector("#geojson-wrap"));