Add Mapbox static map generation for non-JavaScript users
- Implement MapImageService using Mapbox Static Images API - Add server-side /table route with HTML form submission - Generate static map images with auto-fit positioning based on actual location coordinates - Add progressive enhancement with noscript fallbacks and Basic View button - Update map center coordinates to proper Grand Rapids location - Add numbered pins with color coding (red for regular, orange for persistent reports) - Remove server-side caching to ensure fresh map images - Fix theme toggle icon centering in CSS mixins 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6cb165a3c3
commit
96dc6bde42
8 changed files with 1015 additions and 18 deletions
505
package-lock.json
generated
505
package-lock.json
generated
|
@ -10,11 +10,13 @@
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"canvas": "^3.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^7.5.1",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
|
"sharp": "^0.34.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
@ -727,6 +729,16 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
|
@ -1047,6 +1059,402 @@
|
||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-arm64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-darwin-x64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-arm64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-s390x": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linux-x64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-wasm32": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
|
||||||
|
"cpu": [
|
||||||
|
"wasm32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/runtime": "^1.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-arm64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-ia32": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@istanbuljs/load-nyc-config": {
|
"node_modules/@istanbuljs/load-nyc-config": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
|
@ -3532,6 +3940,20 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvas": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-addon-api": "^7.0.0",
|
||||||
|
"prebuild-install": "^7.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.12.0 || >= 20.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
@ -3682,11 +4104,23 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/color": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1",
|
||||||
|
"color-string": "^1.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
|
@ -3699,9 +4133,18 @@
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-support": {
|
"node_modules/color-support": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
|
@ -8220,6 +8663,47 @@
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sharp": {
|
||||||
|
"version": "0.34.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
|
||||||
|
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-libc": "^2.0.4",
|
||||||
|
"semver": "^7.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-darwin-arm64": "0.34.2",
|
||||||
|
"@img/sharp-darwin-x64": "0.34.2",
|
||||||
|
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||||
|
"@img/sharp-linux-arm": "0.34.2",
|
||||||
|
"@img/sharp-linux-arm64": "0.34.2",
|
||||||
|
"@img/sharp-linux-s390x": "0.34.2",
|
||||||
|
"@img/sharp-linux-x64": "0.34.2",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "0.34.2",
|
||||||
|
"@img/sharp-linuxmusl-x64": "0.34.2",
|
||||||
|
"@img/sharp-wasm32": "0.34.2",
|
||||||
|
"@img/sharp-win32-arm64": "0.34.2",
|
||||||
|
"@img/sharp-win32-ia32": "0.34.2",
|
||||||
|
"@img/sharp-win32-x64": "0.34.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
@ -8380,6 +8864,21 @@
|
||||||
"simple-concat": "^1.0.0"
|
"simple-concat": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||||
|
@ -9082,7 +9581,7 @@
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
|
|
|
@ -22,11 +22,13 @@
|
||||||
"postinstall": "npm run build-css"
|
"postinstall": "npm run build-css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"canvas": "^3.1.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^7.5.1",
|
"express-rate-limit": "^7.5.1",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
|
"sharp": "^0.34.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1"
|
"swagger-ui-express": "^5.0.1"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const map = L.map('map').setView([42.9634, -85.6681], 10);
|
const map = L.map('map').setView([42.960081464833195, -85.67402711517647], 10);
|
||||||
|
|
||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
|
|
|
@ -20,6 +20,20 @@
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<noscript>
|
||||||
|
<style>
|
||||||
|
.js-only { display: none !important; }
|
||||||
|
.nojs-fallback { display: block !important; }
|
||||||
|
.nojs-notice {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</noscript>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -29,23 +43,31 @@
|
||||||
<h1>❄️ Great Lakes Ice Report</h1>
|
<h1>❄️ Great Lakes Ice Report</h1>
|
||||||
<p>Community-reported ICEy road conditions and winter hazards (auto-expire after 48 hours)</p>
|
<p>Community-reported ICEy road conditions and winter hazards (auto-expire after 48 hours)</p>
|
||||||
</div>
|
</div>
|
||||||
<button id="theme-toggle" class="theme-toggle" title="Toggle dark mode">
|
<button id="theme-toggle" class="theme-toggle js-only" title="Toggle dark mode">
|
||||||
<span class="theme-icon">🌙</span>
|
<span class="theme-icon">🌙</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<!-- Non-JavaScript fallback notice -->
|
||||||
|
<noscript>
|
||||||
|
<div class="nojs-notice">
|
||||||
|
<p><strong>JavaScript is disabled.</strong> For the best experience with interactive maps, please enable JavaScript.</p>
|
||||||
|
<p><a href="/table" style="color: #007bff; text-decoration: underline;">Click here to view the table-only version →</a></p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2>Report ICEy Conditions</h2>
|
<h2>Report ICEy Conditions</h2>
|
||||||
<form id="location-form">
|
<form id="location-form" method="POST" action="/submit-report">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="address">Address or Location *</label>
|
<label for="address">Address or Location *</label>
|
||||||
<div class="autocomplete-container">
|
<div class="autocomplete-container">
|
||||||
<input type="text" id="address" name="address" required
|
<input type="text" id="address" name="address" required
|
||||||
placeholder="Enter address, intersection (e.g., Main St & Second St, City), or landmark"
|
placeholder="Enter address, intersection (e.g., Main St & Second St, City), or landmark"
|
||||||
autocomplete="off">
|
autocomplete="off">
|
||||||
<div id="autocomplete-list" class="autocomplete-list"></div>
|
<div id="autocomplete-list" class="autocomplete-list js-only"></div>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-help">Examples: "123 Main St, City" or "Main St & Oak Ave, City" or "CVS Pharmacy, City"</small>
|
<small class="input-help">Examples: "123 Main St, City" or "Main St & Oak Ave, City" or "CVS Pharmacy, City"</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,23 +81,38 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
|
||||||
|
|
||||||
<button type="submit" id="submit-btn">
|
<button type="submit" id="submit-btn">
|
||||||
<span id="submit-text">Report Location</span>
|
<span id="submit-text">Report Location</span>
|
||||||
<span id="submit-loading" style="display: none;">Submitting...</span>
|
<span id="submit-loading" style="display: none;" class="js-only">Submitting...</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="message" class="message"></div>
|
<div id="message" class="message js-only"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-section">
|
<div class="map-section">
|
||||||
<div class="reports-header">
|
<div class="reports-header">
|
||||||
<h2>Current Reports</h2>
|
<h2>Current Reports</h2>
|
||||||
<div class="view-toggle">
|
<div class="view-toggle js-only">
|
||||||
<button id="map-view-btn" class="toggle-btn active">📍 Map View</button>
|
<button id="map-view-btn" class="toggle-btn active">📍 Map View</button>
|
||||||
<button id="table-view-btn" class="toggle-btn">📋 Table View</button>
|
<button id="table-view-btn" class="toggle-btn">📋 Table View</button>
|
||||||
|
<a href="/table" class="toggle-btn" style="text-decoration: none; line-height: normal;" title="Server-side view that works without JavaScript">📊 Basic View</a>
|
||||||
</div>
|
</div>
|
||||||
|
<noscript>
|
||||||
|
<div class="nojs-fallback" style="display: block;">
|
||||||
|
<div style="text-align: center; margin: 24px 0;">
|
||||||
|
<h3>Static Map Overview</h3>
|
||||||
|
<img src="/map-image.png?width=600&height=400"
|
||||||
|
alt="Static map showing ice report locations"
|
||||||
|
style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
|
<p style="font-size: 14px; color: #666; margin-top: 8px;">
|
||||||
|
Red markers: Regular reports | Orange markers: Persistent reports
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p style="text-align: center;"><a href="/table" style="color: #007bff; text-decoration: underline;">📋 View Detailed Table Format →</a></p>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="map-view" class="view-container">
|
<div id="map-view" class="view-container js-only">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div class="map-info">
|
<div class="map-info">
|
||||||
<p><strong>🔴 Red markers:</strong> Icy conditions reported</p>
|
<p><strong>🔴 Red markers:</strong> Icy conditions reported</p>
|
||||||
|
@ -84,7 +121,7 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="table-view" class="view-container" style="display: none;">
|
<div id="table-view" class="view-container js-only" style="display: none;">
|
||||||
<div class="table-controls">
|
<div class="table-controls">
|
||||||
<div class="table-info">
|
<div class="table-info">
|
||||||
<p id="table-location-count">Loading locations...</p>
|
<p id="table-location-count">Loading locations...</p>
|
||||||
|
|
|
@ -187,3 +187,23 @@ button {
|
||||||
|
|
||||||
.w-100 { width: 100%; }
|
.w-100 { width: 100%; }
|
||||||
.h-100 { height: 100%; }
|
.h-100 { height: 100%; }
|
||||||
|
|
||||||
|
// Progressive enhancement styles
|
||||||
|
.js-only {
|
||||||
|
// Show by default (when JavaScript is available)
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nojs-fallback {
|
||||||
|
// Hide by default (only show when JavaScript is disabled via noscript)
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nojs-notice {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@
|
||||||
|
|
||||||
// Theme Toggle Mixin (consolidates duplicated theme toggle styles)
|
// Theme Toggle Mixin (consolidates duplicated theme toggle styles)
|
||||||
@mixin theme-toggle-styles($width: 40px, $height: 40px) {
|
@mixin theme-toggle-styles($width: 40px, $height: 40px) {
|
||||||
@include button($bg-color: transparent);
|
@include button($bg-color: var(--card-bg), $text-color: var(--text-color));
|
||||||
border: 2px solid var(--border-color);
|
border: 2px solid var(--border-color);
|
||||||
border-radius: $border-radius-full;
|
border-radius: $border-radius-full;
|
||||||
width: $width;
|
width: $width;
|
||||||
|
@ -132,9 +132,21 @@
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 2px 4px var(--shadow);
|
box-shadow: 0 2px 4px var(--shadow);
|
||||||
|
background-color: var(--card-bg) !important;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
padding: 0 !important; // Remove padding to ensure centering
|
||||||
|
|
||||||
|
// Ensure the icon is centered
|
||||||
|
.theme-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--table-hover);
|
background-color: var(--table-hover) !important;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
256
src/server.ts
256
src/server.ts
|
@ -13,6 +13,7 @@ dotenv.config();
|
||||||
// Import services and models
|
// Import services and models
|
||||||
import DatabaseService from './services/DatabaseService';
|
import DatabaseService from './services/DatabaseService';
|
||||||
import ProfanityFilterService from './services/ProfanityFilterService';
|
import ProfanityFilterService from './services/ProfanityFilterService';
|
||||||
|
import MapImageService from './services/MapImageService';
|
||||||
|
|
||||||
// Import route modules
|
// Import route modules
|
||||||
import configRoutes from './routes/config';
|
import configRoutes from './routes/config';
|
||||||
|
@ -26,11 +27,12 @@ const PORT: number = parseInt(process.env.PORT || '3000', 10);
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
|
||||||
// Database and services setup
|
// Database and services setup
|
||||||
const databaseService = new DatabaseService();
|
const databaseService = new DatabaseService();
|
||||||
let profanityFilter: ProfanityFilterService | FallbackFilter;
|
let profanityFilter: ProfanityFilterService | FallbackFilter;
|
||||||
|
const mapImageService = new MapImageService();
|
||||||
|
|
||||||
// Fallback filter interface for type safety
|
// Fallback filter interface for type safety
|
||||||
interface FallbackFilter {
|
interface FallbackFilter {
|
||||||
|
@ -181,17 +183,263 @@ function setupRoutes(): void {
|
||||||
// Static page routes
|
// Static page routes
|
||||||
app.get('/', (req: Request, res: Response): void => {
|
app.get('/', (req: Request, res: Response): void => {
|
||||||
console.log('Serving the main page');
|
console.log('Serving the main page');
|
||||||
res.sendFile(path.join(__dirname, '../../public', 'index.html'));
|
res.sendFile(path.join(__dirname, '../public', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Non-JavaScript table view route
|
||||||
|
app.get('/table', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
console.log('Serving table view for non-JS users');
|
||||||
|
try {
|
||||||
|
const locations = await locationModel.getActive();
|
||||||
|
|
||||||
|
const formatTimeRemaining = (createdAt: string): string => {
|
||||||
|
const created = new Date(createdAt);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = (48 * 60 * 60 * 1000) - (now.getTime() - created.getTime());
|
||||||
|
|
||||||
|
if (diffMs <= 0) return 'Expired';
|
||||||
|
|
||||||
|
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
} else {
|
||||||
|
return `${minutes}m`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string): string => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableRows = locations.map((location, index) => `
|
||||||
|
<tr>
|
||||||
|
<td style="text-align: center; font-weight: bold;">${index + 1}</td>
|
||||||
|
<td>${location.address}</td>
|
||||||
|
<td>${location.description || 'No additional details'}</td>
|
||||||
|
<td>${location.created_at ? formatDate(location.created_at) : 'Unknown'}</td>
|
||||||
|
<td>${location.persistent ? 'Persistent' : (location.created_at ? formatTimeRemaining(location.created_at) : 'Unknown')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Great Lakes Ice Report - Table View</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-text">
|
||||||
|
<h1>❄️ Great Lakes Ice Report</h1>
|
||||||
|
<p>Community-reported ICEy road conditions and winter hazards</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>Report ICEy Conditions</h2>
|
||||||
|
<form method="POST" action="/submit-report">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="address">Address or Location *</label>
|
||||||
|
<input type="text" id="address" name="address" required
|
||||||
|
placeholder="Enter address, intersection, or landmark">
|
||||||
|
<small class="input-help">Examples: "123 Main St, City" or "Main St & Oak Ave, City"</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Additional Details (Optional)</label>
|
||||||
|
<textarea id="description" name="description" rows="3"
|
||||||
|
placeholder="Number of vehicles, time observed, etc."></textarea>
|
||||||
|
<small class="input-help">Keep descriptions appropriate and relevant to road conditions.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit">Report Location</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="map-section">
|
||||||
|
<div class="reports-header">
|
||||||
|
<h2>Current Reports (${locations.length} active)</h2>
|
||||||
|
<p><a href="/">← Back to Interactive Map</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${locations.length > 0 ? `
|
||||||
|
<div style="text-align: center; margin: 24px 0;">
|
||||||
|
<h3>Static Map Overview</h3>
|
||||||
|
<img src="/map-image.png?width=800&height=400"
|
||||||
|
alt="Static map showing ice report locations"
|
||||||
|
style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||||
|
<p style="font-size: 14px; color: #666; margin-top: 8px;">
|
||||||
|
Red markers: Regular reports | Orange markers: Persistent reports
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="reports-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Details</th>
|
||||||
|
<th>Reported</th>
|
||||||
|
<th>Time Remaining</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${tableRows || '<tr><td colspan="5">No reports currently available</td></tr>'}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p><strong>Safety Notice:</strong> This is a community tool for awareness. Stay safe and verify information independently.</p>
|
||||||
|
<div class="disclaimer">
|
||||||
|
<small>Reports are automatically deleted after 48 hours. • <a href="/privacy">Privacy Policy</a></small>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error serving table view:', err);
|
||||||
|
res.status(500).send('Internal server error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle form submission for non-JS users
|
||||||
|
app.post('/submit-report', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
console.log('Handling form submission for non-JS users');
|
||||||
|
|
||||||
|
const { address, description } = req.body;
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (!address || typeof address !== 'string' || address.trim().length === 0) {
|
||||||
|
res.status(400).send(`
|
||||||
|
<html>
|
||||||
|
<head><title>Error</title><link rel="stylesheet" href="style.css"></head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Address is required.</p>
|
||||||
|
<p><a href="/table">← Go Back</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for profanity if description provided
|
||||||
|
if (description && profanityFilter) {
|
||||||
|
try {
|
||||||
|
const analysis = profanityFilter.analyzeProfanity(description);
|
||||||
|
if (analysis.hasProfanity) {
|
||||||
|
res.status(400).send(`
|
||||||
|
<html>
|
||||||
|
<head><title>Submission Rejected</title><link rel="stylesheet" href="style.css"></head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Submission Rejected</h1>
|
||||||
|
<p>Your description contains inappropriate language and cannot be posted.</p>
|
||||||
|
<p>Please revise your description to focus on road conditions and keep it professional.</p>
|
||||||
|
<p><a href="/table">← Go Back</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (filterError) {
|
||||||
|
console.error('Error checking profanity:', filterError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await locationModel.create({
|
||||||
|
address: address.trim(),
|
||||||
|
description: description?.trim() || null
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(`
|
||||||
|
<html>
|
||||||
|
<head><title>Report Submitted</title><link rel="stylesheet" href="style.css"></head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>✅ Report Submitted Successfully</h1>
|
||||||
|
<p>Your ice condition report has been added to the system.</p>
|
||||||
|
<p><a href="/table">← View All Reports</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error creating location:', err);
|
||||||
|
res.status(500).send(`
|
||||||
|
<html>
|
||||||
|
<head><title>Error</title><link rel="stylesheet" href="style.css"></head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Failed to submit report. Please try again.</p>
|
||||||
|
<p><a href="/table">← Go Back</a></p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Static map image generation route
|
||||||
|
app.get('/map-image.png', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
console.log('Generating static map image');
|
||||||
|
try {
|
||||||
|
const locations = await locationModel.getActive();
|
||||||
|
|
||||||
|
// Parse query parameters for customization
|
||||||
|
const width = parseInt(req.query.width as string) || 800;
|
||||||
|
const height = parseInt(req.query.height as string) || 600;
|
||||||
|
const padding = parseInt(req.query.padding as string) || 50;
|
||||||
|
|
||||||
|
const imageBuffer = await mapImageService.generateMapImage(locations, {
|
||||||
|
width: Math.min(Math.max(width, 400), 1200), // Clamp between 400-1200
|
||||||
|
height: Math.min(Math.max(height, 300), 900), // Clamp between 300-900
|
||||||
|
padding: Math.min(Math.max(padding, 20), 100) // Clamp between 20-100
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'image/png');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.send(imageBuffer);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating map image:', err);
|
||||||
|
res.status(500).send('Error generating map image');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/admin', (req: Request, res: Response): void => {
|
app.get('/admin', (req: Request, res: Response): void => {
|
||||||
console.log('Serving the admin page');
|
console.log('Serving the admin page');
|
||||||
res.sendFile(path.join(__dirname, '../../public', 'admin.html'));
|
res.sendFile(path.join(__dirname, '../public', 'admin.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/privacy', (req: Request, res: Response): void => {
|
app.get('/privacy', (req: Request, res: Response): void => {
|
||||||
console.log('Serving the privacy policy page');
|
console.log('Serving the privacy policy page');
|
||||||
res.sendFile(path.join(__dirname, '../../public', 'privacy.html'));
|
res.sendFile(path.join(__dirname, '../public', 'privacy.html'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
179
src/services/MapImageService.ts
Normal file
179
src/services/MapImageService.ts
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import https from 'https';
|
||||||
|
import { Location } from '../types';
|
||||||
|
|
||||||
|
interface MapOptions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
padding?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapImageService {
|
||||||
|
private defaultOptions: MapOptions = {
|
||||||
|
width: 800,
|
||||||
|
height: 600
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a static map image using Mapbox Static Maps API
|
||||||
|
*/
|
||||||
|
async generateMapImage(locations: Location[], options: Partial<MapOptions> = {}): Promise<Buffer> {
|
||||||
|
const opts = { ...this.defaultOptions, ...options };
|
||||||
|
|
||||||
|
console.log('Generating Mapbox static map focused on location data');
|
||||||
|
console.log('Canvas size:', opts.width, 'x', opts.height);
|
||||||
|
console.log('Number of locations:', locations.length);
|
||||||
|
|
||||||
|
const mapboxBuffer = await this.fetchMapboxStaticMapAutoFit(opts, locations);
|
||||||
|
|
||||||
|
if (mapboxBuffer) {
|
||||||
|
return mapboxBuffer;
|
||||||
|
} else {
|
||||||
|
// Return a simple error image if Mapbox fails
|
||||||
|
return this.generateErrorImage(opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch map using Mapbox Static Maps API with auto-fit to location data
|
||||||
|
*/
|
||||||
|
private async fetchMapboxStaticMapAutoFit(options: MapOptions, locations: Location[]): Promise<Buffer | null> {
|
||||||
|
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
||||||
|
if (!mapboxToken) {
|
||||||
|
console.error('No Mapbox token available');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build overlay string for all locations with correct format
|
||||||
|
let overlays = '';
|
||||||
|
locations.forEach((location, index) => {
|
||||||
|
if (location.latitude && location.longitude) {
|
||||||
|
console.log(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`);
|
||||||
|
// Correct format: pin-s-label+color(lng,lat)
|
||||||
|
const color = location.persistent ? 'ff9800' : 'ff0000'; // Orange for persistent, red for regular
|
||||||
|
const label = (index + 1).toString();
|
||||||
|
overlays += `pin-s-${label}+${color}(${location.longitude},${location.latitude}),`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove trailing comma
|
||||||
|
overlays = overlays.replace(/,$/, '');
|
||||||
|
|
||||||
|
console.log('Generated overlays string:', overlays);
|
||||||
|
|
||||||
|
// Build Mapbox Static Maps URL with auto-fit
|
||||||
|
let mapboxUrl;
|
||||||
|
if (overlays) {
|
||||||
|
// Use auto-fit to center on all pins
|
||||||
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${overlays}/auto/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
||||||
|
} else {
|
||||||
|
// No locations, use Grand Rapids as fallback
|
||||||
|
const fallbackLat = 42.960081464833195;
|
||||||
|
const fallbackLng = -85.67402711517647;
|
||||||
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${fallbackLng},${fallbackLat},10/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching Mapbox static map with auto-fit...');
|
||||||
|
console.log('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN'));
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
console.log('Mapbox static map fetched successfully');
|
||||||
|
resolve(Buffer.concat(chunks));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Mapbox API error:', response.statusCode);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', (err) => {
|
||||||
|
console.error('Error fetching Mapbox map:', err.message);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('timeout', () => {
|
||||||
|
console.error('Mapbox request timeout');
|
||||||
|
request.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy method - keeping for potential fallback
|
||||||
|
*/
|
||||||
|
private async fetchMapboxStaticMap(centerLat: number, centerLng: number, zoom: number, options: MapOptions, locations: Location[]): Promise<Buffer | null> {
|
||||||
|
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
||||||
|
if (!mapboxToken) {
|
||||||
|
console.error('No Mapbox token available');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build overlay string for location markers
|
||||||
|
let overlays = '';
|
||||||
|
locations.forEach((location, index) => {
|
||||||
|
if (location.latitude && location.longitude) {
|
||||||
|
console.log(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`);
|
||||||
|
const color = location.persistent ? 'orange' : 'red';
|
||||||
|
const label = (index + 1).toString();
|
||||||
|
overlays += `pin-s-${label}+${color}(${location.longitude},${location.latitude}),`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove trailing comma
|
||||||
|
overlays = overlays.replace(/,$/, '');
|
||||||
|
|
||||||
|
// Build Mapbox Static Maps URL
|
||||||
|
let mapboxUrl;
|
||||||
|
if (overlays) {
|
||||||
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${overlays}/${centerLng},${centerLat},${zoom}/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
||||||
|
} else {
|
||||||
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${centerLng},${centerLat},${zoom}/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching Mapbox static map...');
|
||||||
|
console.log('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN'));
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
response.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
response.on('end', () => {
|
||||||
|
console.log('Mapbox static map fetched successfully');
|
||||||
|
resolve(Buffer.concat(chunks));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Mapbox API error:', response.statusCode);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', (err) => {
|
||||||
|
console.error('Error fetching Mapbox map:', err.message);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('timeout', () => {
|
||||||
|
console.error('Mapbox request timeout');
|
||||||
|
request.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a simple error image when Mapbox fails
|
||||||
|
*/
|
||||||
|
private generateErrorImage(options: MapOptions): Buffer {
|
||||||
|
// Return a simple text-based error - in a real implementation you'd create a proper error image
|
||||||
|
const errorText = `Map generation failed - Mapbox API error\nSize: ${options.width}x${options.height}`;
|
||||||
|
return Buffer.from(errorText, 'utf8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MapImageService;
|
Loading…
Add table
Add a link
Reference in a new issue