Add osm-map-nano
This commit is contained in:
commit
bedef43e13
22 changed files with 739 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
data/* filter=lfs diff=lfs merge=lfs -text
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
build
|
||||
node_modules
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
omit[] = "optional"
|
||||
package-lock = false
|
||||
BIN
data/shibuya.osm
(Stored with Git LFS)
Normal file
BIN
data/shibuya.osm
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
data/test.png
(Stored with Git LFS)
Normal file
BIN
data/test.png
(Stored with Git LFS)
Normal file
Binary file not shown.
24
eslint.config.js
Normal file
24
eslint.config.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from 'eslint/config';
|
||||
import tsparser from '@typescript-eslint/parser';
|
||||
import stylistic from '@stylistic/eslint-plugin';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
sourceType: 'module',
|
||||
},
|
||||
ignores: ['**/dist/*', '**/*.min.js'],
|
||||
plugins: {
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
rules: {
|
||||
'@stylistic/array-bracket-spacing' : ['error', 'never'],
|
||||
'@stylistic/arrow-spacing' : ['error', {'before': true, 'after': true}],
|
||||
'@stylistic/block-spacing' : ['error', 'always'],
|
||||
'@stylistic/quotes' : ['error', 'single'],
|
||||
'@stylistic/semi' : ['error', 'always']
|
||||
}
|
||||
}
|
||||
]);
|
||||
34
kousen_home.osm
Normal file
34
kousen_home.osm
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<osm version="0.6" generator="openstreetmap-cgimap 2.1.0 (682953 spike-08.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
|
||||
<bounds minlat="33.9336800" minlon="134.2833100" maxlat="33.9986000" maxlon="134.4180700"/>
|
||||
<node id="268978937" visible="true" version="2" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9763341" lon="134.3615177"/>
|
||||
<way id="1299239449" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580">
|
||||
<nd ref="12035615873"/>
|
||||
<nd ref="12035615874"/>
|
||||
<nd ref="12035615875"/>
|
||||
<nd ref="12035615876"/>
|
||||
<nd ref="12035615877"/>
|
||||
<nd ref="12035615878"/>
|
||||
<nd ref="12035615879"/>
|
||||
<nd ref="12035615880"/>
|
||||
<nd ref="12035615881"/>
|
||||
<nd ref="12035615882"/>
|
||||
<nd ref="12035615883"/>
|
||||
<nd ref="12035615884"/>
|
||||
<nd ref="12035615873"/>
|
||||
<tag k="building" v="school"/>
|
||||
<tag k="name" v="神山まるごと高等専門学校"/>
|
||||
</way>
|
||||
<node id="12035615873" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728406" lon="134.3623724"/>
|
||||
<node id="12035615874" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9727562" lon="134.3623792"/>
|
||||
<node id="12035615875" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728069" lon="134.3632910"/>
|
||||
<node id="12035615876" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9729026" lon="134.3632832"/>
|
||||
<node id="12035615877" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728926" lon="134.3631019"/>
|
||||
<node id="12035615878" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9729577" lon="134.3630967"/>
|
||||
<node id="12035615879" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9729507" lon="134.3629704"/>
|
||||
<node id="12035615880" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728868" lon="134.3629755"/>
|
||||
<node id="12035615881" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728680" lon="134.3626345"/>
|
||||
<node id="12035615882" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9729233" lon="134.3626301"/>
|
||||
<node id="12035615883" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9729167" lon="134.3625115"/>
|
||||
<node id="12035615884" visible="true" version="1" changeset="153581872" timestamp="2024-07-05T10:24:33Z" user="awa showtaro" uid="6809580" lat="33.9728486" lon="134.3625170"/>
|
||||
</osm>
|
||||
28
package.json
Normal file
28
package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "js-osm",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"author": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebuild": "./scripts/tmpfs_dirs.sh",
|
||||
"build": "tsc --build && node scripts/build.js",
|
||||
"clean": "rm -rf build/*",
|
||||
"test": "node scripts/test.cjs"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.50.1",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
||||
"eslint": "^9.34.0",
|
||||
"osm-map-nao": "./packages/osm-map-nano",
|
||||
"rollup": "^4.50.1",
|
||||
"selenium-webdriver": "^4.35.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0"
|
||||
}
|
||||
}
|
||||
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
78
scripts/build.js
Normal file
78
scripts/build.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { rollup } from 'rollup';
|
||||
|
||||
const root_dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const root_info = JSON.parse(fs.readFileSync(path.join(root_dir, 'package.json')));
|
||||
|
||||
async function build(options)
|
||||
{
|
||||
let bundle;
|
||||
let success = true;
|
||||
try
|
||||
{
|
||||
bundle = await rollup(options.input);
|
||||
await bundle.write(options.output);
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
success = false;
|
||||
console.error(`Error building ${options}`);
|
||||
console.error(error);
|
||||
}
|
||||
if(bundle)
|
||||
{
|
||||
await bundle.close();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
for(const package_path of fs.globSync(root_info.workspaces))
|
||||
{
|
||||
const pkg_info = JSON.parse(fs.readFileSync(path.join(root_dir, package_path, 'package.json')));
|
||||
const build_dir = path.join('build', 'packages', pkg_info.name);
|
||||
const dist_dir = path.join(build_dir, 'dist');
|
||||
|
||||
// Building main package
|
||||
fs.mkdirSync(dist_dir, {recursive: true});
|
||||
|
||||
if(!await build({
|
||||
input: {
|
||||
input: path.join(build_dir, 'src', 'index.js')
|
||||
},
|
||||
output: {
|
||||
compact: true,
|
||||
extend: true,
|
||||
file: `${dist_dir}/${pkg_info.name}.js`,
|
||||
name: 'OSM',
|
||||
format: 'iife'
|
||||
}}))
|
||||
process.exit(1);
|
||||
|
||||
// Building tests
|
||||
const html_test_template = fs.readFileSync(path.join(root_dir, 'scripts', 'test_template.html')).toString()
|
||||
.replaceAll('{{bundle}}', `${pkg_info.name}.js`)
|
||||
.replaceAll('{{data}}', '/data/shibuya.osm');
|
||||
for(const test_file of fs.globSync(path.join(build_dir, 'tests', '*.js')))
|
||||
{
|
||||
const test_name = path.parse(test_file).name;
|
||||
if(!await build({
|
||||
input: {
|
||||
input: test_file,
|
||||
external: ['osm-map-nano'],
|
||||
},
|
||||
output: {
|
||||
compact: true,
|
||||
globals: { 'osm-map-nano': 'OSM' },
|
||||
extend: true,
|
||||
file: `${dist_dir}/${test_name}.js`,
|
||||
name: 'OSM',
|
||||
format: 'iife'
|
||||
}}))
|
||||
process.exit(1);
|
||||
fs.writeFileSync(`${dist_dir}/${test_name}.html`,
|
||||
html_test_template.replaceAll('{{test_script}}', `${test_name}.js`).replaceAll('{{test_name}}', test_name));
|
||||
}
|
||||
}
|
||||
66
scripts/server.js
Normal file
66
scripts/server.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
|
||||
const port = 8080;
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
const mimeType = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'text/javascript',
|
||||
'.gz': 'application/javascript',
|
||||
};
|
||||
const mimeEncoding = {
|
||||
'.gz': 'gzip',
|
||||
};
|
||||
let pathname = path.join('.', request.url);
|
||||
|
||||
if(!fs.existsSync(pathname))
|
||||
{
|
||||
// If the file is not found, return 404
|
||||
response.statusCode = 404;
|
||||
response.end(`File ${pathname} not found!`);
|
||||
return;
|
||||
}
|
||||
// If is a directory, then look for index.html
|
||||
if (fs.statSync(pathname).isDirectory()) {
|
||||
pathname = path.join(pathname, 'index.html');
|
||||
}
|
||||
// Read file from file system
|
||||
fs.readFile(pathname, function(error, data){
|
||||
if(error)
|
||||
{
|
||||
response.statusCode = 500;
|
||||
response.end(`Error getting the file: ${error}.`);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Based on the URL path, extract the file extention. e.g. .js, .doc, ...
|
||||
const extension = path.parse(pathname).ext;
|
||||
// Set Content-Type
|
||||
response.setHeader('Content-Type', mimeType[extension] || 'text/plain' );
|
||||
// If the file is found, set Content-Encoding
|
||||
if(mimeEncoding[extension])
|
||||
{
|
||||
response.setHeader('Content-Encoding', mimeEncoding[extension]);
|
||||
}
|
||||
response.end(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, (error) => {
|
||||
if (error)
|
||||
return console.log(`Server cannot listen port ${port} :`, error);
|
||||
console.log(`Server is listening on port ${port}`);
|
||||
});
|
||||
|
||||
const exit = () => {
|
||||
server.close();
|
||||
console.log('Server stopped');
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
process.stdout.write('\r \r');
|
||||
exit();
|
||||
});
|
||||
67
scripts/test.cjs
Normal file
67
scripts/test.cjs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const assert = require('node:assert');
|
||||
const { spawn } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const { Browser, Builder, By } = require('selenium-webdriver');
|
||||
const firefox = require('selenium-webdriver/firefox');
|
||||
|
||||
async function test() {
|
||||
const generate_env_var = process.env.OSM_MAP_GENERATE_DATA || 'false';
|
||||
const generete_data = generate_env_var === '1' || generate_env_var.toLowerCase() === 'true';
|
||||
|
||||
const server = spawn('node', ['scripts/server.js'], {stdio: [null, null, 'inherit']}); // show stderr (stdout is read)
|
||||
console.log('Creating selenium driver');
|
||||
const options = new firefox.Options();
|
||||
options.addArguments('--headless');
|
||||
const driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(options).build();
|
||||
try
|
||||
{
|
||||
const max_try = 5;
|
||||
let read_try = 0;
|
||||
for(; read_try < max_try; read_try +=1)
|
||||
{
|
||||
const server_out = server.stdout.read();
|
||||
if(server_out != null)
|
||||
{
|
||||
assert.equal('Server is listening on port 8080\n', server_out.toString(), 'Server not starting properly');
|
||||
break;
|
||||
}
|
||||
await driver.sleep(50);
|
||||
}
|
||||
assert.notEqual(read_try, max_try, 'Timeout waiting for server starting');
|
||||
|
||||
const test_name = 'test';
|
||||
console.log('Fetching test html');
|
||||
await driver.get('http://localhost:8080/build/packages/osm-map-nano/dist/test.html');
|
||||
assert.equal(`OSM Map Test : ${test_name}`, await driver.getTitle(), 'Wrong HTML title');
|
||||
|
||||
let canvas = await driver.findElement(By.id('canvas-test'));
|
||||
const image_url_prefix = 'data:image/png;base64,';
|
||||
const image_data = Buffer.from(
|
||||
(await driver.executeScript('return arguments[0].toDataURL()', canvas)).substring(image_url_prefix.length), 'base64');
|
||||
const expected_data = fs.readFileSync('data/test.png');
|
||||
if(generete_data)
|
||||
{
|
||||
if(!expected_data.equals(image_data))
|
||||
{
|
||||
console.log(`Generating image for test ${test_name}`);
|
||||
fs.writeFileSync('data/test.png', image_data);
|
||||
}
|
||||
}
|
||||
else
|
||||
assert.ok(expected_data.equals(image_data));
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error('TEST FAILED');
|
||||
console.error(error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
console.log('Exiting selenium driver');
|
||||
await driver.quit();
|
||||
server.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
|
||||
test();
|
||||
20
scripts/test_template.html
Normal file
20
scripts/test_template.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>OSM Map Test : {{test_name}}</title>
|
||||
<style>
|
||||
body { background: #333; color: #ddd}
|
||||
</style>
|
||||
<script src="{{bundle}}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas-test"></canvas>
|
||||
<script src="{{test_script}}"></script>
|
||||
<script>
|
||||
fetch('{{data}}')
|
||||
.then(response => response.text())
|
||||
.then(data => { test(document.getElementById('canvas-test'), data); } );
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
12
scripts/tmpfs_dirs.sh
Executable file
12
scripts/tmpfs_dirs.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#! /bin/bash
|
||||
|
||||
CWD=$(cd -P . && pwd)
|
||||
readarray -t TMPFS_DIRS < <(df -lt tmpfs --output=target | tail -n +2)
|
||||
|
||||
|
||||
if [[ " ${TMPFS_DIRS[*]} " =~ " ${CWD}/build " ]]; then
|
||||
echo "build alread a tmpfs"
|
||||
else
|
||||
echo "Mounting build as tmpfs"
|
||||
sudo mount -m -t tmpfs tmpfs build
|
||||
fi
|
||||
37
tsconfig.json
Normal file
37
tsconfig.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"removeComments": false,
|
||||
"rootDir": ".",
|
||||
|
||||
"lib": ["DOM", "ES2023"],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
"alwaysStrict": true,
|
||||
"allowJs": false,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictBuiltinIteratorReturn": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"references": [
|
||||
{"path": "packages/osm-map-nano"}
|
||||
]
|
||||
}
|
||||
1
tsconfig.tsbuildinfo
Normal file
1
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