Add osm-map-nano
This commit is contained in:
commit
bedef43e13
22 changed files with 739 additions and 0 deletions
12
packages/osm-map-nano/index.js
Normal file
12
packages/osm-map-nano/index.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import { Mapper } from './src/Mapper.js';
|
||||
|
||||
module.exports = {Mapper};
|
||||
|
||||
// module.exports = require('./src/index.js');
|
||||
|
||||
// if(process.env.NODE_ENV === 'production')
|
||||
// module.exports = require('./dist/index.min.cjs');
|
||||
// else
|
||||
// module.exports = require('./dist/index.cjs');
|
||||
13
packages/osm-map-nano/package.json
Normal file
13
packages/osm-map-nano/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "osm-map-nano",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"lint": "eslint packages/*",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
}
|
||||
}
|
||||
313
packages/osm-map-nano/src/Mapper.ts
Normal file
313
packages/osm-map-nano/src/Mapper.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
namespace OSM
|
||||
{
|
||||
export type Tags = {
|
||||
[key: string]: string
|
||||
};
|
||||
|
||||
export class Bounds {
|
||||
constructor(
|
||||
public max_lat: number,
|
||||
public max_lon: number,
|
||||
public min_lat: number,
|
||||
public min_lon: number) {}
|
||||
}
|
||||
|
||||
export class Member {
|
||||
constructor(
|
||||
public ref: number,
|
||||
public role: string,
|
||||
public type: string) {}
|
||||
}
|
||||
|
||||
export class Node {
|
||||
constructor(
|
||||
public id: number,
|
||||
public lat: number,
|
||||
public lon: number,
|
||||
public tags: Tags,
|
||||
public uid: number,
|
||||
public version: number,
|
||||
public visible: boolean) {}
|
||||
}
|
||||
|
||||
export class Relation {
|
||||
constructor(
|
||||
public id: number,
|
||||
public members: Member[],
|
||||
public tags: Tags,
|
||||
public uid: number,
|
||||
public version: number,
|
||||
public visible: boolean) {}
|
||||
}
|
||||
|
||||
export class Way {
|
||||
constructor(
|
||||
public id: number,
|
||||
public nodes: Node[],
|
||||
public tags: Tags,
|
||||
public uid: number,
|
||||
public version: number,
|
||||
public visible: boolean) {}
|
||||
}
|
||||
}
|
||||
|
||||
interface MapperOptions {
|
||||
bounds?: OSM.Bounds,
|
||||
canvas?: HTMLCanvasElement
|
||||
}
|
||||
|
||||
export class Mapper
|
||||
{
|
||||
current_bounds: OSM.Bounds | null;
|
||||
data_bounds: OSM.Bounds | null;
|
||||
nodes: Map<Number, OSM.Node> = new Map();
|
||||
relations: Map<Number, OSM.Relation> = new Map();
|
||||
ways: Map<Number, OSM.Way> = new Map();
|
||||
|
||||
canvas: HTMLCanvasElement;
|
||||
zoom_level: number = 0;
|
||||
zoom_scale: number = 0;
|
||||
|
||||
private readonly DEG2RAD: number = Math.PI / 180;
|
||||
private readonly PI_4: number = Math.PI / 4;
|
||||
|
||||
constructor({bounds, canvas}: MapperOptions = {})
|
||||
{
|
||||
this.current_bounds = bounds || null;
|
||||
this.data_bounds = bounds || null;
|
||||
this.canvas = canvas || document.createElement('canvas');
|
||||
}
|
||||
|
||||
private getXMLTags(element: Element): OSM.Tags
|
||||
{
|
||||
let tags: OSM.Tags = {};
|
||||
element.querySelectorAll('tag').forEach(tag => {
|
||||
tags[tag.getAttribute('k') || ''] = tag.getAttribute('v') || '';
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
private getXMLVisible(element: Element): boolean
|
||||
{
|
||||
const visible: string = element.getAttribute('visible') || 'false';
|
||||
return visible === '1' || visible.toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
public loadXMLData(xml_data: string)
|
||||
{
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xml_data, 'application/xml');
|
||||
const doc_bounds = doc.querySelector('bounds');
|
||||
if(doc_bounds !== null)
|
||||
{
|
||||
this.data_bounds = new OSM.Bounds(
|
||||
parseFloat(doc_bounds.getAttribute('maxlat') || '0'),
|
||||
parseFloat(doc_bounds.getAttribute('maxlon') || '0'),
|
||||
parseFloat(doc_bounds.getAttribute('minlat') || '0'),
|
||||
parseFloat(doc_bounds.getAttribute('minlon') || '0')
|
||||
);
|
||||
this.current_bounds = this.data_bounds;
|
||||
}
|
||||
doc.querySelectorAll('node').forEach(doc_node => {
|
||||
const node_id: number = parseInt(doc_node.getAttribute('id') || '0');
|
||||
this.nodes.set(node_id, new OSM.Node(
|
||||
node_id,
|
||||
parseFloat(doc_node.getAttribute('lat') || '0'),
|
||||
parseFloat(doc_node.getAttribute('lon') || '0'),
|
||||
this.getXMLTags(doc_node),
|
||||
parseFloat(doc_node.getAttribute('uid') || '0'),
|
||||
parseFloat(doc_node.getAttribute('version') || '0'),
|
||||
this.getXMLVisible(doc_node)
|
||||
));
|
||||
});
|
||||
|
||||
doc.querySelectorAll('way').forEach(doc_way => {
|
||||
const way_id: number = parseInt(doc_way.getAttribute('id') || '0');
|
||||
let nodes: OSM.Node[] = [];
|
||||
doc_way.querySelectorAll('nd').forEach(way_nd => {
|
||||
const node_id: number = parseInt(way_nd.getAttribute('ref') || '0');
|
||||
const node = this.nodes.get(node_id);
|
||||
if(node === undefined)
|
||||
throw `Invalid data : reference to non existent Node ${node_id} from Way ${way_id}`;
|
||||
nodes.push(node);
|
||||
});
|
||||
this.ways.set(way_id, new OSM.Way(
|
||||
way_id,
|
||||
nodes,
|
||||
this.getXMLTags(doc_way),
|
||||
parseFloat(doc_way.getAttribute('uid') || '0'),
|
||||
parseFloat(doc_way.getAttribute('version') || '0'),
|
||||
this.getXMLVisible(doc_way)
|
||||
));
|
||||
});
|
||||
|
||||
doc.querySelectorAll('relation').forEach(doc_relation => {
|
||||
const relation_id: number = parseInt(doc_relation.getAttribute('id') || '0');
|
||||
let members: OSM.Member[] = [];
|
||||
doc_relation.querySelectorAll('member').forEach(member => {
|
||||
members.push(new OSM.Member(
|
||||
parseInt(member.getAttribute('ref') || '0'),
|
||||
member.getAttribute('role') || '',
|
||||
member.getAttribute('rype') || ''));
|
||||
});
|
||||
this.relations.set(relation_id, new OSM.Relation(
|
||||
relation_id,
|
||||
members,
|
||||
this.getXMLTags(doc_relation),
|
||||
parseFloat(doc_relation.getAttribute('uid') || '0'),
|
||||
parseFloat(doc_relation.getAttribute('version') || '0'),
|
||||
this.getXMLVisible(doc_relation)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
private get_map_raw_coords(lon: number, lat: number): [number, number]
|
||||
{
|
||||
return [
|
||||
this.zoom_scale * lon * this.DEG2RAD,
|
||||
this.zoom_scale * Math.log(Math.tan(this.PI_4 + (lat * this.DEG2RAD / 2)))
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public draw(target_width: number, canvas?: HTMLCanvasElement)
|
||||
{
|
||||
if(canvas)
|
||||
this.canvas = canvas;
|
||||
if(this.current_bounds === null || this.data_bounds === null)
|
||||
return;
|
||||
|
||||
this.zoom_level = Math.ceil(-Math.log2(
|
||||
((this.current_bounds.max_lon - this.current_bounds.min_lon) * 2 * Math.PI) / target_width));
|
||||
this.zoom_scale = (2**this.zoom_level) * target_width / (2 * Math.PI);
|
||||
const [min_x, min_y] = this.get_map_raw_coords(this.current_bounds.min_lon, this.current_bounds.min_lat);
|
||||
const [max_x, max_y] = this.get_map_raw_coords(this.current_bounds.max_lon, this.current_bounds.max_lat);
|
||||
const width = max_x - min_x;
|
||||
const height = max_y - min_y;
|
||||
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
let ctx = this.canvas.getContext('2d', {alpha: false}) as CanvasRenderingContext2D;
|
||||
|
||||
const get_map_coords: (lon: number, lat:number) => [number, number] = (lon: number, lat: number) => {
|
||||
const [x, y] = this.get_map_raw_coords(lon, lat);
|
||||
return [Math.floor(x - min_x), Math.floor(max_y - y)];
|
||||
};
|
||||
|
||||
const background_color = 'rgb(235, 235, 235)';
|
||||
ctx.fillStyle = background_color;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
const draw_way_polygon = (way: OSM.Way, color: string, outline: string | null = null,
|
||||
width: number = 1) => {
|
||||
let path_len: number = 0;
|
||||
let previous_coords: [number, number] | null = null;
|
||||
let path = new Path2D();
|
||||
for(const node of way.nodes)
|
||||
{
|
||||
const coords = get_map_coords(node.lon, node.lat);
|
||||
if(previous_coords === null || previous_coords[0] !== coords[0] || previous_coords[1] !== coords[1])
|
||||
{
|
||||
if(previous_coords === null)
|
||||
path.moveTo(coords[0], coords[1]);
|
||||
else
|
||||
path.lineTo(coords[0], coords[1]);
|
||||
path_len += 1;
|
||||
previous_coords = coords;
|
||||
}
|
||||
}
|
||||
path.closePath();
|
||||
if(path_len > 2)
|
||||
{
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill(path);
|
||||
if(outline !== null)
|
||||
{
|
||||
ctx.lineWidth = width;
|
||||
ctx.stroke(path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const draw_way_line = (way: OSM.Way, width: number, color: string) => {
|
||||
let path_len: number = 0;
|
||||
let previous_coords: [number, number] | null = null;
|
||||
let path = new Path2D();
|
||||
for(const node of way.nodes)
|
||||
{
|
||||
const coords = get_map_coords(node.lon, node.lat);
|
||||
if(previous_coords === null || previous_coords[0] !== coords[0] || previous_coords[1] !== coords[1])
|
||||
{
|
||||
if(previous_coords === null)
|
||||
path.moveTo(coords[0], coords[1]);
|
||||
else
|
||||
path.lineTo(coords[0], coords[1]);
|
||||
path_len += 1;
|
||||
previous_coords = coords;
|
||||
}
|
||||
}
|
||||
if(path_len > 1)
|
||||
{
|
||||
ctx.lineWidth = width;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.stroke(path);
|
||||
}
|
||||
};
|
||||
|
||||
const bridge_outline: string = 'rgb(80, 80, 80)';
|
||||
const thickest_stroke: number = Math.floor(Math.min(width, height) / 32);
|
||||
|
||||
this.ways.forEach(way => {
|
||||
if(way.tags['building'] !== undefined)
|
||||
{
|
||||
draw_way_polygon(way, 'rgb(190, 180, 160)', 'rgb(120, 120, 120)');
|
||||
}
|
||||
});
|
||||
|
||||
this.ways.forEach(way => {
|
||||
if(way.tags['highway'] !== undefined)
|
||||
{
|
||||
switch(way.tags['highway'])
|
||||
{
|
||||
case 'motorway':
|
||||
case 'motorway_link':
|
||||
draw_way_line(way, thickest_stroke, 'rgb(40, 20, 10)');
|
||||
draw_way_line(way, thickest_stroke * 0.9, 'rgb(220, 160, 160)');
|
||||
break;
|
||||
case 'trunk':
|
||||
case 'trunk_link':
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.9),
|
||||
(way.tags['bridge'] !== undefined) ? bridge_outline : 'rgb(180, 120, 50)');
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.8),'rgb(240, 170, 150)');
|
||||
break;
|
||||
case 'primary':
|
||||
case 'primary_link':
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.8),
|
||||
(way.tags['bridge'] !== undefined) ? bridge_outline : 'rgb(150, 80, 60)');
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.7),'rgb(245, 215, 150)');
|
||||
break;
|
||||
case 'secondary':
|
||||
case 'secondary_link':
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.6),
|
||||
(way.tags['bridge'] !== undefined) ? bridge_outline : 'rgb(50, 40, 20)');
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.5),'rgb(252, 250, 200)');
|
||||
break;
|
||||
case 'tertiary':
|
||||
case 'tertiary_link':
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.4),
|
||||
(way.tags['bridge'] !== undefined) ? bridge_outline : 'rgb(160, 160, 160)');
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.3),'rgb(245, 245, 245)');
|
||||
break;
|
||||
case 'residential':
|
||||
case 'unclassified':
|
||||
case 'road':
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.35),
|
||||
(way.tags['bridge'] !== undefined) ? bridge_outline : 'rgb(160, 160, 160)');
|
||||
draw_way_line(way, Math.floor(thickest_stroke * 0.25),'rgb(250, 250, 250)');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
5
packages/osm-map-nano/src/index.ts
Normal file
5
packages/osm-map-nano/src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Mapper } from './Mapper.js';
|
||||
|
||||
export {
|
||||
Mapper
|
||||
};
|
||||
14
packages/osm-map-nano/tests/test.ts
Normal file
14
packages/osm-map-nano/tests/test.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Mapper } from 'osm-map-nano';
|
||||
|
||||
(globalThis as any).test = (canvas: HTMLCanvasElement, data: string) => {
|
||||
const test = new Mapper({canvas: canvas});
|
||||
test.loadXMLData(data);
|
||||
|
||||
console.log(JSON.stringify(test.data_bounds));
|
||||
console.log(`Nodes: ${test.nodes.size}`);
|
||||
console.log(`Ways: ${test.ways.size}`);
|
||||
console.log(`Relations: ${test.relations.size}`);
|
||||
|
||||
test.draw(1600);
|
||||
console.log('Draw done');
|
||||
};
|
||||
3
packages/osm-map-nano/tsconfig.json
Normal file
3
packages/osm-map-nano/tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
1
packages/osm-map-nano/tsconfig.tsbuildinfo
Normal file
1
packages/osm-map-nano/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue