├── debug
├── css
│ ├── performance.css
│ └── debug.css
├── styles
│ ├── mapnik.png
│ ├── osmosnimki.png
│ ├── surface.js
│ └── osmosnimki.js
├── kothic-include.js
├── tile.html
└── index.html
├── .gitignore
├── package.json
├── LICENSE
├── src
├── utils
│ ├── collisions.js
│ ├── geom.js
│ └── rbush.js
├── renderer
│ ├── polygon.js
│ ├── line.js
│ ├── texticons.js
│ ├── shields.js
│ ├── path.js
│ └── text.js
├── style
│ ├── style.js
│ └── mapcss.js
└── kothic.js
├── README.md
├── Gruntfile.js
└── dist
└── kothic-leaflet.js
/debug/css/performance.css:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/kothic*.js
2 | node_modules
3 | .idea
4 |
--------------------------------------------------------------------------------
/debug/styles/mapnik.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kothic/kothic-js/HEAD/debug/styles/mapnik.png
--------------------------------------------------------------------------------
/debug/styles/osmosnimki.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kothic/kothic-js/HEAD/debug/styles/osmosnimki.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kothic-js",
3 | "version": "0.3.0",
4 | "devDependencies": {
5 | "grunt": "^1.0.4",
6 | "grunt-contrib-jshint": "^2.1.0",
7 | "grunt-contrib-concat": "^1.0.1",
8 | "grunt-contrib-uglify": "^4.0.1"
9 | },
10 | "scripts": {
11 | "prepublish": "grunt"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/debug/css/debug.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 | html, body, #map {
6 | height: 100%;
7 | }
8 | #debug {
9 | position: absolute;
10 | top: 0;
11 | right: 0;
12 | background: rgba(255,255,255,0.7);
13 | border: 1px solid #ddd;
14 | padding: 10px;
15 | z-index: 10000;
16 | font: 12px/1.4 Verdana, sans-serif;
17 | width: 170px;
18 | }
19 | #trace {
20 | margin-top: 10px;
21 | font: 10px/1 Verdana, sans-serif;
22 | }
23 | #trace table {
24 | margin-top: 3px;
25 | }
26 | #mapnik {
27 | margin-bottom: 10px;
28 | }
29 | table {
30 | border-collapse: collapse;
31 | }
32 | table td {
33 | padding-right: 10px;
34 | }
35 |
36 | #trace .tileIndex {
37 | font-weight: bold;
38 | }
39 |
40 | td.time {
41 | text-align: right;
42 | }
43 |
--------------------------------------------------------------------------------
/debug/kothic-include.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var scripts = [
3 | 'kothic.js',
4 | 'renderer/path.js',
5 | 'renderer/line.js',
6 | 'renderer/polygon.js',
7 | 'renderer/shields.js',
8 | 'renderer/texticons.js',
9 | 'renderer/text.js',
10 | 'style/mapcss.js',
11 | 'style/style.js',
12 | 'utils/collisions.js',
13 | 'utils/geom.js',
14 | 'utils/rbush.js'
15 | ];
16 |
17 | function getSrcUrl() {
18 | var scripts = document.getElementsByTagName('script');
19 | for (var i = 0; i < scripts.length; i++) {
20 | var src = scripts[i].src;
21 | if (src) {
22 | var res = src.match(/^(.*)kothic-include\.js$/);
23 | if (res) {
24 | return res[1] + '../src/';
25 | }
26 | }
27 | }
28 |
29 | return "";
30 | }
31 |
32 | var path = getSrcUrl();
33 | for (var i = 0; i < scripts.length; i++) {
34 | document.writeln("");
35 | }
36 | })();
37 |
--------------------------------------------------------------------------------
/debug/styles/surface.js:
--------------------------------------------------------------------------------
1 |
2 | (function (MapCSS) {
3 | 'use strict';
4 |
5 | function restyle(style, tags, zoom, type, selector) {
6 | var s_default = {}, s_overlay = {};
7 |
8 | if (((type == 'way' && tags['highway'] == 'primary' && (!tags.hasOwnProperty('surface'))))) {
9 | s_overlay['color'] = '#f00';
10 | s_overlay['width'] = 1;
11 | s_overlay['z-index'] = 100;
12 | }
13 |
14 | if (Object.keys(s_default).length) {
15 | style['default'] = s_default;
16 | }
17 | if (Object.keys(s_overlay).length) {
18 | style['overlay'] = s_overlay;
19 | }
20 | return style;
21 | }
22 |
23 | var sprite_images = {
24 | }, external_images = [], presence_tags = ['surface'], value_tags = ['highway'];
25 |
26 | MapCSS.loadStyle('surface', restyle, sprite_images, external_images, presence_tags, value_tags);
27 | MapCSS.preloadExternalImages('surface');
28 | })(MapCSS);
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2013, Darafei Praliaskouski, Vladimir Agafonkin, Maksim Gurtovenko
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are
5 | permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this list of
8 | conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list
11 | of conditions and the following disclaimer in the documentation and/or other materials
12 | provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
17 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
21 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23 |
--------------------------------------------------------------------------------
/src/utils/collisions.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.CollisionBuffer = function (height, width) {
3 | this.buffer = rbush();
4 | this.height = height;
5 | this.width = width;
6 | };
7 |
8 | Kothic.CollisionBuffer.prototype = {
9 | addPointWH: function (point, w, h, d, id) {
10 | this.buffer.insert(this.getBoxFromPoint(point, w, h, d, id));
11 | },
12 |
13 | addPoints: function (params) {
14 | var points = [];
15 | for (var i = 0, len = params.length; i < len; i++) {
16 | points.push(this.getBoxFromPoint.apply(this, params[i]));
17 | }
18 | this.buffer.load(points);
19 | },
20 |
21 | checkBox: function (b, id) {
22 | var result = this.buffer.search(b),
23 | i, len;
24 |
25 | if (b[0] < 0 || b[1] < 0 || b[2] > this.width || b[3] > this.height) { return true; }
26 |
27 | for (i = 0, len = result.length; i < len; i++) {
28 | // if it's the same object (only different styles), don't detect collision
29 | if (id !== result[i][4]) {
30 | return true;
31 | }
32 | }
33 |
34 | return false;
35 | },
36 |
37 | checkPointWH: function (point, w, h, id) {
38 | return this.checkBox(this.getBoxFromPoint(point, w, h, 0), id);
39 | },
40 |
41 | getBoxFromPoint: function (point, w, h, d, id) {
42 | var dx = w / 2 + d,
43 | dy = h / 2 + d;
44 |
45 | return [
46 | point[0] - dx,
47 | point[1] - dy,
48 | point[0] + dx,
49 | point[1] + dy,
50 | id
51 | ];
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/renderer/polygon.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.polygon = {
3 | render: function (ctx, feature, nextFeature, ws, hs, granularity) {
4 | var style = feature.style,
5 | nextStyle = nextFeature && nextFeature.style;
6 |
7 | if (!this.pathOpened) {
8 | this.pathOpened = true;
9 | ctx.beginPath();
10 | }
11 |
12 | Kothic.path(ctx, feature, false, true, ws, hs, granularity);
13 |
14 | if (nextFeature &&
15 | (nextStyle['fill-color'] === style['fill-color']) &&
16 | (nextStyle['fill-image'] === style['fill-image']) &&
17 | (nextStyle['fill-opacity'] === style['fill-opacity'])) {
18 | return;
19 | }
20 |
21 | this.fill(ctx, style);
22 |
23 | this.pathOpened = false;
24 | },
25 |
26 | fill: function (ctx, style, fillFn) {
27 | var opacity = style["fill-opacity"] || style.opacity, image;
28 |
29 | if (style.hasOwnProperty('fill-color')) {
30 | // first pass fills with solid color
31 | Kothic.style.setStyles(ctx, {
32 | fillStyle: style["fill-color"] || "#000000",
33 | globalAlpha: opacity || 1
34 | });
35 | if (fillFn) {
36 | fillFn();
37 | } else {
38 | ctx.fill();
39 | }
40 | }
41 |
42 | if (style.hasOwnProperty('fill-image')) {
43 | // second pass fills with texture
44 | image = MapCSS.getImage(style['fill-image']);
45 | if (image) {
46 | Kothic.style.setStyles(ctx, {
47 | fillStyle: ctx.createPattern(image, 'repeat'),
48 | globalAlpha: opacity || 1
49 | });
50 | if (fillFn) {
51 | fillFn();
52 | } else {
53 | ctx.fill();
54 | }
55 | }
56 | }
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Kothic JS** is a full-featured JavaScript map rendering engine using HTML5 Canvas.
2 | It was initially developed as a JavaScript port of [Kothic](http://wiki.openstreetmap.org/wiki/Kothic) rendering engine written in Python.
3 |
4 | Check out the demo: http://kothic.org/
5 |
6 | ### Features
7 |
8 | * Rendering [OpenStreetMap](http://openstreetmap.org) data visually on par with [Mapnik](http://mapnik.org)
9 | * [MapCSS](http://wiki.openstreetmap.org/wiki/MapCSS/0.2) support (see [How to Prepare a Map Style](https://github.com/kothic/kothic-js/wiki/How-to-prepare-map-style))
10 | * rendering from lightweight GeoJSON-like tiles (see [Tiles Format](https://github.com/kothic/kothic-js/wiki/Tiles-format))
11 | * easy integration with [Leaflet](http://leaflet.cloudmade.com) (interactive maps library)
12 |
13 | ### Building Kothic
14 |
15 | Install Node.js, then run:
16 |
17 | ```
18 | npm install
19 | npm install -g grunt-cli
20 | grunt
21 | ```
22 |
23 | Minified Kothic source will be generated in the `dist` folder.
24 |
25 | ### Basic usage
26 |
27 | Include `kothic.js` from the `dist` folder on your page. Now you can call:
28 |
29 | ```javascript
30 | Kothic.render(
31 | canvas, // canvas element (or its id) to render on
32 | data, // JSON data to render
33 | zoom, // zoom level
34 | {
35 | onRenderComplete: callback, // (optional) callback to call when rendering is done
36 | styles: ['osmosnimki-maps', 'surface'], // (optional) only specified styles will be rendered, if any
37 | locales: ['be', 'ru', 'en'] // (optional) map languages, see below
38 | });
39 | ```
40 |
41 | `locales` Kothic-JS supports map localization based on name:*lang* tags. Renderer will check all mentioned languages in order of persence. If object doesn't have localized name, *name* tag will be used.
42 |
43 | ### Contributing to Kothic JS
44 |
45 | Kothic JS is licensed under a BSD license, and we'll be glad to accept your contributions!
46 |
47 | #### Core contributors:
48 |
49 | * Darafei Praliaskouski ([@Komzpa](https://github.com/Komzpa))
50 | * Vladimir Agafonkin ([@mourner](https://github.com/mourner), creator of [Leaflet](http://leafletjs.com))
51 | * Maksim Gurtovenko ([@Miroff](https://github.com/Miroff))
52 |
53 | * Leaflet 1.x compatibility, Stephan Brandt ([@braandl](https://github.com/braandl))
--------------------------------------------------------------------------------
/debug/tile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Kothic JS tile test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*global module:false*/
2 | module.exports = function (grunt) {
3 | 'use strict';
4 |
5 | grunt.initConfig({
6 | jshint: {
7 | options: {
8 | strict: false,
9 |
10 | bitwise: false,
11 | curly: false,
12 | eqeqeq: true,
13 | immed: true,
14 | latedef: true,
15 | newcap: true,
16 | noarg: true,
17 | noempty: true,
18 | nonew: true,
19 | sub: true,
20 | undef: false,
21 | unused: true,
22 |
23 | globals: {
24 | MapCSS: true,
25 | L: true,
26 | Kothic: true,
27 | console: true,
28 | rbush: true
29 | },
30 |
31 | // camelcase: true,
32 | trailing: true,
33 | indent: 4,
34 | // quotmark: 'single',
35 | // maxlen: 120,
36 |
37 | // force breaking complex functions into smaller ones for readability
38 | // maxstatements: 10,
39 | // maxcomplexity: 5,
40 |
41 | browser: true
42 | },
43 | all: {
44 | src: ['Gruntfile.js', 'src/**/*.js', 'dist/kothic-leaflet.js', '!src/utils/rbush.js']
45 | }
46 | },
47 |
48 | concat: {
49 | options: {
50 | separator: ';'
51 | },
52 | dist: {
53 | src: ['src/**/*.js'],
54 | dest: 'dist/kothic.js'
55 | }
56 | },
57 |
58 | uglify: {
59 | options: {
60 | banner: '/*\n (c) 2011-2019, Darafei Praliaskouski, Vladimir Agafonkin, Maksim Gurtovenko, Stephan Brandt\n' +
61 | ' Kothic JS is a full-featured JavaScript map rendering engine using HTML5 Canvas.\n' +
62 | ' Built on <%= grunt.template.today("dd-mm-yyyy") %> |' +
63 | ' https://github.com/kothic/kothic-js\n*/\n'
64 | },
65 | dist: {
66 | files: {
67 | 'dist/kothic.min.js': ['<%= concat.dist.dest %>']
68 | }
69 | }
70 | },
71 | });
72 |
73 | grunt.loadNpmTasks('grunt-contrib-jshint');
74 | grunt.loadNpmTasks('grunt-contrib-concat');
75 | grunt.loadNpmTasks('grunt-contrib-uglify');
76 |
77 | grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
78 | };
79 |
--------------------------------------------------------------------------------
/src/renderer/line.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.line = {
3 |
4 | renderCasing: function (ctx, feature, nextFeature, ws, hs, granularity) {
5 | var style = feature.style,
6 | nextStyle = nextFeature && nextFeature.style;
7 |
8 | if (!this.pathOpened) {
9 | this.pathOpened = true;
10 | ctx.beginPath();
11 | }
12 |
13 | Kothic.path(ctx, feature, style["casing-dashes"] || style.dashes, false, ws, hs, granularity);
14 |
15 | if (nextFeature &&
16 | nextStyle.width === style.width &&
17 | nextStyle['casing-width'] === style['casing-width'] &&
18 | nextStyle['casing-color'] === style['casing-color'] &&
19 | nextStyle['casing-dashes'] === style['casing-dashes'] &&
20 | nextStyle['casing-opacity'] === style['casing-opacity']) {
21 | return;
22 | }
23 |
24 | Kothic.style.setStyles(ctx, {
25 | lineWidth: 2 * style["casing-width"] + (style.hasOwnProperty("width") ? style.width : 0),
26 | strokeStyle: style["casing-color"] || "#000000",
27 | lineCap: style["casing-linecap"] || style.linecap || "butt",
28 | lineJoin: style["casing-linejoin"] || style.linejoin || "round",
29 | globalAlpha: style["casing-opacity"] || 1
30 | });
31 |
32 | ctx.stroke();
33 | this.pathOpened = false;
34 | },
35 |
36 | render: function (ctx, feature, nextFeature, ws, hs, granularity) {
37 | var style = feature.style,
38 | nextStyle = nextFeature && nextFeature.style;
39 |
40 | if (!this.pathOpened) {
41 | this.pathOpened = true;
42 | ctx.beginPath();
43 | }
44 |
45 | Kothic.path(ctx, feature, style.dashes, false, ws, hs, granularity);
46 |
47 | if (nextFeature &&
48 | nextStyle.width === style.width &&
49 | nextStyle.color === style.color &&
50 | nextStyle.image === style.image &&
51 | nextStyle.opacity === style.opacity) {
52 | return;
53 | }
54 |
55 | if ('color' in style || !('image' in style)) {
56 | var t_width = style.width || 1,
57 | t_linejoin = "round",
58 | t_linecap = "round";
59 |
60 | if (t_width <= 2) {
61 | t_linejoin = "miter";
62 | t_linecap = "butt";
63 | }
64 | Kothic.style.setStyles(ctx, {
65 | lineWidth: t_width,
66 | strokeStyle: style.color || '#000000',
67 | lineCap: style.linecap || t_linecap,
68 | lineJoin: style.linejoin || t_linejoin,
69 | globalAlpha: style.opacity || 1,
70 | miterLimit: 4
71 | });
72 | ctx.stroke();
73 | }
74 |
75 |
76 | if ('image' in style) {
77 | // second pass fills with texture
78 | var image = MapCSS.getImage(style.image);
79 |
80 | if (image) {
81 | Kothic.style.setStyles(ctx, {
82 | strokeStyle: ctx.createPattern(image, 'repeat') || "#000000",
83 | lineWidth: style.width || 1,
84 | lineCap: style.linecap || "round",
85 | lineJoin: style.linejoin || "round",
86 | globalAlpha: style.opacity || 1
87 | });
88 |
89 | ctx.stroke();
90 | }
91 | }
92 | this.pathOpened = false;
93 | },
94 |
95 | pathOpened: false
96 | };
97 |
--------------------------------------------------------------------------------
/src/utils/geom.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.geom = {
3 | transformPoint: function (point, ws, hs) {
4 | return [ws * point[0], hs * point[1]];
5 | },
6 |
7 | transformPoints: function (points, ws, hs) {
8 | var transformed = [], i, len;
9 | for (i = 0, len = points.length; i < len; i++) {
10 | transformed.push(this.transformPoint(points[i], ws, hs));
11 | }
12 | return transformed;
13 | },
14 |
15 | getReprPoint: function (feature) {
16 | var point, len;
17 | switch (feature.type) {
18 | case 'Point':
19 | point = feature.coordinates;
20 | break;
21 | case 'Polygon':
22 | point = feature.reprpoint;
23 | break;
24 | case 'LineString':
25 | len = Kothic.geom.getPolyLength(feature.coordinates);
26 | point = Kothic.geom.getAngleAndCoordsAtLength(feature.coordinates, len / 2, 0);
27 | point = [point[1], point[2]];
28 | break;
29 | case 'GeometryCollection':
30 | //TODO: Disassemble geometry collection
31 | return;
32 | case 'MultiPoint':
33 | //TODO: Disassemble multi point
34 | return;
35 | case 'MultiPolygon':
36 | point = feature.reprpoint;
37 | break;
38 | case 'MultiLineString':
39 | //TODO: Disassemble geometry collection
40 | return;
41 | }
42 | return point;
43 | },
44 |
45 | getPolyLength: function (points) {
46 | var pointsLen = points.length,
47 | c, pc, i,
48 | dx, dy,
49 | len = 0;
50 |
51 | for (i = 1; i < pointsLen; i++) {
52 | c = points[i];
53 | pc = points[i - 1];
54 | dx = pc[0] - c[0];
55 | dy = pc[1] - c[1];
56 | len += Math.sqrt(dx * dx + dy * dy);
57 | }
58 | return len;
59 | },
60 |
61 | getAngleAndCoordsAtLength: function (points, dist, width) {
62 | var pointsLen = points.length,
63 | dx, dy, x, y,
64 | i, c, pc,
65 | len = 0,
66 | segLen = 0,
67 | angle, partLen, sameseg = true,
68 | gotxy = false;
69 |
70 | width = width || 0; // by default we think that a letter is 0 px wide
71 |
72 | for (i = 1; i < pointsLen; i++) {
73 | if (gotxy) {
74 | sameseg = false;
75 | }
76 |
77 | c = points[i];
78 | pc = points[i - 1];
79 |
80 | dx = c[0] - pc[0];
81 | dy = c[1] - pc[1];
82 | segLen = Math.sqrt(dx * dx + dy * dy);
83 |
84 | if (!gotxy && len + segLen >= dist) {
85 | partLen = dist - len;
86 | x = pc[0] + dx * partLen / segLen;
87 | y = pc[1] + dy * partLen / segLen;
88 |
89 | gotxy = true;
90 | }
91 |
92 | if (gotxy && len + segLen >= dist + width) {
93 | partLen = dist + width - len;
94 | dx = pc[0] + dx * partLen / segLen;
95 | dy = pc[1] + dy * partLen / segLen;
96 | angle = Math.atan2(dy - y, dx - x);
97 |
98 | if (sameseg) {
99 | return [angle, x, y, segLen - partLen];
100 | } else {
101 | return [angle, x, y, 0];
102 | }
103 | }
104 |
105 | len += segLen;
106 | }
107 | }
108 | };
109 |
110 |
--------------------------------------------------------------------------------
/src/renderer/texticons.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.texticons = {
3 |
4 | render: function (ctx, feature, collides, ws, hs, renderText, renderIcon) {
5 | var style = feature.style, img, point, w, h;
6 |
7 | if (renderIcon || (renderText && feature.type !== 'LineString')) {
8 | var reprPoint = Kothic.geom.getReprPoint(feature);
9 | if (!reprPoint) {
10 | return;
11 | }
12 | point = Kothic.geom.transformPoint(reprPoint, ws, hs);
13 | }
14 |
15 | if (renderIcon) {
16 | img = MapCSS.getImage(style['icon-image']);
17 | if (!img) { return; }
18 |
19 | w = img.width;
20 | h = img.height;
21 |
22 | if (style['icon-width'] || style['icon-height']){
23 | if (style['icon-width']) {
24 | w = style['icon-width'];
25 | h = img.height * w / img.width;
26 | }
27 | if (style['icon-height']) {
28 | h = style['icon-height'];
29 | if (!style['icon-width']) {
30 | w = img.width * h / img.height;
31 | }
32 | }
33 | }
34 | if ((style['allow-overlap'] !== 'true') &&
35 | collides.checkPointWH(point, w, h, feature.kothicId)) {
36 | return;
37 | }
38 | }
39 |
40 | var text = String(style.text).trim();
41 |
42 | if (renderText && text) {
43 | Kothic.style.setStyles(ctx, {
44 | lineWidth: style['text-halo-radius'] * 2,
45 | font: Kothic.style.getFontString(style['font-family'], style['font-size'], style)
46 | });
47 |
48 | var halo = (style.hasOwnProperty('text-halo-radius'));
49 |
50 | Kothic.style.setStyles(ctx, {
51 | fillStyle: style['text-color'] || '#000000',
52 | strokeStyle: style['text-halo-color'] || '#ffffff',
53 | globalAlpha: style['text-opacity'] || style.opacity || 1,
54 | textAlign: 'center',
55 | textBaseline: 'middle'
56 | });
57 |
58 | if (style['text-transform'] === 'uppercase')
59 | text = text.toUpperCase();
60 | else if (style['text-transform'] === 'lowercase')
61 | text = text.toLowerCase();
62 | else if (style['text-transform'] === 'capitalize')
63 | text = text.replace(/(^|\s)\S/g, function(ch) { return ch.toUpperCase(); });
64 |
65 | if (feature.type === 'Polygon' || feature.type === 'Point') {
66 | var textWidth = ctx.measureText(text).width,
67 | letterWidth = textWidth / text.length,
68 | collisionWidth = textWidth,
69 | collisionHeight = letterWidth * 2.5,
70 | offset = style['text-offset'] || 0;
71 |
72 | if ((style['text-allow-overlap'] !== 'true') &&
73 | collides.checkPointWH([point[0], point[1] + offset], collisionWidth, collisionHeight, feature.kothicId)) {
74 | return;
75 | }
76 |
77 | if (halo) {
78 | ctx.strokeText(text, point[0], point[1] + offset);
79 | }
80 | ctx.fillText(text, point[0], point[1] + offset);
81 |
82 | var padding = style['-x-kot-min-distance'] || 20;
83 | collides.addPointWH([point[0], point[1] + offset], collisionWidth, collisionHeight, padding, feature.kothicId);
84 |
85 | } else if (feature.type === 'LineString') {
86 |
87 | var points = Kothic.geom.transformPoints(feature.coordinates, ws, hs);
88 | Kothic.textOnPath(ctx, points, text, halo, collides);
89 | }
90 | }
91 |
92 | if (renderIcon) {
93 | ctx.drawImage(img,
94 | Math.floor(point[0] - w / 2),
95 | Math.floor(point[1] - h / 2), w, h);
96 |
97 | var padding2 = parseFloat(style['-x-kot-min-distance']) || 0;
98 | collides.addPointWH(point, w, h, padding2, feature.kothicId);
99 | }
100 | }
101 | };
102 |
--------------------------------------------------------------------------------
/src/style/style.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.style = {
3 |
4 | defaultCanvasStyles: {
5 | strokeStyle: 'rgba(0,0,0,0.5)',
6 | fillStyle: 'rgba(0,0,0,0.5)',
7 | lineWidth: 1,
8 | lineCap: 'round',
9 | lineJoin: 'round',
10 | textAlign: 'center',
11 | textBaseline: 'middle'
12 | },
13 |
14 | populateLayers: function (features, zoom, styles) {
15 | var layers = {},
16 | i, len, feature, layerId, layerStyle;
17 |
18 | var styledFeatures = Kothic.style.styleFeatures(features, zoom, styles);
19 |
20 | for (i = 0, len = styledFeatures.length; i < len; i++) {
21 | feature = styledFeatures[i];
22 | layerStyle = feature.style['-x-mapnik-layer'];
23 | layerId = !layerStyle ? feature.properties.layer || 0 :
24 | layerStyle === 'top' ? 10000 : -10000;
25 |
26 | layers[layerId] = layers[layerId] || [];
27 | layers[layerId].push(feature);
28 | }
29 |
30 | return layers;
31 | },
32 |
33 | getStyle: function (feature, zoom, styleNames) {
34 | var shape = feature.type,
35 | type, selector;
36 | if (shape === 'LineString' || shape === 'MultiLineString') {
37 | type = 'way';
38 | selector = 'line';
39 | } else if (shape === 'Polygon' || shape === 'MultiPolygon') {
40 | type = 'way';
41 | selector = 'area';
42 | } else if (shape === 'Point' || shape === 'MultiPoint') {
43 | type = 'node';
44 | selector = 'node';
45 | }
46 |
47 | return MapCSS.restyle(styleNames, feature.properties !== null ? feature.properties : [], zoom, type, selector);
48 | },
49 |
50 | styleFeatures: function (features, zoom, styleNames) {
51 | var styledFeatures = [],
52 | i, j, len, feature, style, restyledFeature, k;
53 |
54 | for (i = 0, len = features.length; i < len; i++) {
55 | feature = features[i];
56 | style = this.getStyle(feature, zoom, styleNames);
57 |
58 | for (j in style) {
59 | if (j === 'default') {
60 | restyledFeature = feature;
61 | } else {
62 | restyledFeature = {};
63 | for (k in feature) {
64 | restyledFeature[k] = feature[k];
65 | }
66 | }
67 |
68 | restyledFeature.kothicId = i + 1;
69 | restyledFeature.style = style[j];
70 | restyledFeature.zIndex = style[j]['z-index'] || 0;
71 | restyledFeature.sortKey = (style[j]['fill-color'] || '') + (style[j].color || '');
72 | styledFeatures.push(restyledFeature);
73 | }
74 | }
75 |
76 | styledFeatures.sort(function (a, b) {
77 | return a.zIndex !== b.zIndex ? a.zIndex - b.zIndex :
78 | a.sortKey < b.sortKey ? -1 :
79 | a.sortKey > b.sortKey ? 1 : 0;
80 | });
81 |
82 | return styledFeatures;
83 | },
84 |
85 | getFontString: function (name, size, st) {
86 | name = name || '';
87 | size = size || 9;
88 |
89 | var family = name ? name + ', ' : '';
90 |
91 | name = name.toLowerCase();
92 |
93 | var styles = [];
94 | if (st['font-style'] === 'italic' || st['font-style'] === 'oblique') {
95 | styles.push(st['font-style']);
96 | }
97 | if (st['font-variant'] === 'small-caps') {
98 | styles.push(st['font-variant']);
99 | }
100 | if (st['font-weight'] === 'bold') {
101 | styles.push(st['font-weight']);
102 | }
103 |
104 | styles.push(size + 'px');
105 |
106 | if (name.indexOf('serif') !== -1 && name.indexOf('sans-serif') === -1) {
107 | family += 'Georgia, serif';
108 | } else {
109 | family += '"Helvetica Neue", Arial, Helvetica, sans-serif';
110 | }
111 | styles.push(family);
112 |
113 | return styles.join(' ');
114 | },
115 |
116 | setStyles: function (ctx, styles) {
117 | var i;
118 | for (i in styles) {
119 | if (styles.hasOwnProperty(i)) {
120 | ctx[i] = styles[i];
121 | }
122 | }
123 | }
124 | };
125 |
--------------------------------------------------------------------------------
/src/renderer/shields.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.shields = {
3 | render: function (ctx, feature, collides, ws, hs) {
4 | var style = feature.style, reprPoint = Kothic.geom.getReprPoint(feature),
5 | point, img, len = 0, found = false, i, sgn;
6 |
7 | if (!reprPoint) {
8 | return;
9 | }
10 |
11 | point = Kothic.geom.transformPoint(reprPoint, ws, hs);
12 |
13 | if (style["shield-image"]) {
14 | img = MapCSS.getImage(style["icon-image"]);
15 |
16 | if (!img) {
17 | return;
18 | }
19 | }
20 |
21 | Kothic.style.setStyles(ctx, {
22 | font: Kothic.style.getFontString(style["shield-font-family"] || style["font-family"], style["shield-font-size"] || style["font-size"], style),
23 | fillStyle: style["shield-text-color"] || "#000000",
24 | globalAlpha: style["shield-text-opacity"] || style.opacity || 1,
25 | textAlign: 'center',
26 | textBaseline: 'middle'
27 | });
28 |
29 | var text = String(style['shield-text']),
30 | textWidth = ctx.measureText(text).width,
31 | letterWidth = textWidth / text.length,
32 | collisionWidth = textWidth + 2,
33 | collisionHeight = letterWidth * 1.8;
34 |
35 | if (feature.type === 'LineString') {
36 | len = Kothic.geom.getPolyLength(feature.coordinates);
37 |
38 | if (Math.max(collisionHeight / hs, collisionWidth / ws) > len) {
39 | return;
40 | }
41 |
42 | for (i = 0, sgn = 1; i < len / 2; i += Math.max(len / 30, collisionHeight / ws), sgn *= -1) {
43 | reprPoint = Kothic.geom.getAngleAndCoordsAtLength(feature.coordinates, len / 2 + sgn * i, 0);
44 | if (!reprPoint) {
45 | break;
46 | }
47 |
48 | reprPoint = [reprPoint[1], reprPoint[2]];
49 |
50 | point = Kothic.geom.transformPoint(reprPoint, ws, hs);
51 | if (img && (style["allow-overlap"] !== "true") &&
52 | collides.checkPointWH(point, img.width, img.height, feature.kothicId)) {
53 | continue;
54 | }
55 | if ((style["allow-overlap"] !== "true") &&
56 | collides.checkPointWH(point, collisionWidth, collisionHeight, feature.kothicId)) {
57 | continue;
58 | }
59 | found = true;
60 | break;
61 | }
62 | }
63 |
64 | if (!found) {
65 | return;
66 | }
67 |
68 | if (style["shield-casing-width"]) {
69 | Kothic.style.setStyles(ctx, {
70 | fillStyle: style["shield-casing-color"] || "#000000",
71 | globalAlpha: style["shield-casing-opacity"] || style.opacity || 1
72 | });
73 | var p = style["shield-casing-width"] + (style["shield-frame-width"] || 0);
74 | ctx.fillRect(point[0] - collisionWidth / 2 - p,
75 | point[1] - collisionHeight / 2 - p,
76 | collisionWidth + 2 * p,
77 | collisionHeight + 2 * p);
78 | }
79 |
80 | if (style["shield-frame-width"]) {
81 | Kothic.style.setStyles(ctx, {
82 | fillStyle: style["shield-frame-color"] || "#000000",
83 | globalAlpha: style["shield-frame-opacity"] || style.opacity || 1
84 | });
85 | ctx.fillRect(point[0] - collisionWidth / 2 - style["shield-frame-width"],
86 | point[1] - collisionHeight / 2 - style["shield-frame-width"],
87 | collisionWidth + 2 * style["shield-frame-width"],
88 | collisionHeight + 2 * style["shield-frame-width"]);
89 | }
90 |
91 | if (style["shield-color"]) {
92 | Kothic.style.setStyles(ctx, {
93 | fillStyle: style["shield-color"] || "#000000",
94 | globalAlpha: style["shield-opacity"] || style.opacity || 1
95 | });
96 | ctx.fillRect(point[0] - collisionWidth / 2,
97 | point[1] - collisionHeight / 2,
98 | collisionWidth,
99 | collisionHeight);
100 | }
101 |
102 | if (img) {
103 | ctx.drawImage(img,
104 | Math.floor(point[0] - img.width / 2),
105 | Math.floor(point[1] - img.height / 2));
106 | }
107 | Kothic.style.setStyles(ctx, {
108 | fillStyle: style["shield-text-color"] || "#000000",
109 | globalAlpha: style["shield-text-opacity"] || style.opacity || 1
110 | });
111 |
112 | ctx.fillText(text, point[0], Math.ceil(point[1]));
113 | if (img) {
114 | collides.addPointWH(point, img.width, img.height, 0, feature.kothicId);
115 | }
116 |
117 | collides.addPointWH(point, collisionHeight, collisionWidth,
118 | (parseFloat(style["shield-casing-width"]) || 0) + (parseFloat(style["shield-frame-width"]) || 0) + (parseFloat(style["-x-mapnik-min-distance"]) || 30), feature.kothicId);
119 |
120 | }
121 | };
122 |
--------------------------------------------------------------------------------
/src/renderer/path.js:
--------------------------------------------------------------------------------
1 |
2 | Kothic.path = (function () {
3 | // check if the point is on the tile boundary
4 | // returns bitmask of affected tile boundaries
5 | function isTileBoundary(p, size) {
6 | var r = 0;
7 | if (p[0] === 0)
8 | r |= 1;
9 | else if (p[0] === size)
10 | r |= 2;
11 | if (p[1] === 0)
12 | r |= 4;
13 | else if (p[1] === size)
14 | r |= 8;
15 | return r;
16 | }
17 |
18 | /* check if 2 points are both on the same tile boundary
19 | *
20 | * If points of the object are on the same tile boundary it is assumed
21 | * that the object is cut here and would originally continue beyond the
22 | * tile borders.
23 | *
24 | * This does not catch the case where the object is indeed exactly
25 | * on the tile boundaries, but this case can't properly be detected here.
26 | */
27 | function checkSameBoundary(p, q, size) {
28 | var bp = isTileBoundary(p, size);
29 | if (!bp)
30 | return 0;
31 |
32 | return (bp & isTileBoundary(q, size));
33 | }
34 |
35 | return function (ctx, feature, dashes, fill, ws, hs, granularity) {
36 | var type = feature.type,
37 | coords = feature.coordinates;
38 |
39 | if (type === "Polygon") {
40 | coords = [coords];
41 | type = "MultiPolygon";
42 | } else if (type === "LineString") {
43 | coords = [coords];
44 | type = "MultiLineString";
45 | }
46 |
47 | var i, j, k,
48 | points,
49 | len = coords.length,
50 | len2, pointsLen,
51 | prevPoint, point, screenPoint,
52 | dx, dy, dist;
53 |
54 | if (type === "MultiPolygon") {
55 | for (i = 0; i < len; i++) {
56 | for (k = 0, len2 = coords[i].length; k < len2; k++) {
57 | points = coords[i][k];
58 | pointsLen = points.length;
59 | prevPoint = points[0];
60 |
61 | for (j = 0; j <= pointsLen; j++) {
62 | point = points[j] || points[0];
63 | screenPoint = Kothic.geom.transformPoint(point, ws, hs);
64 |
65 | if (j === 0) {
66 | ctx.moveTo(screenPoint[0], screenPoint[1]);
67 | if (dashes)
68 | ctx.setLineDash(dashes);
69 | else
70 | ctx.setLineDash([]);
71 | } else if (!fill && checkSameBoundary(point, prevPoint, granularity)) {
72 | ctx.moveTo(screenPoint[0], screenPoint[1]);
73 | } else {
74 | ctx.lineTo(screenPoint[0], screenPoint[1]);
75 | }
76 | prevPoint = point;
77 | }
78 | }
79 | }
80 | } else if (type === "MultiLineString") {
81 | var pad = 50, // how many pixels to draw out of the tile to avoid path edges when lines crosses tile borders
82 | skip = 2; // do not draw line segments shorter than this
83 |
84 | for (i = 0; i < len; i++) {
85 | points = coords[i];
86 | pointsLen = points.length;
87 |
88 | for (j = 0; j < pointsLen; j++) {
89 | point = points[j];
90 |
91 | // continue path off the tile by some amount to fix path edges between tiles
92 | if ((j === 0 || j === pointsLen - 1) && isTileBoundary(point, granularity)) {
93 | k = j;
94 | do {
95 | k = j ? k - 1 : k + 1;
96 | if (k < 0 || k >= pointsLen)
97 | break;
98 | prevPoint = points[k];
99 |
100 | dx = point[0] - prevPoint[0];
101 | dy = point[1] - prevPoint[1];
102 | dist = Math.sqrt(dx * dx + dy * dy);
103 | } while (dist <= skip);
104 |
105 | // all points are so close to each other that it doesn't make sense to
106 | // draw the line beyond the tile border, simply skip the entire line from
107 | // here
108 | if (k < 0 || k >= pointsLen)
109 | break;
110 |
111 | point[0] = point[0] + pad * dx / dist;
112 | point[1] = point[1] + pad * dy / dist;
113 | }
114 | screenPoint = Kothic.geom.transformPoint(point, ws, hs);
115 |
116 | if (j === 0) {
117 | ctx.moveTo(screenPoint[0], screenPoint[1]);
118 | if (dashes)
119 | ctx.setLineDash(dashes);
120 | else
121 | ctx.setLineDash([]);
122 | } else {
123 | ctx.lineTo(screenPoint[0], screenPoint[1]);
124 | }
125 | }
126 | }
127 | }
128 | };
129 | }());
130 |
--------------------------------------------------------------------------------
/debug/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Kothic debug page
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
OpenStreetMap data rendered on the browser using Kothic JS
36 |
41 |
Surface overlay
42 |
Rendering...
43 |
44 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/dist/kothic-leaflet.js:
--------------------------------------------------------------------------------
1 | L.TileLayer.Kothic = L.GridLayer.extend({
2 | options: {
3 | tileSize: 256,
4 | zoomOffset: 0,
5 | minZoom: 2,
6 | maxZoom: 22,
7 | updateWhenIdle: true,
8 | unloadInvisibleTiles: true,
9 | attribution: 'Map data © 2019 OpenStreetMap contributors,' +
10 | ' Rendering by Kothic JS',
11 | async: true,
12 | buffered: false,
13 | styles: MapCSS.availableStyles
14 | },
15 |
16 | initialize: function(url,options) {
17 | L.Util.setOptions(this, options);
18 |
19 | this._url = url;
20 | this._canvases = {};
21 | this._debugMessages = [];
22 |
23 | window.onKothicDataResponse = L.Util.bind(this._onKothicDataResponse, this);
24 | },
25 |
26 | _onKothicDataResponse: function(data, zoom, x, y, done) {
27 | var error;
28 | var key = [zoom, x, y].join('/'),
29 | canvas = this._canvases[key],
30 | zoomOffset = this.options.zoomOffset;
31 |
32 | if (!canvas) {
33 | return;
34 | }
35 |
36 | function onRenderComplete() {
37 | done(error, canvas);
38 | }
39 |
40 | this._invertYAxe(data);
41 |
42 | var styles = this.options.styles;
43 |
44 | Kothic.render(canvas, data, zoom + zoomOffset, {
45 | styles: styles,
46 | locales: ['be', 'ru', 'en'],
47 | onRenderComplete: onRenderComplete
48 | });
49 |
50 | delete this._canvases[key];
51 | },
52 |
53 | getDebugMessages: function() {
54 | return this._debugMessages;
55 | },
56 |
57 | createTile: function(tilePoint, done) {
58 | // create a