Skip to content

Instantly share code, notes, and snippets.

@fabiovalse
Last active July 28, 2018 15:12
Show Gist options
  • Save fabiovalse/9e711f6d83f80e2f698cad58a5f75ba3 to your computer and use it in GitHub Desktop.
Save fabiovalse/9e711f6d83f80e2f698cad58a5f75ba3 to your computer and use it in GitHub Desktop.
Vue.js + Leaflet.js

An example about how Leaflet.js can be integrated with Vue.js.

{
"annotations": [
{
"p1": {
"x": 10745,
"y": -23.25
},
"p2": {
"x": 11283.75,
"y": -387
},
"text": {
"full": "坤與 萬國 全圗",
"tokens": [
{
"text": "坤與",
"graphemes": [
{
"text": "坤",
"tone": "1",
"transliteration": {
"pinyin": "kun",
"zhuyin_fuhao": "ㄎㄨㄣ"
},
"radical": "土",
"domain": "cosmologia",
"translation": {
"fr": "Globe terrestre; obéissance, soumission.",
"en": "The earth as contrasted with heaven."
}
},
{
"text": "與",
"tone": "2",
"transliteration": {
"pinyin": "yu",
"zhuyin_fuhao": "ㄩˊ"
},
"radical": "車",
"domain": "cosmologia",
"translation": {
"fr": "La plate-forme ou plancher d'une voiture. Voiture, chaise à porteurs, bard, civièr. La terre. ",
"en": "The bottom of a carriage. A carriage or chariot; a sedan-chair."
}
}
]
},
{
"text": "萬國",
"graphemes": [
{
"text": "萬",
"tone": "4",
"transliteration": {
"pinyin": "wan",
"zhuyin_fuhao": "ㄨㄢˋ"
},
"radical": "艸",
"domain": "",
"translation": {
"fr": "Dix mille; très nombreux; tout, entièrment.",
"en": "Ten thousand. Large numbers. All."
}
},
{
"text": "國",
"tone": "2",
"transliteration": {
"pinyin": "guo",
"zhuyin_fuhao": "ㄍㄨㄛˊ"
},
"radical": "囗",
"domain": "",
"translation": {
"fr": "État, royaume, principauté.",
"en": "A kingdom; a nation."
}
}
]
},
{
"text": "全圗",
"graphemes": [
{
"text": "全",
"tone": "2",
"transliteration": {
"pinyin": "quan",
"zhuyin_fuhao": "ㄑㄩㄢˊ"
},
"radical": "入",
"domain": "",
"translation": {
"fr": "Tout, entier, complet, parfait.",
"en": "Perfect, entire, the whole, all, complete."
}
},
{
"text": "圗",
"tone": "2",
"transliteration": {
"pinyin": "tu",
"zhuyin_fuhao": "ㄊㄨˊ"
},
"radical": "囗",
"domain": "",
"translation": {
"fr": "Delibération, dessein, projet, plan, détermination.",
"en": "A map; a picture; a diagram; a portrait."
}
}
]
}
],
"translation": {
"it": "Carta geografica completa di tutti i regni"
}
}
},
{
"p1": {
"x": 10745,
"y": -387
},
"p2": {
"x": 11283.75,
"y": -5075.5
},
"text1": "",
"text2": ""
},
{
"p1": {
"x": 9591.5,
"y": -22.25
},
"p2": {
"x": 9936,
"y": -357.25
},
"text1": "",
"text2": ""
},
{
"p1": {
"x": 10448.4,
"y": -852
},
"p2": {
"x": 10745,
"y": -1365.8
},
"text1": "",
"text2": ""
},
{
"p1": {
"x": 29,
"y": -1214
},
"p2": {
"x": 534.8,
"y": -3881
},
"text1": "",
"text2": ""
},
{
"p1": {
"x": 5949.4,
"y": -1920.8
},
"p2": {
"x": 6997.9,
"y": -2511.3
},
"text1": "",
"text2": ""
},
{
"path": [
{"x": 1418.1, "y": -1338.4},
{"x": 3050.1, "y": -959.4},
{"x": 3068.3, "y": -1041.9},
{"x": 1425.6, "y": -1401.2}
],
"text1": "歐邏巴",
"text2": "Europa (Ou Luo Ba)"
},
{
"path": [
{"x": 3854, "y": -794.2},
{"x": 3932, "y": -785},
{"x": 4109, "y": -2479.7},
{"x": 4044, "y": -2492}
],
"text1": "亞細亞",
"text2": "Asia (Ya Xi Ya)"
},
{
"path": [
{"x": 2095.4, "y": -1919.3},
{"x": 2173.9, "y": -1920},
{"x": 2116.3, "y": -3298.3},
{"x": 2038.3, "y": -3297.3}
],
"text1": "利未亞",
"text2": "Africa (Libya) (Li Wei Ya)"
},
{
"path": [
{"x": 8058.75, "y": -564.5},
{"x": 8134.75, "y": -565},
{"x": 8143.5, "y": -1763.75},
{"x": 8066, "y": -1762.75}
],
"text1": "北亞墨利加",
"text2": "North America (Bei Ya Mo Li Jia)"
},
{
"path": [
{"x": 9350.6, "y": -2286.5},
{"x": 9415.1, "y": -2285},
{"x": 9414.3, "y": -3735},
{"x": 9328.8, "y": -3737.5}
],
"text1": "南亞墨利加",
"text2": "South America (Nan Ya Mo Li Jia)"
}
]
}
leaflet_conf =
width: 45177
height: 20538
tileLayer: 'http://wafi.iit.cnr.it/webvis/tiles/vips_zoomify_kunyu_wanguo_quantu/'
realMaxZoom: 6
zoomControlPosition: 'bottomright'
map: {
minZoom: 1,
maxZoom: 8,
zoomControl: false,
crs: L.CRS.Simple
}
app = new Vue
el: '#app'
data:
annotations: []
conf: leaflet_conf
components:
'leaflet': window.leafletComponent
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#app {
display: flex;
width: 100%;
height: 100%;
}
#map {
flex-grow: 1;
}
.image_portion rect, .image_portion path {
stroke: steelblue;
fill: rgba(176,196,222,0.8);
}
.infobox {
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 100%;
background: #FFFFFF;
z-index: 1;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js + Leaflet.js</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<script src="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
<script src="L.D3SvgOverlay.js"></script>
<script src="L.TileLayer.Zoomify.js"></script>
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" />
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="app">
<leaflet :conf="conf"></leaflet>
</div>
<script src="leaflet.js"></script>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var app, leaflet_conf;
leaflet_conf = {
width: 45177,
height: 20538,
tileLayer: 'http://wafi.iit.cnr.it/webvis/tiles/vips_zoomify_kunyu_wanguo_quantu/',
realMaxZoom: 6,
zoomControlPosition: 'bottomright',
map: {
minZoom: 1,
maxZoom: 8,
zoomControl: false,
crs: L.CRS.Simple
}
};
app = new Vue({
el: '#app',
data: {
annotations: [],
conf: leaflet_conf
},
components: {
'leaflet': window.leafletComponent
}
});
}).call(this);
/**
* Copyright 2015 Teralytics AG
*
* @author Kirill Zhuravlev <kirill.zhuravlev@teralytics.ch>
*
*/
(function (factory) {
if (typeof define === 'function' && define.amd) {
define(['leaflet', 'd3'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('leaflet', 'd3'));
} else {
factory(L, d3);
}
}(function (L, d3) {
// Check requirements
if (typeof d3 == "undefined") {
throw "D3 SVG Overlay for Leaflet requires D3 library loaded first";
}
if (typeof L == "undefined") {
throw "D3 SVG Overlay for Leaflet requires Leaflet library loaded first";
}
// Tiny stylesheet bundled here instead of a separate file
if (L.version >= "1.0") {
d3.select("head")
.append("style").attr("type", "text/css")
.text("g.d3-overlay *{pointer-events:visiblePainted;}");
}
// Class definition
L.D3SvgOverlay = (L.version < "1.0" ? L.Class : L.Layer).extend({
includes: (L.version < "1.0" ? L.Mixin.Events : []),
_undef: function(a){ return typeof a == "undefined"; },
_options: function (options) {
if (this._undef(options)) {
return this.options;
}
options.zoomHide = this._undef(options.zoomHide) ? false : options.zoomHide;
options.zoomDraw = this._undef(options.zoomDraw) ? true : options.zoomDraw;
return this.options = options;
},
_disableLeafletRounding: function(){
this._leaflet_round = L.Point.prototype._round;
L.Point.prototype._round = function(){ return this; };
},
_enableLeafletRounding: function(){
L.Point.prototype._round = this._leaflet_round;
},
draw: function () {
this._disableLeafletRounding();
this._drawCallback(this.selection, this.projection, this.map.getZoom());
this._enableLeafletRounding();
},
initialize: function (drawCallback, options) { // (Function(selection, projection)), (Object)options
this._options(options || {});
this._drawCallback = drawCallback;
},
// Handler for "viewreset"-like events, updates scale and shift after the animation
_zoomChange: function (evt) {
this._disableLeafletRounding();
var newZoom = this._undef(evt.zoom) ? this.map._zoom : evt.zoom; // "viewreset" event in Leaflet has not zoom/center parameters like zoomanim
this._zoomDiff = newZoom - this._zoom;
this._scale = Math.pow(2, this._zoomDiff);
this.projection.scale = this._scale;
this._shift = this.map.latLngToLayerPoint(this._wgsOrigin)
._subtract(this._wgsInitialShift.multiplyBy(this._scale));
var shift = ["translate(", this._shift.x, ",", this._shift.y, ") "];
var scale = ["scale(", this._scale, ",", this._scale,") "];
this._rootGroup.attr("transform", shift.concat(scale).join(""));
if (this.options.zoomDraw) { this.draw() }
this._enableLeafletRounding();
},
onAdd: function (map) {
this.map = map;
var _layer = this;
// SVG element
if (L.version < "1.0") {
map._initPathRoot();
this._svg = d3.select(map._panes.overlayPane)
.select("svg");
this._rootGroup = this._svg.append("g");
} else {
this._svg = L.svg();
map.addLayer(this._svg);
this._rootGroup = d3.select(this._svg._rootGroup).classed("d3-overlay", true);
}
this._rootGroup.classed("leaflet-zoom-hide", this.options.zoomHide);
this.selection = this._rootGroup;
// Init shift/scale invariance helper values
this._pixelOrigin = map.getPixelOrigin();
this._wgsOrigin = L.latLng([0, 0]);
this._wgsInitialShift = this.map.latLngToLayerPoint(this._wgsOrigin);
this._zoom = this.map.getZoom();
this._shift = L.point(0, 0);
this._scale = 1;
// Create projection object
this.projection = {
latLngToLayerPoint: function (latLng, zoom) {
zoom = _layer._undef(zoom) ? _layer._zoom : zoom;
var projectedPoint = _layer.map.project(L.latLng(latLng), zoom)._round();
return projectedPoint._subtract(_layer._pixelOrigin);
},
layerPointToLatLng: function (point, zoom) {
zoom = _layer._undef(zoom) ? _layer._zoom : zoom;
var projectedPoint = L.point(point).add(_layer._pixelOrigin);
return _layer.map.unproject(projectedPoint, zoom);
},
unitsPerMeter: 256 * Math.pow(2, _layer._zoom) / 40075017,
map: _layer.map,
layer: _layer,
scale: 1
};
this.projection._projectPoint = function(x, y) {
var point = _layer.projection.latLngToLayerPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y);
};
this.projection.pathFromGeojson =
d3.geoPath().projection(d3.geoTransform({point: this.projection._projectPoint}));
// Compatibility with v.1
this.projection.latLngToLayerFloatPoint = this.projection.latLngToLayerPoint;
this.projection.getZoom = this.map.getZoom.bind(this.map);
this.projection.getBounds = this.map.getBounds.bind(this.map);
this.selection = this._rootGroup;
if (L.version < "1.0") map.on("viewreset", this._zoomChange, this);
// Initial draw
this.draw();
},
// Leaflet 1.0
getEvents: function() { return {zoomend: this._zoomChange}; },
onRemove: function (map) {
if (L.version < "1.0") {
map.off("viewreset", this._zoomChange, this);
this._rootGroup.remove();
} else {
this._svg.remove();
}
},
addTo: function (map) {
map.addLayer(this);
return this;
}
});
L.D3SvgOverlay.version = "2.2";
// Factory method
L.d3SvgOverlay = function (drawCallback, options) {
return new L.D3SvgOverlay(drawCallback, options);
};
}));
/*
* L.TileLayer.Zoomify display Zoomify tiles with Leaflet
*/
L.TileLayer.Zoomify = L.TileLayer.extend({
options: {
continuousWorld: true,
tolerance: 0.8
},
initialize: function (url, options) {
options = L.setOptions(this, options);
this._url = url;
var imageSize = L.point(options.width, options.height),
tileSize = options.tileSize;
this._imageSize = [imageSize];
this._gridSize = [this._getGridSize(imageSize)];
while (parseInt(imageSize.x) > tileSize || parseInt(imageSize.y) > tileSize) {
imageSize = imageSize.divideBy(2).floor();
this._imageSize.push(imageSize);
this._gridSize.push(this._getGridSize(imageSize));
}
this._imageSize.reverse();
this._gridSize.reverse();
this.options.maxZoom = this._gridSize.length - 1;
},
onAdd: function (map) {
L.TileLayer.prototype.onAdd.call(this, map);
var mapSize = map.getSize(),
zoom = this._getBestFitZoom(mapSize),
imageSize = this._imageSize[zoom],
center = map.options.crs.pointToLatLng(L.point(imageSize.x / 2, imageSize.y / 2), zoom);
map.setView(center, zoom, true);
},
_getGridSize: function (imageSize) {
var tileSize = this.options.tileSize;
return L.point(Math.ceil(imageSize.x / tileSize), Math.ceil(imageSize.y / tileSize));
},
_getBestFitZoom: function (mapSize) {
var tolerance = this.options.tolerance,
zoom = this._imageSize.length - 1,
imageSize, zoom;
while (zoom) {
imageSize = this._imageSize[zoom];
if (imageSize.x * tolerance < mapSize.x && imageSize.y * tolerance < mapSize.y) {
return zoom;
}
zoom--;
}
return zoom;
},
_tileShouldBeLoaded: function (tilePoint) {
var gridSize = this._gridSize[this._map.getZoom()];
return (tilePoint.x >= 0 && tilePoint.x < gridSize.x && tilePoint.y >= 0 && tilePoint.y < gridSize.y);
},
_addTile: function (tilePoint, container) {
var tilePos = this._getTilePos(tilePoint),
tile = this._getTile(),
zoom = this._map.getZoom(),
imageSize = this._imageSize[zoom],
gridSize = this._gridSize[zoom],
tileSize = this.options.tileSize;
if (tilePoint.x === gridSize.x - 1) {
tile.style.width = imageSize.x - (tileSize * (gridSize.x - 1)) + 'px';
}
if (tilePoint.y === gridSize.y - 1) {
tile.style.height = imageSize.y - (tileSize * (gridSize.y - 1)) + 'px';
}
L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23);
this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
this._loadTile(tile, tilePoint);
if (tile.parentNode !== this._tileContainer) {
container.appendChild(tile);
}
},
getTileUrl: function (tilePoint) {
return this._url + 'TileGroup' + this._getTileGroup(tilePoint) + '/' + this._map.getZoom() + '-' + tilePoint.x + '-' + tilePoint.y + '.jpg';
},
_getTileGroup: function (tilePoint) {
var zoom = this._map.getZoom(),
num = 0,
gridSize;
for (z = 0; z < zoom; z++) {
gridSize = this._gridSize[z];
num += gridSize.x * gridSize.y;
}
num += tilePoint.y * this._gridSize[zoom].x + tilePoint.x;
return Math.floor(num / 256);;
}
});
L.tileLayer.zoomify = function (url, options) {
return new L.TileLayer.Zoomify(url, options);
};
window.leafletComponent =
props:
conf:
type: Object
required: true
data: () ->
map: null
point: (x,y) -> @map.unproject([x,-y], @conf.realMaxZoom)
to_path_description: (projection, points) ->
str = ""
starting_point = "#{projection.latLngToLayerPoint(@point(points[0].x, points[0].y)).x} #{projection.latLngToLayerPoint(@point(points[0].x, points[0].y)).y}"
for p,i in points
if i > 0
str += "L #{projection.latLngToLayerPoint(@point(p.x, p.y)).x} #{projection.latLngToLayerPoint(@point(p.x, p.y)).y} "
return "M#{starting_point} #{str} L #{starting_point}"
loadOverlay: () ->
d3.json 'data.json', (data) =>
_this = this
overlay = L.d3SvgOverlay (selection, projection) =>
# Simple rectangles
rectangles = selection.selectAll '.rectangles'
.data data.annotations.filter (d) -> d.p1? and d.p2?
enter_rectangles = rectangles.enter().append 'g'
.attrs
class: 'rectangles image_portion'
.on 'click', (d) ->
console.log "#{d.text.full}"
enter_rectangles.append 'rect'
.attrs
width: (d) -> projection.latLngToLayerPoint(_this.point(d.p2.x, 0)).x - projection.latLngToLayerPoint(_this.point(d.p1.x, 0)).x
height: (d) -> projection.latLngToLayerPoint(_this.point(0, d.p2.y)).y - projection.latLngToLayerPoint(_this.point(0, d.p1.y)).y
x: (d) -> projection.latLngToLayerPoint(_this.point(d.p1.x, d.p1.y)).x
y: (d) -> projection.latLngToLayerPoint(_this.point(d.p1.x, d.p1.y)).y
'stroke-width': 5/_this.map.getZoom()
# Paths
paths = selection.selectAll '.path'
.data data.annotations.filter (d) -> d.path?
enter_paths = paths.enter().append 'g'
.attrs
class: 'path image_portion'
.on 'click', (d) ->
console.log "#{d.text.full}"
enter_paths.append 'path'
.attrs
d: (d) -> _this.to_path_description projection, d.path
'stroke-width': 5/_this.map.getZoom()
overlay.addTo(@map)
template: '''
<div id="map"></div>
'''
mounted: () ->
@map = L.map('map', @conf.map)
center = @point(@conf.width/2, @conf.height/2)
@map.setView(center, 2, {animate:false})
L.tileLayer.zoomify(@conf.tileLayer, {
width: @conf.width
height: @conf.height
}).addTo(@map)
L.control.zoom({
position: @conf.zoomControlPosition
}).addTo(@map)
@loadOverlay()
// Generated by CoffeeScript 1.10.0
(function() {
window.leafletComponent = {
props: {
conf: {
type: Object,
required: true
}
},
data: function() {
return {
map: null,
point: function(x, y) {
return this.map.unproject([x, -y], this.conf.realMaxZoom);
},
to_path_description: function(projection, points) {
var i, j, len, p, starting_point, str;
str = "";
starting_point = (projection.latLngToLayerPoint(this.point(points[0].x, points[0].y)).x) + " " + (projection.latLngToLayerPoint(this.point(points[0].x, points[0].y)).y);
for (i = j = 0, len = points.length; j < len; i = ++j) {
p = points[i];
if (i > 0) {
str += "L " + (projection.latLngToLayerPoint(this.point(p.x, p.y)).x) + " " + (projection.latLngToLayerPoint(this.point(p.x, p.y)).y) + " ";
}
}
return "M" + starting_point + " " + str + " L " + starting_point;
},
loadOverlay: function() {
return d3.json('data.json', (function(_this) {
return function(data) {
var overlay;
_this = _this;
overlay = L.d3SvgOverlay(function(selection, projection) {
var enter_paths, enter_rectangles, paths, rectangles;
rectangles = selection.selectAll('.rectangles').data(data.annotations.filter(function(d) {
return (d.p1 != null) && (d.p2 != null);
}));
enter_rectangles = rectangles.enter().append('g').attrs({
"class": 'rectangles image_portion'
}).on('click', function(d) {
return console.log("" + d.text.full);
});
enter_rectangles.append('rect').attrs({
width: function(d) {
return projection.latLngToLayerPoint(_this.point(d.p2.x, 0)).x - projection.latLngToLayerPoint(_this.point(d.p1.x, 0)).x;
},
height: function(d) {
return projection.latLngToLayerPoint(_this.point(0, d.p2.y)).y - projection.latLngToLayerPoint(_this.point(0, d.p1.y)).y;
},
x: function(d) {
return projection.latLngToLayerPoint(_this.point(d.p1.x, d.p1.y)).x;
},
y: function(d) {
return projection.latLngToLayerPoint(_this.point(d.p1.x, d.p1.y)).y;
},
'stroke-width': 5 / _this.map.getZoom()
});
paths = selection.selectAll('.path').data(data.annotations.filter(function(d) {
return d.path != null;
}));
enter_paths = paths.enter().append('g').attrs({
"class": 'path image_portion'
}).on('click', function(d) {
return console.log("" + d.text.full);
});
return enter_paths.append('path').attrs({
d: function(d) {
return _this.to_path_description(projection, d.path);
},
'stroke-width': 5 / _this.map.getZoom()
});
});
return overlay.addTo(_this.map);
};
})(this));
}
};
},
template: '<div id="map"></div>',
mounted: function() {
var center;
this.map = L.map('map', this.conf.map);
center = this.point(this.conf.width / 2, this.conf.height / 2);
this.map.setView(center, 2, {
animate: false
});
L.tileLayer.zoomify(this.conf.tileLayer, {
width: this.conf.width,
height: this.conf.height
}).addTo(this.map);
L.control.zoom({
position: this.conf.zoomControlPosition
}).addTo(this.map);
return this.loadOverlay();
}
};
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment