├── .bowerrc
├── .dockerignore
├── .gitignore
├── Dockerfile
├── Dockerfile.postgres
├── LICENSE
├── README.md
├── app.json
├── app
├── index.pug
├── routes.js
├── s3.js
└── setup-check.js
├── bower.json
├── build
└── migrate.sql
├── config-example.json
├── config.json
├── docker-compose.yml
├── package.json
├── public
├── directives
│ ├── button-group.js
│ ├── geojson-input.js
│ ├── google-places.js
│ ├── live-link.js
│ ├── map-rough-preview.js
│ ├── preview-link.js
│ ├── published-check.js
│ └── unique-slug.js
├── partials
│ ├── aspect-ratio-selector.html
│ ├── geojson-input.html
│ ├── map-detail.html
│ ├── map-list.html
│ └── publish-modal.html
├── script.js
└── style.css
└── server.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "public/bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | .sass-cache/
4 | bower_components/
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:8-alpine
2 |
3 | RUN apk add --no-cache tini
4 |
5 | EXPOSE 3001
6 |
7 | WORKDIR /usr/src/app
8 |
9 | # Copy just the package*.json files to perform the install
10 | COPY package* ./
11 | RUN npm install
12 |
13 | # Then copy the remaining ones. Splitting the install from the main copy lets us cache the results of the install
14 | # between code changes
15 | COPY . .
16 |
17 | # Node wasn't designed to be run as PID 1. Tini is a tiny init wrapper.
18 | ENTRYPOINT ["/sbin/tini", "--"]
19 |
20 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/Dockerfile.postgres:
--------------------------------------------------------------------------------
1 | FROM postgres
2 | COPY ./build/migrate.sql /docker-entrypoint-initdb.d/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dow Jones
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Note: As of 2021, this tool has been deprecated and the repository is no longer being maintained.
2 |
3 | # Pinpoint Editor
4 |
5 | Pinpoint Editor is a web app for quickly creating and editing [Pinpoint maps](https://github.com/dowjones/pinpoint).
6 |
7 | **Features:**
8 |
9 | - Simple user interface allows maps to be created in seconds.
10 | - Flexible Angular app with Node backend.
11 | - Built-in support for uploading JSON data files to Amazon S3.
12 |
13 | ## How to set up Pinpoint Editor
14 |
15 | Pinpoint Editor requires:
16 |
17 | - A node.js server
18 | - A PostgresSQL database
19 | - NPM and Bower for installing dependencies
20 | - (optional) Amazon S3 to host data
21 |
22 | Here's how to install it locally:
23 |
24 | *Note: If you have trouble setting up Pinpoint Editor, please [open a ticket](https://github.com/dowjones/pinpoint-editor/issues/new) on GitHub.*
25 |
26 | 1. **Install required software**
27 |
28 | If on OS X, you can install all software using these commands:
29 |
30 | # Install Brew
31 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
32 |
33 | # Install NodeJS.
34 | brew install node
35 |
36 | # Install PostgreSQL.
37 | brew install postgresql
38 |
39 | # Install Bower.
40 | npm install bower
41 |
42 | 2. **Set up database**
43 |
44 | Create a PostgresSQL database. You can name it anything you like.
45 |
46 | createdb pinpointDb
47 |
48 | Set `DATABASE_URL` environment variable.
49 |
50 | export DATABASE_URL='postgresql://localhost/pinpointDb'
51 |
52 | Run migration script to set up table and load examples.
53 |
54 | psql $DATABASE_URL < build/migrate.sql
55 |
56 | You may need to start the database server manually:
57 |
58 | pg_ctl -D /usr/local/var/postgres -l /usr/local/var/postgres/server.log start
59 |
60 | 3. **Install dependencies**
61 |
62 | # Install server-side dependencies
63 | npm install
64 |
65 | # Install client-side dependencies
66 | bower install
67 |
68 | 4. **Configure settings**
69 |
70 | Generate a new Google Maps API key by [following these instructions](https://developers.google.com/maps/documentation/javascript/tutorial) and add it to `config.json` (under the `googleMapsAPIKey` property).
71 |
72 | *Optional:* To enable AWS S3 export, set these environment variables:
73 |
74 | export AWS_S3_KEY='XXXXXXXXXXXXXX'
75 | export AWS_S3_SECRET='XXXXXXXXXXXXXX'
76 | export AWS_BUCKET='XXXXXXXXXXXXXX'
77 |
78 | 5. **Run the server!**
79 |
80 | node server.js
81 |
82 | You will then be able to access Pinpoint at [http://localhost:3001](http://localhost:3001/).
83 |
84 | ## Docker setup
85 |
86 | If you have Docker installed you can run the Pinpoint editor by simply typing `docker-compose up`. You can
87 | then access Pinpoint at [http://localhost:3001](http://localhost:3001/).
88 |
89 | ## Architecture
90 |
91 | On the server, Pinpoint uses the minimal Express framework for routing. Data is stored as JSON using [PostgresSQL's native JSON data type](http://schinckel.net/2014/05/25/querying-json-in-postgres/), which can then be accessed via a simple API (see below for details). Data can then be exported to S3-hosted static JSON for production use.
92 |
93 | On the client, Pinpoint is an Angular app made up of multiple custom directives. Key files are `script.js` and `directives/map-detail.html`. Dependencies are managed using Bower.
94 |
95 | ### API routes
96 |
97 | * Get all maps `GET - /api/maps`
98 | * Get map by id `GET - /api/maps/:id`
99 | * Create map `POST - /api/maps/`
100 | * http request header must be `Content-Type: application/json`
101 | * http request body contains the entire data object for that record
102 | * returns `HTTP/1.1 201 Created - {"id": id, "message": "Map created"}`
103 | * Update map `PUT - /api/maps/:id`
104 | * http request header must be `Content-Type: application/json`
105 | * http request body contains the entire data object for that record
106 | * returns `HTTP/1.1 200 OK - {"message": "Map updated"}`
107 |
108 | ## Configuration file
109 |
110 | Various settings are controlled via `config.json`. See `config-example.json` for dummy values.
111 |
112 | **Editor interface options**
113 |
114 | * googleMapsAPIKey: _(required)_ Google maps API key ([get one here](https://developers.google.com/maps/documentation/javascript/tutorial))
115 | * title: Page title, e.g. _The Example Journal Map Tool_
116 | * greeting: Message to go beneath page title. HTML is allowed.
117 | * helpLink: URL of an external help page
118 | * previewLink: URL which, with the current map's slug on the end, links to a preview
119 | * liveLink: URL which, with the current map's slug on the end, links to the live production page for the current map
120 | * s3url: URL which, with the current map's slug (and ".json") on the end, links to the S3-hosted static JSON
121 | * geojsonStyles: Array
122 | * Object
123 | * class: css class for style (string) - eg. "dashed-clockwise"
124 | * name: descriptive name for style (string) - eg. "Dashed, animated clockwise"
125 |
126 | **Map setting options**
127 |
128 | These are used in all Pinpoint instances in the editor.
129 |
130 | * basemap: Leaflet tilelayer URL (string) - eg. "http://{s}.somedomain.com/blabla/{z}/{x}/{y}.png"
131 | * basemapCredit: Credit line for tilelayer - eg. "Leaflet | © Mapbox | © OpenSteetMap contributors"
132 |
133 |
134 | ## Version history
135 |
136 | **v1.2.1** (27 March, 2017)
137 |
138 | - Bugfixes for editor interface
139 |
140 | **v1.2.0** (17 February, 2017)
141 |
142 | - New feature: basemap selection
143 | - Google maps API key controlled via config.json
144 | - Easier customisation of interface text via config.json
145 | - Add pagination to homepage
146 |
147 | **v1.1.0** (17 July, 2015)
148 |
149 | - Update bower.json to allow any 1.1.* versions of Pinpoint library
150 | - Add .bowerrc to fix bower_components location
151 | - Add helpful error message if server port is in use
152 |
153 | **v1.0.1**
154 |
155 | - Update bower.json to allow any 1.0.* versions of Pinpoint library
156 |
157 | **v1.0.0**
158 |
159 | - Initial release
160 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Pinpoint Editor",
3 | "description": "Locator map tool",
4 | "success_url": "/",
5 | "scripts": {
6 | "start": "node server.js",
7 | "postdeploy": "psql $DATABASE_URL < build/migrate.sql"
8 | },
9 | "env": {
10 | "AWS_BUCKET": {
11 | "value": "",
12 | "required": false,
13 | "description": "Name of AWS S3 bucket to upload date files to"
14 | },
15 | "AWS_S3_KEY": {
16 | "value": "",
17 | "required": false,
18 | "description": "AWS key"
19 | },
20 | "AWS_S3_SECRET": {
21 | "value": "",
22 | "required": false,
23 | "description": "AWS secret"
24 | }
25 | },
26 | "addons": [
27 | "heroku-postgresql:hobby-dev"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/app/index.pug:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #{title}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Pinpoint Editor v#{version}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/routes.js:
--------------------------------------------------------------------------------
1 | var moment = require('moment');
2 | var _ = require('lodash');
3 | var fs = require('fs');
4 | var config = require('../config');
5 | config.version = require('../package.json').version;
6 | var pug = require('pug');
7 | var s3 = require('./s3');
8 |
9 | var pg = require('knex')({
10 | client: 'pg',
11 | connection: process.env.DATABASE_URL
12 | });
13 |
14 | module.exports = function(app) {
15 |
16 | var indexPage = pug.compileFile('./app/index.pug')(config);
17 |
18 | app.get('/', function(req, res, next) {
19 | res.send(indexPage);
20 | next();
21 | });
22 |
23 | app.get('/config.json', function(req, res) {
24 | config.s3enabled = s3.enabled();
25 | config.basemaps = config.basemaps || [];
26 | res.json( config );
27 | });
28 |
29 | app.get('/_health', function (req, res) {
30 | res.send("I'm alive.");
31 | });
32 |
33 | // API
34 | app.get('/api/maps', function(req, res) {
35 | pg('locator_map')
36 | .then(function(rows) {
37 | var rows = parse_data(rows);
38 | res.json(rows);
39 | })
40 | .catch(function(error) {
41 | res.status(404).json({error: error});
42 | });
43 | });
44 |
45 | app.get('/api/maps/slug/:slug', function(req, res) {
46 | var slug = req.params.slug;
47 |
48 | pg('locator_map')
49 | .whereRaw("data->>'slug' = ?", slug)
50 | .then(function(rows) {
51 | var rows = parse_data(rows);
52 | res.json(rows[0]);
53 | })
54 | .catch(function(error) {
55 | res.status(404).json({error: error});
56 | });
57 | });
58 |
59 | app.get('/api/maps/:id', function(req, res) {
60 | var id = req.params.id;
61 |
62 | pg('locator_map')
63 | .where('id', id)
64 | .then(function(rows) {
65 | var rows = parse_data(rows);
66 | res.json(rows[0]);
67 | })
68 | .catch(function(error) {
69 | res.status(404).json({error: error});
70 | });
71 | });
72 |
73 | app.post('/api/maps', function(req, res) {
74 | var data = req.body;
75 | data.creation_date = Date.now();
76 | data.modification_date = Date.now();
77 |
78 | pg.insert({data: data})
79 | .returning('id')
80 | .into('locator_map')
81 | .then(function(id) {
82 | res.status(201).json({
83 | id: id[0],
84 | message: 'Map created'
85 | });
86 | })
87 | .catch(function(error) {
88 | res.status(404).json({error: error});
89 | });
90 | });
91 |
92 | app.put('/api/maps/:id', function(req, res) {
93 | var id = req.params.id;
94 | var data = req.body;
95 | delete data.id;
96 | data.modification_date = Date.now();
97 |
98 | pg('locator_map')
99 | .where('id', '=', id)
100 | .update({
101 | data: data
102 | })
103 | .then(function() {
104 | res.json({
105 | message: 'Map updated',
106 | });
107 | })
108 | .catch(function(error) {
109 | res.status(404).json({error: error});
110 | });
111 | });
112 |
113 | app.delete('/api/maps/:id', function(req, res){
114 | var id = req.params.id;
115 | var location = 'projects';
116 |
117 | pg('locator_map')
118 | .where('id', id)
119 | .then(function(rows) {
120 | var slug = rows[0].data.slug;
121 | s3.remove(slug);
122 | return pg('locator_map').where('id', id).del();
123 | })
124 | .then(function(response) {
125 | res.json({
126 | message: 'Map deleted'
127 | });
128 | })
129 | .catch(function(error) {
130 | res.status(404).json({error: error});
131 | });
132 | });
133 |
134 | app.get('/api/slugs', function(req, res) {
135 | pg('locator_map')
136 | .then(function(rows) {
137 | var rows = parse_data(rows);
138 | var slugs = _.pluck(rows, 'slug');
139 | res.json(slugs);
140 | })
141 | .catch(function(error) {
142 | res.json({error: error});
143 | });
144 | });
145 |
146 | app.post('/api/publish', function(req, res) {
147 | var id = req.body.id;
148 | var location = 'projects';
149 | process_publish(id, location, res);
150 | });
151 |
152 | }
153 |
154 | function parse_data(data){
155 | var rows = _.map(data, function(row){
156 | var map = {
157 | id: row.id
158 | };
159 | return _.assign(map, row.data);
160 | });
161 | return rows;
162 | }
163 |
164 | function process_publish(id, location, res){
165 | pg('locator_map')
166 | .where('id', id)
167 | .then(function(rows) {
168 | var rows = parse_data(rows);
169 | var slug = rows[0].slug;
170 | var file = location + '/locator-maps/' + slug + '.json';
171 |
172 | s3.upload(slug, rows[0])
173 | .then(() => {
174 | res.json({
175 | message : "Map published",
176 | url: 'http://'+process.env.AWS_BUCKET+'.s3.amazonaws.com/' + file
177 | });
178 | })
179 | .catch(() => {
180 | res.status(404).json({error: "Error saving map to S3"});
181 | });
182 | })
183 | .catch(function(error) {
184 | res.json({error: error});
185 | });
186 | }
--------------------------------------------------------------------------------
/app/s3.js:
--------------------------------------------------------------------------------
1 | var knox = require('knox');
2 |
3 | if (process.env.AWS_S3_KEY) {
4 | var s3 = knox.createClient({
5 | key: process.env.AWS_S3_KEY,
6 | secret: process.env.AWS_S3_SECRET,
7 | bucket: process.env.AWS_BUCKET
8 | });
9 | }
10 |
11 | var upload = (slug, json) => {
12 |
13 | var string = JSON.stringify(json);
14 |
15 | return new Promise((res, rej) => {
16 |
17 | var publish = s3.put(file, {
18 | 'Content-Length': Buffer.byteLength(string),
19 | 'Content-Type': 'application/json',
20 | 'x-amz-acl': 'public-read'
21 | });
22 |
23 | publish.on('response', function(response){
24 | if (200 == response.statusCode) {
25 | res();
26 | } else {
27 | rej();
28 | }
29 | });
30 |
31 | publish.end(string);
32 |
33 | });
34 |
35 | }
36 |
37 | var remove = (slug) => {
38 |
39 | var path = location + '/locator-maps/' + slug + '.json';
40 |
41 | return new Promise((res, rej) => {
42 | s3.del(path)
43 | .on('response', function(){
44 | res();
45 | }).end();
46 | });
47 |
48 | }
49 |
50 | var enabled = () => (!!process.env.AWS_S3_KEY);
51 |
52 |
53 | module.exports = { upload: upload, remove: remove, enabled: enabled };
--------------------------------------------------------------------------------
/app/setup-check.js:
--------------------------------------------------------------------------------
1 | module.exports = function( callback ){
2 | var fs = require('fs');
3 | var configStr = fs.readFileSync('config.json', 'utf8');
4 | if (!testJSON(configStr)) {
5 | throw('Error: config.json is missing or invalid.');
6 | }
7 |
8 | var config = JSON.parse(configStr);
9 |
10 | if (
11 | (config.googleMapsAPIKey === 'REPLACE WITH API KEY') ||
12 | (config.googleMapsAPIKey === undefined)
13 | ) {
14 | throw('Error: You need to add a Google Maps API key to config.json');
15 | }
16 |
17 | if (!process.env.DATABASE_URL) {
18 | throw('Error: Database URL environment variable ("DATABASE_URL") is not set.')
19 | }
20 |
21 | callback();
22 |
23 | function testJSON(str){
24 | try {
25 | JSON.parse(str);
26 | } catch (err) {
27 | return false;
28 | }
29 | return true;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinpoint-editor",
3 | "version": "1.1.0",
4 | "authors": [
5 | "ejb "
6 | ],
7 | "license": "MIT",
8 | "ignore": [
9 | "**/.*",
10 | "node_modules",
11 | "bower_components",
12 | "test",
13 | "tests",
14 | "public/bower_components/"
15 | ],
16 | "dependencies": {
17 | "pinpoint": "~1.1.0",
18 | "angular": "~1.3.13",
19 | "bootstrap": "~3.3.2",
20 | "leaflet": "~0.7.3",
21 | "jquery": "~2.1.3",
22 | "leaflet-minimap": "~2.1.0",
23 | "angular-route": "~1.3.13"
24 | },
25 | "devDependencies": {
26 | "angular-mocks": "~1.3.13"
27 | },
28 | "resolutions": {
29 | "angular": "~1.3.13"
30 | },
31 | "description": "A web app for creating and editing Pinpoint maps.",
32 | "main": "server.js"
33 | }
34 |
--------------------------------------------------------------------------------
/build/migrate.sql:
--------------------------------------------------------------------------------
1 | --
2 | -- PostgreSQL database dump
3 | --
4 |
5 | SET statement_timeout = 0;
6 | SET lock_timeout = 0;
7 | SET client_encoding = 'UTF8';
8 | SET standard_conforming_strings = on;
9 | SET check_function_bodies = false;
10 | SET client_min_messages = warning;
11 |
12 | SET search_path = public, pg_catalog;
13 |
14 | SET default_tablespace = '';
15 |
16 | SET default_with_oids = false;
17 |
18 | --
19 | -- Name: locator_map; Type: TABLE; Schema: public; Owner: -; Tablespace:
20 | --
21 |
22 | CREATE TABLE locator_map (
23 | id bigint NOT NULL,
24 | data json
25 | );
26 |
27 |
28 | --
29 | -- Name: locator_map_id_seq; Type: SEQUENCE; Schema: public; Owner: -
30 | --
31 |
32 | CREATE SEQUENCE locator_map_id_seq
33 | START WITH 1
34 | INCREMENT BY 1
35 | NO MINVALUE
36 | NO MAXVALUE
37 | CACHE 1;
38 |
39 |
40 | --
41 | -- Name: locator_map_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
42 | --
43 |
44 | ALTER SEQUENCE locator_map_id_seq OWNED BY locator_map.id;
45 |
46 |
47 | --
48 | -- Name: id; Type: DEFAULT; Schema: public; Owner: -
49 | --
50 |
51 | ALTER TABLE ONLY locator_map ALTER COLUMN id SET DEFAULT nextval('locator_map_id_seq'::regclass);
52 |
53 |
54 | --
55 | -- Data for Name: locator_map; Type: TABLE DATA; Schema: public; Owner: -
56 | --
57 |
58 | COPY locator_map (id, data) FROM stdin;
59 | 1 {"slug": "the-news-building", "aspect-ratio": "tall", "dek": "This is the dek", "hed": "Test Hed", "lat": 51.505864, "lon": -0.087765, "minimap": true, "zoom": 15, "markers": [{"lat": 51.505864, "lon": -0.087765, "text": "The News Building", "icon": "square", "label": "callout", "label-direction": "north"}], "creation_date":1423845091336,"modification_date":1423845091336}
60 | \.
61 |
62 |
63 | --
64 | -- Name: locator_map_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
65 | --
66 |
67 | SELECT pg_catalog.setval('locator_map_id_seq', 11, true);
68 |
69 |
70 | --
71 | -- Name: locator_map_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
72 | --
73 |
74 | ALTER TABLE ONLY locator_map
75 | ADD CONSTRAINT locator_map_pkey PRIMARY KEY (id);
76 |
77 |
78 | --
79 | -- PostgreSQL database dump complete
80 | --
81 |
82 |
--------------------------------------------------------------------------------
/config-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "geojsonStyles": [
3 | {
4 | "class": "dashed",
5 | "name": "Dashed"
6 | },
7 | {
8 | "class": "dashed-clockwise",
9 | "name": "Dashed, animated clockwise"
10 | },
11 | {
12 | "class": "dashed-anticlockwise",
13 | "name": "Dashed, animated anticlockwise"
14 | }
15 | ],
16 | "previewLink": "http://preview-site.com/",
17 | "helpLink": "http://github.com/dowjones/pinpoint-editor",
18 | "liveLink": "http://production-site.com/",
19 | "s3url": "https://my-bucket.s3.amazonaws.com/projects/locator-maps/",
20 | "basemaps": [
21 | {
22 | "name": "An example basemap",
23 | "url": "https://{s}.tiles.mapbox.com/v3/mybasemap/{z}/{x}/{y}.png",
24 | "credit": "Leaflet | © Mapbox | © OpenStreetMap "
25 | }
26 | ],
27 | "googleMapsAPIKey": "API_KEY_GOES_HERE"
28 | }
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "geojsonStyles": [
3 | {
4 | "class": "dashed",
5 | "name": "Dashed"
6 | },
7 | {
8 | "class": "dashed-clockwise",
9 | "name": "Dashed, animated clockwise"
10 | },
11 | {
12 | "class": "dashed-anticlockwise",
13 | "name": "Dashed, animated anticlockwise"
14 | }
15 | ],
16 | "title": "Pinpoint Editor",
17 | "googleMapsAPIKey": "REPLACE WITH API KEY"
18 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | postgres:
4 | build:
5 | context: .
6 | dockerfile: ./Dockerfile.postgres
7 | pinpoint:
8 | build: .
9 | environment:
10 | - DATABASE_URL=postgresql://postgres@postgres/postgres
11 | ports:
12 | - "3001:3001"
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pinpoint-editor",
3 | "version": "1.2.1",
4 | "description": "A web app for creating and editing Pinpoint maps.",
5 | "main": "server.js",
6 | "author": "ejb ",
7 | "scripts": {
8 | "start": "node server.js",
9 | "postinstall": "bower install"
10 | },
11 | "dependencies": {
12 | "body-parser": "~1.5.2",
13 | "express": "~4.7.2",
14 | "knex": "^0.6.22",
15 | "knox": "^0.9.2",
16 | "lodash": "^3.1.0",
17 | "moment": "^2.9.0",
18 | "morgan": "~1.2.2",
19 | "pg": "^4.2.0",
20 | "pug": "^2.0.0-beta10"
21 | },
22 | "devDependencies": {
23 | "bower": "^1.8.4",
24 | "jasmine-core": "~2.2.0",
25 | "karma": "~0.12.31",
26 | "karma-chrome-launcher": "~0.1.7",
27 | "karma-jasmine": "~0.3.5",
28 | "karma-phantomjs-launcher": "~0.1.4"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/public/directives/button-group.js:
--------------------------------------------------------------------------------
1 | pinpointTool.directive('buttonGroup', function(){
2 | return {
3 | restrict:'E',
4 | replace:true,
5 | // transclude:true,
6 | scope: {
7 | value:'=',
8 | options:'=',
9 | labels:'=?'
10 | },
11 | template: '
',
12 | link: function($scope, elm, attrs){
13 | var $elm = $(elm);
14 | $.each($scope.options, function(i, o){
15 | if ($scope.labels && $scope.labels[i]) {
16 | var label = $scope.labels[i];
17 | } else {
18 | var label = o;
19 | }
20 | $elm.append(''+label+' ');
21 | });
22 | $elm.find('.btn[data-val="'+$scope.value+'"]').addClass('active');
23 | $elm.find('.btn').click(function(){
24 | var $this = $(this);
25 | $elm.find('.btn').removeClass('active');
26 | $this.addClass('active');
27 | var val = $this.attr('data-val');
28 | $scope.value = val;
29 | $scope.$apply();
30 | setTimeout(function() {
31 | $scope.$apply();
32 | },50);
33 | });
34 | }
35 | }
36 | });
--------------------------------------------------------------------------------
/public/directives/geojson-input.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | angular
3 | .module('pinpointTool')
4 | .directive('geojsonInput', geojsonInput);
5 |
6 | geojsonInput.$inject = ['configService'];
7 | function geojsonInput(configService){
8 | return {
9 | restrict: 'E',
10 | templateUrl: 'partials/geojson-input.html',
11 | scope: {
12 | geojson:'=?'
13 | },
14 | link: function ($scope, element, attrs) {
15 | // configure styles
16 | $scope.geojsonStyles = configService.geojsonStyles;
17 | $scope.geojsonStyles.unshift({
18 | name: 'Default',
19 | class: ''
20 | });
21 |
22 | // load in existing geojson
23 | $scope.geojsonRaw = [];
24 | if ($scope.geojson && $scope.geojson.features.length > 0) {
25 | for (var i = 0; i < $scope.geojson.features.length; i++) {
26 | var feature = $scope.geojson.features[i];
27 | $scope.geojsonRaw[i] = {
28 | valid: true,
29 | style: feature.properties.pinpointStyle,
30 | value: JSON.stringify( feature )
31 | };
32 | }
33 | }
34 | $scope.geojson = {
35 | type: 'FeatureCollection',
36 | features: []
37 | };
38 |
39 | // add/remove geojsonRaw items
40 | $scope.removeFeature = function(feature){
41 | var index = $scope.geojsonRaw.indexOf(feature);
42 | if (index > -1) {
43 | $scope.geojsonRaw.splice(index, 1);
44 | }
45 | }
46 | $scope.addFeature = function(){
47 | $scope.geojsonRaw.push({
48 | value: '',
49 | style: '',
50 | valid: true
51 | });
52 | }
53 |
54 | // validate geojsonRaw
55 | // and pass it back to geojson
56 | $scope.$watch(function(){
57 | var geojsonRaw = $scope.geojsonRaw;
58 | for (var i = 0; i < geojsonRaw.length; i++) {
59 | var valid = true;
60 | try {
61 | var parsed = JSON.parse(geojsonRaw[i].value);
62 | if (!parsed.properties || !parsed.properties.pinpointStyle) {
63 | parsed.properties = {
64 | pinpointStyle: geojsonRaw[i].style
65 | };
66 | } else {
67 | parsed.properties.pinpointStyle = geojsonRaw[i].style;
68 | }
69 | $scope.geojson.features[i] = parsed;
70 | geojsonRaw[i].valid = true;
71 | } catch (err) {
72 | geojsonRaw[i].valid = false;
73 | }
74 | }
75 | }); // $scope.$watch
76 |
77 | } // link
78 | };
79 | }
80 | })();
--------------------------------------------------------------------------------
/public/directives/google-places.js:
--------------------------------------------------------------------------------
1 | pinpointTool.directive('googlePlaces', function(){
2 | return {
3 | restrict:'E',
4 | replace:true,
5 | // transclude:true,
6 | scope: {
7 | location:'=',
8 | locationName:'=?',
9 | placeholder:'@'
10 | },
11 | template: ' ',
12 | link: function($scope, elm, attrs){
13 | var autocomplete = new google.maps.places.Autocomplete(elm[0], {});
14 | google.maps.event.addListener(autocomplete, 'place_changed', function() {
15 | var place = autocomplete.getPlace();
16 | $scope.location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
17 | $scope.locationName = place.name;
18 | $scope.$apply();
19 | elm.val(''); // clear text
20 | });
21 | }
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/public/directives/live-link.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | angular
3 | .module('pinpointTool')
4 | .directive('liveLink', liveLink);
5 |
6 | liveLink.$inject = ['$http', 'configService'];
7 | function liveLink($http, configService){
8 | return {
9 | restrict:'A',
10 | replace:false,
11 | scope: {
12 | published:'=?'
13 | },
14 | link: function($scope, elm, attrs){
15 | function disable(){
16 | $(elm).attr('disabled', true);
17 | $(elm).text('Unpublished');
18 | }
19 | function enable(){
20 | $(elm).attr('disabled', false);
21 | $(elm).text('Live link');
22 | }
23 | var url = configService.liveLink + attrs.slug;
24 | $(elm).attr('href',url).attr('target','_blank');
25 | if (configService.s3url) {
26 | var ajax_url = configService.s3url + attrs.slug + '.json';
27 | $http.get(ajax_url).error(disable);
28 | } else {
29 | enable();
30 | }
31 | $scope.$watch(function(){
32 | if ($scope.published === true) {
33 | enable();
34 | }
35 | });
36 | }
37 | }
38 | }
39 |
40 | })();
--------------------------------------------------------------------------------
/public/directives/map-rough-preview.js:
--------------------------------------------------------------------------------
1 | pinpointTool.directive('mapRoughPreview', ['configService', function(configService){
2 | return {
3 | restrict:'E',
4 | replace:false,
5 | template: '
',
6 | link: function($scope, elm, attrs){
7 | var mapOptions = {
8 | scrollWheelZoom: false,
9 | keyboard: false,
10 | attributionControl: false,
11 | zoomControl: false
12 | };
13 | var mapEl = $(elm).find('.prevmap')[0];
14 | var map = L.map(mapEl, mapOptions)
15 | .setView([attrs.lat, attrs.lon], attrs.zoom-1);
16 |
17 | var basemap = attrs.basemap;
18 | if (!basemap && (configService.basemaps.length > 0)) {
19 | basemap = $scope.config.basemaps[0].url;
20 | } else if (!basemap) {
21 | basemap = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png';
22 | }
23 | L.tileLayer( basemap ).addTo(map);
24 | }
25 | }
26 | }]);
27 |
--------------------------------------------------------------------------------
/public/directives/preview-link.js:
--------------------------------------------------------------------------------
1 | pinpointTool.directive('previewLink', ['configService', function(configService){
2 | return {
3 | restrict:'A',
4 | replace:false,
5 | link: function($scope, elm, attrs){
6 | if (!configService.previewLink) {
7 | return $(elm).hide();
8 | }
9 | var layout = 'margin';
10 | var url = configService.previewLink + attrs.slug;
11 | $(elm).attr('href',url).attr('target','_blank');
12 | }
13 | }
14 | }]);
15 |
--------------------------------------------------------------------------------
/public/directives/published-check.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | angular
3 | .module('pinpointTool')
4 | .directive('publishedCheck', publishedCheck);
5 |
6 | publishedCheck.$inject = ['$http', 'configService'];
7 | function publishedCheck($http, configService){
8 | return {
9 | restrict:'A',
10 | replace:false,
11 | link: function($scope, elm, attrs){
12 | function disable(){
13 | $(elm).html('unpublished');
14 | }
15 | function enable(){
16 | $(elm)
17 | .html(' published')
18 | .removeClass('label-default')
19 | .addClass('label-primary');
20 | }
21 | if (configService.s3url) {
22 | var ajax_url = configService.s3url + attrs.slug + '.json';
23 | $http.get(ajax_url).success(enable).error(disable);
24 | } else {
25 | enable();
26 | }
27 | }
28 | }
29 | }
30 |
31 | })();
--------------------------------------------------------------------------------
/public/directives/unique-slug.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | angular
3 | .module('pinpointTool')
4 | .directive('uniqueSlug', uniqueSlug);
5 |
6 | uniqueSlug.$inject = ['$http'];
7 | function uniqueSlug($http){
8 | return {
9 | restrict: 'A',
10 | require: 'ngModel',
11 | link: function ($scope, element, attrs, ngModel) {
12 |
13 | if ($scope.state === 'update') return;
14 |
15 | function validate(value) {
16 | if ($scope.slug === '') {
17 | ngModel.$setValidity('unique', false);
18 | }
19 | if ($scope.map.id || ($scope.slug && $scope.slug.indexOf(ngModel.$viewValue) === -1)) {
20 | ngModel.$setValidity('unique', true);
21 | } else {
22 | ngModel.$setValidity('unique', false);
23 | }
24 | }
25 |
26 | $http.get('/api/slugs').success( function (slugs) {
27 | $scope.slug = slugs;
28 | validate(ngModel.$viewValue);
29 | });
30 |
31 | $scope.$watch( function() {
32 | return ngModel.$viewValue;
33 | }, validate);
34 | }
35 | };
36 | }
37 | })();
--------------------------------------------------------------------------------
/public/partials/aspect-ratio-selector.html:
--------------------------------------------------------------------------------
1 |
2 | Wide
3 | Square
4 | Tall
5 |
--------------------------------------------------------------------------------
/public/partials/geojson-input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GeoJSON features
4 |
5 |
6 |
For advanced users only: Paste GeoJSON features below to include them on map. Point, MultiPoint and FeatureCollection types are not allowed. GeoJSON.io is a good editor.
7 |
8 |
9 |
10 |
11 | GeoJSON feature {{i+1}}
12 | Delete
13 |
14 |
15 |
16 |
17 |
18 |
19 | Style:
20 |
22 |
23 |
24 |
25 |
32 |
33 |
34 |
35 |
36 |
Add geoJSON feature
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/partials/map-detail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
37 |
38 |
39 |
209 |
210 |
Delete this map
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 | Are you sure you want to delete this map?
225 |
226 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
--------------------------------------------------------------------------------
/public/partials/map-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
16 |
17 |
22 |
23 |
36 |
37 |
38 |
39 |
Load more
40 |
Switch to list view
41 |
42 |
43 |
44 |
45 |
46 |
Create new map
47 |
48 |
49 |
50 |
51 | Slug
52 | Hed
53 | Last updated
54 |
55 |
56 |
57 |
58 | {{map.slug}}
59 | {{map.hed}}
60 | {{map.modification_date | date:"MM/dd/yyyy 'at' h:mma"}}
61 | Preview
62 | Live link
63 |
64 |
65 |
66 |
67 |
68 |
Switch to expanded view
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/public/partials/publish-modal.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | Published to S3
12 |
13 |
18 |
19 |
Raw JSON
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/public/script.js:
--------------------------------------------------------------------------------
1 | var pinpointTool = angular.module('pinpointTool', ['ngRoute']);
2 |
3 | pinpointTool.provider('configService', function () {
4 | var options = {};
5 | this.config = function (opt) {
6 | angular.extend(options, opt);
7 | };
8 | this.$get = [function () {
9 | if (!options) {
10 | throw new Error('Config options must be configured');
11 | }
12 | return options;
13 | }];
14 | })
15 |
16 | angular.element(document).ready(function () {
17 | $.get('/config.json', function (data) {
18 |
19 | angular.module('pinpointTool').config(['configServiceProvider', function (configServiceProvider) {
20 | configServiceProvider.config(data);
21 | }]);
22 |
23 | angular.bootstrap(document, ['pinpointTool']);
24 | });
25 | });
26 |
27 | pinpointTool.config(['$routeProvider',
28 | function($routeProvider) {
29 | $routeProvider.
30 | when('/maps', {
31 | templateUrl: 'partials/map-list.html',
32 | controller: 'mapListCtrl'
33 | }).
34 | when('/maps/:mapId', {
35 | templateUrl: 'partials/map-detail.html',
36 | controller: 'mapDetailCtrl'
37 | }).
38 | when('/maps/new', {
39 | templateUrl: 'partials/map-detail.html',
40 | controller: 'mapDetailCtrl'
41 | }).
42 | otherwise({
43 | redirectTo: '/maps'
44 | });
45 | }]);
46 |
47 | /////////////////
48 | // EDITOR
49 | /////////////////
50 |
51 | pinpointTool.controller('mapDetailCtrl',
52 | ['$scope', '$routeParams', '$http', '$location', 'mapHelper', 'markerStyles', 'mapDefaults', 'dataWrangler', 'configService',
53 | function ($scope, $routeParams, $http, $location, mapHelper, markerStyles, mapDefaults, dataWrangler, configService ) {
54 |
55 | $scope.mapId = $routeParams['mapId'];
56 | $scope.icons = markerStyles.icons;
57 | $scope.labels = markerStyles.labels;
58 | $scope.aspectRatios = ['wide','square','tall'];
59 | $scope.pickedLocation = {};
60 | $scope.config = configService;
61 | var basemaps = [];
62 | if ('basemaps' in $scope.config) {
63 | $scope.basemapNames = $scope.config.basemaps.map(function(d,i) { return d.name });
64 | }
65 | if (basemaps[0]) {
66 | $scope.basemap = basemaps[0].name;
67 | }
68 |
69 | if ($scope.mapId === 'new') {
70 | $scope.map = $.extend({}, mapDefaults.map);
71 | $scope.map.aspectRatio = $scope.map['aspect-ratio'];
72 | } else {
73 | $http.get('/api/maps/'+$scope.mapId)
74 | .success(function(data) {
75 | $scope.map = data;
76 | $.extend({}, mapDefaults.map, $scope.map);
77 |
78 | $scope = dataWrangler.setupExisting($scope);
79 |
80 | })
81 | .error(function(){
82 | $location.path('/maps');
83 | });
84 | }
85 |
86 | $scope.$watch(function() {
87 |
88 | // This is a horribly ugly hack, but I am at my wit's end
89 | var selectedBasemapName = $('.basemap-selector .btn.active').text();
90 |
91 | if ($scope.map) {
92 | if ((selectedBasemapName !== '') && ($scope.map !== undefined)) {
93 | var selectedBasemap = $scope.config.basemaps.filter(function(basemap) {
94 | return basemap.name === selectedBasemapName;
95 | })[0];
96 | $scope.map.basemap = selectedBasemap.url;
97 | $scope.map.basemapCredit = selectedBasemap.credit;
98 | } else if (!$scope.map.basemap && $scope.config.basemaps.length) {
99 | $scope.map.basemap = $scope.config.basemaps[0].url;
100 | }
101 | $scope.map = dataWrangler.onWatch($scope.map);
102 | $scope.cleanMap = JSON.stringify( dataWrangler.cleanMapObj($scope.map), null, 2 );
103 | $scope.pinpoint = mapHelper.buildPreview($scope.map, changeMap, changeMap, changeMarker);
104 | }
105 | });
106 |
107 | $scope.$watch('quickstartLatLonString', function(val){
108 | if (val) {
109 | $scope.map.latLonString = val;
110 | var coords = {
111 | lat: val.split(',')[0],
112 | lon: val.split(',')[1]
113 | }
114 | $scope.addMarker( coords, $scope.quickstartName );
115 |
116 | }
117 | });
118 |
119 | $scope.showPublishModal = function(){
120 | $scope.publishModal = true;
121 | }
122 | $scope.hidePublishModal = function(){
123 | $scope.publishModal = false;
124 | }
125 |
126 | $scope.$watch('map.published', function(val){
127 | if (val === true) {
128 | $scope.save();
129 | }
130 | });
131 |
132 |
133 | function changeMap(ev){
134 | var newLatLon = ev.target.getCenter();
135 | var newZoom = ev.target.getZoom();
136 | $scope.map.latLonString = newLatLon.lat+','+newLatLon.lng;
137 | $scope.map.zoom = newZoom;
138 | $scope.$$childHead.mapform.$setDirty();
139 | $scope.$apply();
140 | }
141 |
142 | function changeMarker(ev){
143 | var marker = ev.target;
144 | var newLatLon = marker._latlng;
145 | $.each($scope.map.markers, function(i,m){
146 | if (marker.options.title === i) {
147 | $scope.map.markers[i].latLonString = newLatLon.lat+','+newLatLon.lng;
148 | }
149 | });
150 | $scope.$$childHead.mapform.$setDirty();
151 | $scope.$apply();
152 | }
153 |
154 |
155 | $scope.$on('$destroy', function() {
156 | window.onbeforeunload = undefined;
157 | });
158 | $scope.$on('$locationChangeStart', function(event, next, current) {
159 | if ($scope.$$childHead.mapform && !$scope.$$childHead.mapform.$pristine && !$scope.bypassSaveDialog) {
160 | if(!confirm("Leave page without saving?")) {
161 | event.preventDefault();
162 | }
163 | }
164 | });
165 |
166 |
167 |
168 | $scope.removeMarker = function(marker){
169 | var index = $scope.map.markers.indexOf(marker);
170 | if (index > -1) {
171 | $scope.map.markers.splice(index, 1);
172 | }
173 | }
174 | $scope.addMarker = function(center, label){
175 | if ($scope.map.markers.length > 4) {
176 | return;
177 | }
178 |
179 | var newMarker = $.extend({}, mapDefaults.marker);
180 | if ($scope.pinpoint) {
181 | center = center || $scope.pinpoint.map.getCenter();
182 | newMarker.lat = center.lat;
183 | newMarker.lon = center.lng || center.lon;
184 | newMarker.latLonString = newMarker.lat+','+newMarker.lon;
185 | }
186 | newMarker.text = label || '';
187 | newMarker.labelDirection = newMarker['label-direction'];
188 | $scope.map.markers.push( newMarker );
189 | }
190 |
191 | $scope.save = function(valid){
192 | if (valid === false) {
193 | return;
194 | }
195 | $scope.saving = true;
196 | var dirty = JSON.parse(JSON.stringify($scope.map));
197 | var clean = dataWrangler.cleanMapObj(dirty);
198 | if ($scope.map.id && ($scope.map.id !== 'new')) {
199 | // update map
200 | $http
201 | .put('/api/maps/'+$scope.mapId, clean)
202 | .success(function(){
203 | $scope.saving = false;
204 | if ($scope.$$childHead.mapform) {
205 | $scope.$$childHead.mapform.$setPristine();
206 | }
207 | });
208 | } else {
209 | // create a new map
210 | $http
211 | .post('/api/maps/', clean)
212 | .success(function(d){
213 | $scope.map.id = d.id;
214 | $scope.saving = false;
215 | $location.path('/maps/'+d.id);
216 | $scope.$$childHead.mapform.$setPristine();
217 | });
218 | }
219 | if ($scope.map.published === true) {
220 | $scope.publish();
221 | }
222 | }
223 | $scope.publish = function(valid){
224 | if (valid === false) {
225 | return;
226 | }
227 | var dirty = JSON.parse(JSON.stringify($scope.map));
228 | var clean = dataWrangler.cleanMapObj(dirty);
229 | if ($scope.mapId !== 'new') {
230 | clean.id = +$scope.mapId;
231 | }
232 |
233 | $http
234 | .post('/api/publish/', clean)
235 | .success(function(e,r){
236 | $scope.$$childHead.mapform.$setPristine();
237 | $scope.published = true;
238 | })
239 | .error(function(){
240 | alert('Not published due to error');
241 | });
242 | }
243 | $scope.delete = function(){
244 | $scope.deleteModal = true;
245 | }
246 | $scope.cancelDelete = function(){
247 | $scope.deleteModal = false;
248 | }
249 | $scope.definitelyDelete = function(){
250 | if ($scope.map.id && ($scope.map.id !== 'new')) {
251 | // existing map
252 | $http
253 | .delete('/api/maps/'+$scope.map.id)
254 | .success(function(e,r){
255 | alert('Map deleted');
256 | $scope.bypassSaveDialog = true;
257 | $location.path('/maps/');
258 | })
259 | .error(function(){
260 | alert('Not deleted due to error');
261 | $scope.deleteModal = false;
262 | });
263 |
264 | } else {
265 | $scope.bypassSaveDialog = true;
266 | $location.path('/maps/');
267 |
268 | }
269 | }
270 |
271 |
272 | }]);
273 |
274 |
275 | pinpointTool.factory('mapHelper', [function() {
276 | var p;
277 | var build = function(opts, dragend, zoomend, markerdragend){
278 | opts.dragend = dragend;
279 | opts.zoomend = zoomend;
280 | opts.markerdragend = markerdragend;
281 |
282 | $('.map-outer.inactive').html('
');
283 | if (typeof p !== 'undefined') {
284 | try {
285 | p.remove();
286 | } catch (err) {
287 | //
288 | }
289 | }
290 | opts.creation = true;
291 | opts.el = '.map-preview';
292 | if ( $(opts.el).length === 1 ) {
293 | $(opts.el).attr('class',opts.el.replace('.','') + ' '+opts['aspect-ratio']);
294 | p = new Pinpoint(opts);
295 | }
296 | return p;
297 | };
298 | var splitLatLonString = function(string){
299 | if (!string) {
300 | return [0,0];
301 | }
302 | var lat = +string.replace(/\s/g,'').split(',')[0];
303 | var lon = +string.replace(/\s/g,'').split(',')[1];
304 | return [lat, lon];
305 | }
306 |
307 | return {
308 | buildPreview: build,
309 | splitLatLonString: splitLatLonString
310 | };
311 | }]);
312 |
313 | pinpointTool.factory('markerStyles', function() {
314 |
315 | var icons = [
316 | "square",
317 | "circle",
318 | "none"
319 | ];
320 | var labels_obj = [
321 | {
322 | "name": "plain",
323 | "directions": [
324 | "north",
325 | "northeast",
326 | "east",
327 | "southeast",
328 | "south",
329 | "southwest",
330 | "west",
331 | "northwest"
332 | ]
333 | },
334 | {
335 | "name": "callout",
336 | "directions": [
337 | "north",
338 | "south"
339 | ]
340 | }
341 | ];
342 | var getDirectionsforLabel = function( label ) {
343 | labels_obj.forEach(function(l){
344 | if (l.name === label) {
345 | return l.directions;
346 | }
347 | });
348 | }
349 | var labels = [], labels_directions = [];
350 | labels_obj.forEach(function(l){
351 | labels.push(l.name);
352 | });
353 | labels_obj.forEach(function(l){
354 | labels_directions[l.name] = l.directions;
355 | });
356 |
357 | return {
358 | icons: icons,
359 | labels: labels,
360 | directions: labels_directions
361 | };
362 | });
363 |
364 | pinpointTool.value('mapDefaults', {
365 | map: {
366 | hed: '',
367 | dek: '',
368 | lat: 51.5049378,
369 | lon: -0.0870377,
370 | latLonString: '51.5049378, -0.0870377',
371 | zoom: 4,
372 | minimap: false,
373 | "aspect-ratio": 'wide',
374 | "minimap-zoom-offset": -5,
375 | markers: []
376 | },
377 | marker: {
378 | lat: 0,
379 | lon: 0,
380 | text: "",
381 | icon: "square",
382 | "label-direction": "north"
383 | }
384 | });
385 |
386 | pinpointTool.factory('dataWrangler', ['mapHelper', 'markerStyles', function(mapHelper, markerStyles){
387 | var clean = function(input){
388 | var output = JSON.parse(JSON.stringify(input));
389 | var toDelete = [
390 | 'labelDirections',
391 | 'latLonString',
392 | 'el',
393 | 'id',
394 | 'aspectRatio',
395 | 'minimapZoomOffset',
396 | 'labelDirection',
397 | 'creation',
398 | 'creation_date',
399 | 'modification_date'
400 | ];
401 | $.each(toDelete, function(i, d){
402 | delete output[d];
403 | });
404 | $.each(input.markers, function(j, marker){
405 | $.each(toDelete, function(i, d){
406 | delete output.markers[j][d];
407 | });
408 | });
409 | if (output.geojson && output.geojson.features.length === 0) {
410 | delete output.geojson;
411 | }
412 | if (output.markers.length === 0) {
413 | delete output.markers;
414 | }
415 | return output;
416 | }
417 | var setupExisting = function(scope) {
418 | if (scope.map.lat && scope.map.lon) {
419 | scope.map.latLonString = scope.map.lat + ',' + scope.map.lon;
420 | } else {
421 | scope.map.latLonString = '51.5049378,-0.0870377';
422 | }
423 | scope.map.minimapZoomOffset = scope.map['minimap-zoom-offset'];
424 | scope.map.aspectRatio = scope.map['aspect-ratio'];
425 |
426 | if (typeof scope.map.minimapZoomOffset !== 'number') {
427 | scope.map.minimapZoomOffset = -5;
428 | }
429 |
430 | scope.map.markers = scope.map.markers || [];
431 | $.each(scope.map.markers, function(i,m){
432 | if (m.lat && m.lon) {
433 | m.latLonString = m.lat + ',' + m.lon;
434 | } else {
435 | m.latLonString = '51.5049378,-0.0870377';
436 | }
437 | m.labelDirections = markerStyles.directions[m.label];
438 | m['label-direction'] = m['label-direction'] || m.labelDirections[0];
439 | scope.map.markers[i] = m;
440 | });
441 |
442 | if (scope.map.basemap && scope.config.basemaps) {
443 | scope.basemap = scope.config.basemaps.filter(function(b) {
444 | return b.url === scope.map.basemap;
445 | })[0];
446 | }
447 |
448 | return scope;
449 |
450 | }
451 | var watch = function(map){
452 | map.zoom = parseInt( map.zoom );
453 | map.lat = mapHelper.splitLatLonString(map.latLonString)[0];
454 | map.lon = mapHelper.splitLatLonString(map.latLonString)[1];
455 | map['minimap-zoom-offset'] = +map.minimapZoomOffset || map['minimap-zoom-offset'];
456 | map['aspect-ratio'] = map.aspectRatio || map['aspect-ratio'];
457 | $.each(map.markers, function(i,m){
458 | m.labelDirections = markerStyles.directions[m.label];
459 | m['label-direction'] = m.labelDirection || m['label-direction'];
460 | m.lat = mapHelper.splitLatLonString(m.latLonString)[0];
461 | m.lon = mapHelper.splitLatLonString(m.latLonString)[1];
462 | map.markers[i] = m;
463 | });
464 |
465 | return map;
466 | }
467 | return {
468 | cleanMapObj: clean,
469 | setupExisting: setupExisting,
470 | onWatch: watch
471 | }
472 | }]);
473 |
474 | /////////////////
475 | // HOMEPAGE
476 | /////////////////
477 |
478 | pinpointTool.controller('mapListCtrl',
479 | ['$scope', '$http', '$location', '$filter', '$sce', 'configService', function ($scope, $http, $location, $filter, $sce, configService) {
480 | $scope.config = configService;
481 | $scope.listView = false;
482 | $scope.changeView = function(){
483 | $scope.listView = !$scope.listView;
484 | }
485 | $scope.maps = [];
486 | $scope.allMaps = [];
487 | $http.get('/api/maps').success(function(data) {
488 | $scope.allMaps = $filter('orderBy')(data, 'creation_date', true);;
489 | $scope.loadMore();
490 | });
491 |
492 | var numberToLoadEachTime = 10;
493 | $scope.loadMore = function() {
494 | $scope.maps = $scope.allMaps.slice(0, $scope.maps.length + numberToLoadEachTime);
495 | $scope.hideLoadMore = ($scope.maps.length === $scope.allMaps.length);
496 | }
497 |
498 | $scope.previewLink = function(map){
499 | if (map['aspect-ratio'] === 'wide') {
500 | var layout = 'offset';
501 | } else {
502 | var layout = 'margin';
503 | }
504 | var url = configService.previewLink + map.slug;
505 | return url;
506 | }
507 | $scope.liveLink = function(map){
508 | var url = configService.liveLink+attr.slug;
509 | return url;
510 | }
511 |
512 | }]);
513 |
514 | pinpointTool.filter('html', function($sce) {
515 | return function(val) {
516 | return $sce.trustAsHtml(val);
517 | };
518 | });
519 |
520 |
--------------------------------------------------------------------------------
/public/style.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | padding-bottom: 20px;
4 | }
5 |
6 |
7 |
8 | /* menu */
9 |
10 | .thumbnail {
11 | min-height: 240px !important;
12 | }
13 | .thumbnail:hover {
14 | text-decoration: none;
15 | }
16 |
17 | h4 small {
18 | white-space: nowrap;
19 | }
20 |
21 | .published-label {
22 | opacity: 0.75;
23 | }
24 |
25 |
26 | /* map editor */
27 |
28 | .map-outer {
29 | // height: 400px;
30 | }
31 |
32 | #map {
33 | height: 100%;
34 | }
35 |
36 | .container {
37 | margin-top: 40px;
38 | }
39 |
40 | .marker-box {
41 | // border: 1px solid #ddd;
42 | // padding: 20px;
43 | }
44 |
45 | .map-preview {
46 | width: 300px;
47 | background: white;
48 | }
49 |
50 | .map-preview.wide {
51 | // width: 540px;
52 | }
53 |
54 | .delete-marker {
55 | float: right;
56 | padding: 0;
57 | color: white;
58 | }
59 | .delete-marker:hover {
60 | color: white;
61 | }
62 | .viewToggle {
63 | float: right;
64 | }
65 | .prevmap {
66 | height: 100px;
67 | pointer-events: none;
68 | }
69 | .new-map {
70 | text-align: center;
71 | padding: 60px 0;
72 | }
73 |
74 |
75 | .search-form {
76 | border-radius: 30px;
77 | }
78 |
79 | .save-button {
80 | margin-left: 20px;
81 | }
82 |
83 | .save-notice {
84 | font-size: 12px;
85 | color: #777;
86 | display: inline-block;
87 | margin-left: 10px;
88 | }
89 |
90 | .delete-true-button {
91 | margin-right: 50px !important;
92 | }
93 |
94 | input[type="range"] {
95 | width: 25%;
96 | display: inline-block;
97 | vertical-align: middle;
98 | margin-left: 10px;
99 | }
100 |
101 | .help-button {
102 | float: right;
103 | }
104 |
105 | .zoom-input {
106 | width: 40px;
107 | text-align: center;
108 | }
109 |
110 | div.leaflet-control-minimap.leaflet-container.leaflet-retina.leaflet-fade-anim.leaflet-control,
111 | a.leaflet-control-zoom-in {
112 | box-sizing: content-box;
113 | }
114 |
115 | label[disabled] {
116 | cursor: default;
117 | color: #aaa;
118 | }
119 |
120 | .json-input {
121 | font-family: 'Andale Mono', monospace;
122 | font-size: 11px;
123 | -webkit-user-select: all;
124 | -moz-user-select: all;
125 | -ms-user-select: all;
126 | user-select: all;
127 | }
128 |
129 | .geojson-input {
130 | font-family: 'Andale Mono', monospace;
131 | font-size: 10px;
132 | }
133 |
134 | .modal-dialog {
135 | z-index: 50000;
136 | }
137 |
138 | @media (min-width: 800px) {
139 | .map-preview {
140 | position: fixed;
141 | right: 60%;
142 | }
143 | }
144 |
145 |
146 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var app = express();
3 | var morgan = require('morgan');
4 | var bodyParser = require('body-parser');
5 | var fs = require('fs');
6 | var check = require('./app/setup-check.js');
7 |
8 | var port = process.env.PORT || 3001;
9 |
10 | app.use(morgan('dev'));
11 | app.use(bodyParser.urlencoded({'extended':'true'}));
12 | app.use(bodyParser.json());
13 | app.use(function(req, res, next) {
14 | res.header("Access-Control-Allow-Origin", "*");
15 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
16 | next();
17 | });
18 |
19 | check(function(){
20 | require('./app/routes.js')(app);
21 | app.use(express.static(__dirname + '/public'));
22 |
23 | app.listen(port).on('error', function(err) {
24 | if (err.errno === 'EADDRINUSE') {
25 | throw('Error: Port '+port+' is already in use, which means that Pinpoint is probably already running on this computer.');
26 | }
27 | });
28 | console.log("I'm on port " + port);
29 | });
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------