Initial commit

This commit is contained in:
Paul Hitt 2025-12-08 15:12:52 -05:00
commit 3d4eb2b4a6
212 changed files with 412123 additions and 0 deletions

7
.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"presets": [
"@babel/react",
"@babel/preset-env",
],
"plugins": ["@babel/plugin-proposal-export-default-from"]
}

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
venv

7
.eslintrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
"extends": "airbnb-base",
"plugins": [
"import",
"react",
]
};

131
.gitignore vendored Normal file
View File

@ -0,0 +1,131 @@
venv
db.sqlite3
data/rawdata
node_modules
### Vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### Node ###
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules

25
Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM python:3-alpine3.12
ENV PYTHONUNBUFFERED 1
WORKDIR /app
# Postgres and python deps
RUN apk update && \
apk add --virtual build-deps gcc python-dev musl-dev && \
apk add postgresql-dev curl bash
# Install python deps
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY spacedb spacedb
COPY spaceobjects spaceobjects
COPY static static
COPY templates templates
COPY data data
COPY manage.py manage.py
EXPOSE 8000
CMD python /app/manage.py runserver 0.0.0.0:8000

4
README Normal file
View File

@ -0,0 +1,4 @@
when setting up a new instance, also don't forget to populate
/var/spacedb/manual - manual copy
(/var/spacedb/raw - raw data) -- should persist

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# spacedb
To get started:
```
virtualenv venv
source venv/bin/activate
```
First time:
```
# Python backend
pip install -r requirements.txt
./manage.py migrate
./manage.py loaddata orbit_class
# Webpack frontend
yarn install
```
Now run it:
```
# Terminal 1: run the web server
./manage.py runserver
# Terminal 2: build the js assets continuously
yarn build:watch
```

View File

@ -0,0 +1,55 @@
import React from 'react';
import PropTypes from 'react-proptypes';
import AsyncSelect from 'react-select/lib/Async';
const loadOptions = (inputValue, callback) => {
return new Promise(resolve => {
if (inputValue.length < 3) {
resolve([]);
return;
}
fetch(`/api/objects/search?q=${inputValue}`).then(resp => {
return resp.json()
}).then(respJson => {
resolve(respJson.results.map(result => {
return {
label: result.fullname,
value: result.slug,
};
}));
});
});
};
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue: '',
};
}
handleChange(inputValue) {
this.setState({ inputValue })
window.location.href = `/asteroid/${inputValue.value}`;
}
render() {
return (
<AsyncSelect
cacheOptions
loadOptions={loadOptions}
onChange={this.handleChange.bind(this)}
placeholder="Search for an asteroid or comet..."
className="topnav__react-search__control"
/>
);
}
}
Search.propTypes = {
blah: PropTypes.string,
};
export default Search;

View File

@ -0,0 +1,200 @@
import React from 'react';
import PropTypes from 'react-proptypes';
import AsyncSelect from 'react-select/lib/Async';
import { parseQuery } from '../util';
const INPUT_LENGTH_MIN = 3;
const loadOptions = (inputValue, callback) => {
return new Promise(resolve => {
if (inputValue.length < INPUT_LENGTH_MIN) {
resolve([]);
return;
}
fetch(`/api/objects/search?q=${inputValue}`).then(resp => {
return resp.json()
}).then(respJson => {
resolve(respJson.results.map(result => {
return {
label: result.fullname,
vizLabel: result.name,
value: result.slug,
ephem: result.ephem,
};
}));
});
});
};
class SearchAndVisualize extends React.Component {
constructor(props) {
super(props);
this._objCount = 0;
this.state = {
inputValue: '',
savedDate: undefined,
selectedObjects: [],
};
}
componentDidMount() {
const hash = parseQuery(window.location.hash);
if (hash.ob) {
fetch(`/api/objects?slugs=${hash.ob}`).then(resp => {
return resp.json()
}).then(respJson => {
this.addMany(respJson.results.map(result => {
return {
label: result.fullname,
vizLabel: result.name,
value: result.slug,
ephem: result.ephem,
};
}), {
showLabel: true,
showOrbit: true,
updateHash: true,
});
});
}
if (hash.cat) {
const query = parseQuery(window.location.search);
const limit = query.limit || 3000;
fetch(`/api/category/${hash.cat}?limit=${limit}`).then(resp => {
return resp.json()
}).then(respJson => {
this.addMany(respJson.data.map(result => {
return {
label: result.fullname,
vizLabel: result.name,
value: result.slug,
ephem: result.ephem,
};
}), {
showLabel: false,
showOrbit: false,
updateHash: false,
displayEclipticLines: false,
});
});
}
if (hash.date) {
const dateObj = new Date(Date.parse(`${hash.date}T00:00:00Z`));
window.viz.setDate(dateObj);
this.setState({
savedDate: hash.date,
});
}
}
addMany(objs, opts) {
opts = opts || {};
objs.forEach(obj => {
const key = `spaceobject${this._objCount++}`;
window.viz.createObject(key, Object.assign(window.VIZ_OBJECT_OPTS, {
ephem: new Spacekit.Ephem(obj.ephem, 'deg'),
// Show short name
labelText: opts.showLabel ? obj.vizLabel : undefined,
hideOrbit: !opts.showOrbit,
particleSize: opts.particleSize,
ecliptic: {
displayLines: opts.displayEclipticLines,
},
}));
});
if (opts.updateHash) {
this.setState(prevState => ({ selectedObjects: prevState.selectedObjects.concat(objs) }), () => {
let hashStr = '#ob=' + this.state.selectedObjects.map(object => object.value).join(',');
if (this.state.savedDate) {
hashStr += `&date=${this.state.savedDate}`;
}
window.location.hash = hashStr;
});
}
}
handleChange(inputValue) {
this.addMany([inputValue], {
showLabel: true,
showOrbit: true,
updateHash: true,
});
}
getObjectList() {
return this.state.selectedObjects.map(object => {
return (
<div className="tile" key={`selectedObject-${object.value}`}>
<a target="_blank" href={`/asteroid/${object.value}`}>
<h5>{object.label}</h5>
</a>
</div>
);
});
}
render() {
return (
<div>
<AsyncSelect
cacheOptions
loadOptions={loadOptions}
onChange={this.handleChange.bind(this)}
value={null}
placeholder="Search for an asteroid or comet..."
className="topnav__react-search__control"
noOptionsMessage={() => {
if (this.state.inputValue.length < INPUT_LENGTH_MIN) {
return null;
}
return "No matching objects"
}}
styles={{
input: base => ({
...base,
color: "#fff"
}),
noOptionsMessage: base => ({
...base,
color: "#ccc"
}),
loadingMessage: base => ({
...base,
color: "#ccc"
}),
}}
theme={(theme) => ({
...theme,
borderRadius: 0,
colors: {
...theme.colors,
primary25: '#404040',
primary: 'black',
neutral0: '#1d1d1d',
neutral5: '#000a',
neutral10: '#000b',
neutral20: '#666',
neutral30: '#000d',
neutral40: '#000e',
},
})}
/>
{this.state.selectedObjects.length > 0 ? (
<div className="item-container tile-list">
{this.getObjectList()}
</div>
): null}
</div>
);
}
}
SearchAndVisualize.propTypes = {
blah: PropTypes.string,
};
export default SearchAndVisualize;

13
client/index.js Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Search from './components/Search.jsx';
import SearchAndVisualize from './components/SearchAndVisualize.jsx';
Array.from(document.getElementsByClassName('react-search')).forEach(elt => {
ReactDOM.render(<Search/>, elt);
});
Array.from(document.getElementsByClassName('react-search-and-visualize')).forEach(elt => {
ReactDOM.render(<SearchAndVisualize/>, elt);
});

15
client/util.js Normal file
View File

@ -0,0 +1,15 @@
export function parseQuery(queryString) {
const query = {};
if (!queryString) {
return query;
}
const firstChar = queryString[0];
const cleanedString = firstChar === '?' || firstChar === '#' ?
queryString.substr(1) : queryString;
const pairs = cleanedString.split('&');
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}

View File

@ -0,0 +1,25 @@
upstream appserver {
server app:8000;
}
server {
listen 80;
server_name spacereference.org;
return 301 $scheme://www.spacereference.org$request_uri;
}
server {
listen 80;
server_name localhost www.spacereference.org;
location / {
proxy_pass http://appserver;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /var/www/static/;
}
}

0
data/__init__.py Normal file
View File

24
data/download.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
mkdir -p rawdata
echo 'Downloading SBDB...'
./download_sbdb.sh
echo 'Downloading close approaches...'
./download_close_approaches.sh
echo 'Downloading sentry...'
./download_sentry.sh
echo 'Downloading nhats...'
./download_nhats.sh
echo 'Downloading damit...'
./download_damit.sh
echo 'Done.'
popd &>/dev/null

View File

@ -0,0 +1,7 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
curl 'https://ssd-api.jpl.nasa.gov/cad.api?date-min=2019-01-01&date-max=2200-01-01&dist-max=0.2&fullname=true' -o rawdata/close_approach.json
popd &>/dev/null

20
data/download_damit.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
mkdir -p rawdata/shapes
rm -rf rawdata/shapes/*
cd rawdata/shapes
# Download complete flush from
# http://astro.troja.mff.cuni.cz/projects/asteroids3D/web.php?page=db_export
#curl -o damit.tar.gz 'https://astro.troja.mff.cuni.cz/projects/damit/exports/complete/damit-20210401T000301Z.tar.gz'
curl -o damit.tar.gz 'https://www.ianww.com/damit_old.tar.gz'
# Decompress
tar xzvf damit.tar.gz
# Put everything into one directory instead of breaking it up every thousand
# Use find instead of mv because on mac mv limits the number of arguments.
find archive/ -name '*.*' -exec mv {} archive/. \;
popd &>/dev/null

7
data/download_nhats.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
curl -o rawdata/nhats.json https://ssd-api.jpl.nasa.gov/nhats.api
popd &>/dev/null

8
data/download_sbdb.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
curl -v 'https://ssd-api.jpl.nasa.gov/sbdb_query.api?fields=full_name,pdes,name,class,neo,pha,moid,moid_jup,epoch,e,a,q,i,om,w,ma,tp,per,n,ad,first_obs,last_obs,n_obs_used,H,M1,diameter,density,extent,rot_per,GM,pole,albedo,BV,UB,IR,spec_T,spec_B' -o rawdata/sbdb.json
gzip -f rawdata/sbdb.json
popd &>/dev/null

7
data/download_sentry.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
curl -o rawdata/sentry.json https://ssd-api.jpl.nasa.gov/sentry.api?all=1
popd &>/dev/null

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

296454
data/manual/shapes/bennu.obj Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

12
data/process.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
./process_sbdb.py
./process_close_approach.py
./process_sentry.py
./process_nhats.py
./process_damit.py
./process_manual_shapes.py
popd &>/dev/null

83
data/process_close_approach.py Executable file
View File

@ -0,0 +1,83 @@
#!/usr/bin/env python
import csv
import json
import logging
import os
import sys
from datetime import datetime
import django
from django.db import transaction
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings')
django.setup()
from spaceobjects.models import SpaceObject, CloseApproach
from data.util import get_normalized_full_name
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@transaction.atomic
def insert_all(newobjects, delete=False):
if delete:
CloseApproach.objects.all().only('pk').delete()
CloseApproach.objects.bulk_create(newobjects, batch_size=499)
def process(fields, data):
CloseApproach.objects.all().delete()
newobjects = []
inserted_once = False
for count, row in enumerate(data, 1):
if count % 100 == 0:
logger.info(count)
if count % 20000 == 0:
# Subdivide insertions - slower, but needed for low memory
# environments like production machine
logger.info('Inserting...')
insert_all(newobjects, delete=(not inserted_once))
inserted_once = True
newobjects = []
ca_raw = dict(zip(fields, row))
fullname = get_normalized_full_name(ca_raw['fullname'])
date_str = ca_raw['cd']
date = datetime.strptime(date_str, '%Y-%b-%d %H:%M')
try:
space_object = SpaceObject.objects.get(fullname=fullname)
ca = CloseApproach(
space_object=space_object,
date=date,
dist_au=float(ca_raw['dist']),
dist_min_au=float(ca_raw['dist_min']),
v_rel=float(ca_raw['v_rel']),
# TODO(ian): Make hmag nullable
h_mag=float(ca_raw['h'] if ca_raw['h'] else -99),
)
newobjects.append(ca)
except SpaceObject.DoesNotExist:
logger.error('Cannot find space object %s' % fullname)
logger.info('Inserting final records...')
insert_all(newobjects, delete=(not inserted_once))
logger.info('Done.')
if __name__ == '__main__':
logger.info('Processing close approach data')
dir_path = os.path.dirname(os.path.realpath(__file__))
data_path = os.path.realpath(os.path.join(dir_path, 'rawdata/close_approach.json'))
with open(data_path) as f:
close_approach_file = json.load(f)
process(close_approach_file["fields"],
close_approach_file["data"])

128
data/process_damit.py Executable file
View File

@ -0,0 +1,128 @@
#!/usr/bin/env python
import codecs
import csv
import logging
import os
import sys
import django
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings')
django.setup()
from spaceobjects.models import SpaceObject, ShapeModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def convert_shape_to_obj(inpath, outpath):
outlines = []
with open(inpath, 'r') as f:
count = int(f.readline().split()[0])
for x in range(count):
outlines.append('v %s' % (f.readline()))
line = f.readline()
while line:
outlines.append('f %s' % line)
line = f.readline()
with open(outpath, 'w') as f:
f.write(''.join(outlines))
def process_references(f_in):
reader = csv.DictReader(codecs.EncodedFile(f_in, 'utf8', 'utf-8-sig'), delimiter=',')
ret = {}
for row in reader:
cite = '%s. %s. %s. %s:%s' % (row['Author'], row['Year'], row['Title'], row['Publication'], row['Page'])
ret[row['Reference ID']] = {
'cite': cite,
'url': row['URI'],
}
return ret
def process_shapes(f_in, refs):
reader = csv.DictReader(f_in, delimiter=',')
# Map from fullname to SpaceObject
spaceobject_lookup = {}
matched = 0
count = 0
newobjects = []
for row in reader:
count += 1
reconstructed_desig = '%s %s' % (row['asteroid number'], row['asteroid name'])
desig_with_parens = '(%s)' % row['asteroid designation']
fullname = reconstructed_desig if row['asteroid name'] else desig_with_parens
logger.info('%d: %s' % (count, fullname))
# Locate SpaceObject match
space_object = spaceobject_lookup.get(fullname)
if space_object:
matched += 1
else:
try:
space_object = SpaceObject.objects.get(fullname=fullname)
matched += 1
except SpaceObject.MultipleObjectsReturned:
logger.error('Multiple objects returned (ambiguous query) for %s' % fullname)
continue
except SpaceObject.DoesNotExist:
try:
space_object = SpaceObject.objects.get(fullname__contains=fullname)
matched += 1
except SpaceObject.DoesNotExist:
logger.error('Cannot find space object %s' % fullname)
continue
except SpaceObject.MultipleObjectsReturned:
logger.error('Multiple objects returned (ambiguous query) for %s' % fullname)
continue
spaceobject_lookup[fullname] = space_object
# Create ShapeModel object
ast_id = row['asteroid id']
model_id = row['model id']
filename = 'A%s.M%s' % (ast_id, model_id)
convert_shape_to_obj('./rawdata/shapes/archive/%s.shape.txt' % filename,
'./rawdata/shapes/archive/%s.obj' % filename)
shape_path = '/data/shapefiles/damit/%s.obj' % filename
render_path = '/data/shapefiles/damit/%s.shape.png' % filename
yorp = float(row['yorp']) if row['yorp'] else None
quality = float(row['quality flag']) if row['quality flag'] else -1
diam = float(row['equivalent diameter']) if row['equivalent diameter'] else None
#reference = refs[row['reference id']]
newobjects.append(ShapeModel(space_object=space_object,
shape_path=shape_path,
render_path=render_path,
spin_latitude=float(row['beta']),
spin_longitude=float(row['beta']),
spin_angle=float(row['phi0']),
period_hr=float(row['period']),
jd=float(row['jd0']),
yorp=yorp,
equiv_diameter_km=diam,
quality=quality,
source='damit',
#reference=reference,
))
logger.info('%d/%d objects matched' % (matched, count))
logger.info('Creating...')
ShapeModel.objects.all().delete()
ShapeModel.objects.bulk_create(newobjects)
logger.info('Done.')
if __name__ == '__main__':
with open('rawdata/shapes/damit_flush_extended.csv', 'r', encoding='utf-8-sig') as f_main, \
open('rawdata/shapes/damit_flush_refs.csv', 'r') as f_refs:
#refs = process_references(f_refs)
refs = {}
process_shapes(f_main, refs)

93
data/process_manual_shapes.py Executable file
View File

@ -0,0 +1,93 @@
#!/usr/bin/env python
import codecs
import csv
import logging
import os
import sys
import django
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings')
django.setup()
from spaceobjects.models import SpaceObject, ShapeModel
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
SHAPES = [
{
'name': '486958 Arrokoth (2014 MU69)',
'shape_path': '2014_mu69.obj',
'render_path': '2014_mu69.png',
# https://science.sciencemag.org/content/364/6441/eaaw9771?intcmp=trendmd-sci
'source': 'S. A. Stern et al., Science, 2019',
},
{
'name': '101955 Bennu (1999 RQ36)',
'shape_path': 'bennu.obj',
'render_path': 'bennu.gif',
# https://www.asteroidmission.org/updated-bennu-shape-model-3d-files/
'source': 'NASA/Goddard/University of Arizona',
},
# http://observations.lam.fr/astero/
{
'name': '2 Pallas',
'shape_path': 'Pallas_ADAM.obj',
'render_path': 'pallas.png',
# https://www.nature.com/articles/s41550-019-1007-5
'source': 'Marsset et al., Nature Astronomy, 2020',
},
]
def process_shapes():
# Map from fullname to SpaceObject
spaceobject_lookup = {}
newobjects = []
matched = 0
for count, shape in enumerate(SHAPES):
fullname = shape['name']
logger.info('%d: %s' % (count, fullname))
# Locate SpaceObject match
space_object = spaceobject_lookup.get(fullname)
if space_object:
matched += 1
else:
try:
space_object = SpaceObject.objects.get(fullname=fullname)
matched += 1
except SpaceObject.DoesNotExist:
try:
space_object = SpaceObject.objects.get(fullname__contains=fullname)
matched += 1
except SpaceObject.DoesNotExist:
logger.error('Cannot find space object %s' % fullname)
continue
spaceobject_lookup[fullname] = space_object
# Create ShapeModel object
shape_path = '/data/shapefiles/manual/%s' % shape['shape_path']
render_path = '/data/shapefiles/manual/%s' % shape['render_path']
newobjects.append(ShapeModel(space_object=space_object,
shape_path=shape_path,
render_path=render_path,
source=shape['source'],
))
logger.info('%d/%d objects matched' % (matched, count))
logger.info('Creating...')
#ShapeModel.objects.all().delete()
ShapeModel.objects.bulk_create(newobjects)
logger.info('Done.')
if __name__ == '__main__':
process_shapes()

65
data/process_nhats.py Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env python
# TODO(ian): Process detail for each object, including orbital plot
# TODO(ian): Orbit condition codes (occ) https://minorplanetcenter.net/iau/info/UValue.html
import csv
import django
import json
import logging
import os
import sys
from datetime import datetime
from django.db import transaction
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings')
django.setup()
from spaceobjects.models import NhatsObject, SpaceObject
from data.util import get_normalized_full_name
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def process(entries):
newobjects = []
count = 0
for entry in entries:
count += 1
if count % 500 == 0:
logger.info(count)
fullname = get_normalized_full_name(entry['fullname'])
try:
space_object = SpaceObject.objects.get(fullname=fullname)
newobj = NhatsObject(
space_object=space_object,
min_dv=entry['min_dv']['dv'],
min_dv_duration=entry['min_dv']['dur'],
min_diameter=entry['min_size'],
max_diameter=entry['max_size'],
num_trajectories=entry['n_via_traj'],
)
newobjects.append(newobj)
except SpaceObject.DoesNotExist:
logger.error('Cannot find space object %s' % fullname)
logger.info('Inserting records...')
NhatsObject.objects.all().delete()
NhatsObject.objects.bulk_create(newobjects, batch_size=499)
logger.info('%d records inserted' % len(newobjects))
if __name__ == '__main__':
logger.info('Processing sentry data')
dir_path = os.path.dirname(os.path.realpath(__file__))
data_path = os.path.realpath(os.path.join(dir_path, 'rawdata/nhats.json'))
with open(data_path) as f:
result = json.load(f)
process(result['data'])

127
data/process_sbdb.py Executable file
View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
import csv
import gzip
import json
import logging
import os
import sys
import django
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.utils.text import slugify
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings_pipeline')
django.setup()
from spaceobjects.models import SpaceObject, OrbitClass, ObjectType
from data.util import get_normalized_full_name, queryset_iterator
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@transaction.atomic
def insert_all(newobjects, delete=False):
if delete:
logger.info('Deleting...')
for obj in SpaceObject.objects.all().only('pk').iterator():
obj.delete()
logger.info('Finished deleting.')
SpaceObject.objects.bulk_create(newobjects, batch_size=499)
def process(reader):
newobjects = []
inserted_once = False
failures_ma = 0
for count, row in enumerate(reader, 1):
if count % 1000 == 0:
logger.info(count)
if count % 1000 == 0:
# Subdivide insertions - slower, but needed for low memory
# environments like production machine
logger.info('Inserting...')
insert_all(newobjects, delete=(not inserted_once))
inserted_once = True
newobjects = []
if not row['ma']:
logger.warn('Missing mean anom: Failed to parse row %d (%s): %s' % \
(count, row.get('full_name', '?'), json.dumps(row)))
failures_ma += 1
continue
fullname = get_normalized_full_name(row['full_name'])
try:
orbit_class = OrbitClass.objects.get(abbrev__iexact=row['class'])
except ObjectDoesNotExist:
orbit_class = None
object_type = ObjectType.from_class(row['class'])
if row['name'] and object_type != ObjectType.COMET:
shortname = row['name'].strip()
else:
shortname = fullname
magnitude = float(row['H']) if row['H'] else None
if not magnitude:
# Comet total magnitude
magnitude = float(row['M1']) if row['M1'] else None
try:
diam = float(row['diameter'].strip())
except:
diam = None
space_object = SpaceObject(
fullname = fullname,
name = shortname,
slug = slugify(fullname.replace('/',' ')),
a = float(row['a']),
e = float(row['e']),
i = float(row['i']),
om = float(row['om']),
w = float(row['w']),
ma = float(row['ma']),
epoch = float(row['epoch']),
is_nea = True if row['neo'] == 'Y' else False,
is_pha = True if row['pha'] == 'Y' else False,
orbit_class = orbit_class,
object_type = object_type,
diameter = diam,
spec_B = row['spec_B'],
spec_T = row['spec_T'],
H = magnitude,
sbdb_entry = row,
)
newobjects.append(space_object)
logger.info('Inserting final records...')
insert_all(newobjects, delete=(not inserted_once))
logger.warning('%d blank mean anomalies' % failures_ma)
logger.info('Done.')
def generate_rows(fields, data):
for row in data:
yield dict(zip(fields, row))
if __name__ == '__main__':
logger.info('Loading sbdb data...')
dir_path = os.path.dirname(os.path.realpath(__file__))
data_path = os.path.realpath(os.path.join(dir_path, 'rawdata/sbdb.json.gz'))
with gzip.open(data_path) as f:
obj = json.load(f)
logger.info('Loaded sbdb data, processing...')
fields = obj['fields']
data = obj['data']
rows = generate_rows(fields, data)
process(rows)

75
data/process_sentry.py Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
import csv
import django
import json
import logging
import os
import sys
from datetime import datetime
from django.db import transaction
current_dir = os.path.dirname(__file__)
parent_dir = os.path.join(current_dir, '../')
sys.path.insert(0, os.path.realpath(parent_dir))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'spacedb.settings')
django.setup()
from spaceobjects.models import SentryEvent, SpaceObject
from data.util import get_normalized_full_name
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def process(events):
newobjects = []
count = 0
invalid_count = 0
for event in events:
count += 1
if count % 2000 == 0:
logger.info(count)
fullname = get_normalized_full_name(event['fullname'])
date_str = event['date']
try:
date = datetime.strptime(date_str[:date_str.find('.')], '%Y-%m-%d')
except:
invalid_count += 1
continue
try:
space_object = SpaceObject.objects.get(fullname=fullname)
except SpaceObject.DoesNotExist:
try:
space_object = SpaceObject.objects.get(fullname=fullname.replace('(', '').replace(')', ''))
except SpaceObject.DoesNotExist:
logger.error('Cannot find space object %s' % fullname)
se = SentryEvent(
space_object=space_object,
date=date,
energy_mt=float(event['energy']),
prob=float(event['ip']),
torino_scale=float(event['ts']) if event['ts'] else -1,
palermo_scale=float(event['ps']),
)
newobjects.append(se)
logger.warn('%d invalid dates' % invalid_count)
logger.info('Inserting records...')
SentryEvent.objects.all().delete()
SentryEvent.objects.bulk_create(newobjects, batch_size=499)
logger.info('%d records inserted' % len(newobjects))
if __name__ == '__main__':
logger.info('Processing sentry data')
dir_path = os.path.dirname(os.path.realpath(__file__))
data_path = os.path.realpath(os.path.join(dir_path, 'rawdata/sentry.json'))
with open(data_path) as f:
result = json.load(f)
process(result['data'])

9
data/run_pipeline.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
./download.sh
./process.sh
popd &>/dev/null

25
data/util.py Normal file
View File

@ -0,0 +1,25 @@
import gc
from spaceobjects.models import ObjectType
def get_normalized_full_name(raw):
fullname = raw.strip()
if fullname[0] == '(' and fullname[-1] == ')':
return fullname[1:-1]
return fullname
def queryset_iterator(qs, batchsize = 500, gc_collect = True):
iterator = qs.values_list('pk', flat=True).order_by('pk').distinct().iterator()
eof = False
while not eof:
primary_key_buffer = []
try:
while len(primary_key_buffer) < batchsize:
primary_key_buffer.append(iterator.next())
except StopIteration:
eof = True
for obj in qs.filter(pk__in=primary_key_buffer).order_by('pk').iterator():
yield obj
if gc_collect:
gc.collect()

46
docker-compose.yml Normal file
View File

@ -0,0 +1,46 @@
version: '3.7'
services:
db:
image: postgres:10.1-alpine
volumes:
- /var/spacedb/postgres/data:/var/lib/postgresql/data/
networks:
- db_network
app:
build: .
command: gunicorn --access-logfile - --error-logfile - --capture-output --log-level info -b '0.0.0.0:8000' -w 5 spacedb.wsgi
environment:
- DJANGO_SETTINGS_MODULE=spacedb.settings_prod
volumes:
- ./static:/app/static
- /var/spacedb/raw:/app/data/rawdata
- /var/spacedb/manual:/app/data/manual
ports:
- 8000:8000
networks:
- nginx_network
- db_network
depends_on:
- db
nginx:
image: nginx:1.13
volumes:
- ./config/nginx/conf.d:/etc/nginx/conf.d
- ./static:/var/www/static
- /var/spacedb/raw/shapes/archive:/var/www/static/data/shapefiles/damit
- /var/spacedb/manual/shapes:/var/www/static/data/shapefiles/manual
ports:
- 80:80
networks:
- nginx_network
depends_on:
- app
networks:
nginx_network:
driver: bridge
db_network:
driver: bridge

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spacedb.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

35
package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "spacedb",
"version": "0.0.1",
"main": "client/index.js",
"repository": "git@github.com:judymou/spacedb.git",
"author": "Ian and Judy",
"license": "N/A",
"scripts": {
"build": "webpack",
"build:watch": "webpack --watch",
"lint": "eslint ./client",
"lint:fix": "eslint --fix ./client"
},
"dependencies": {
"prop-types": "^15.7.2",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-proptypes": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-export-default-from": "^7.2.0",
"@babel/preset-env": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.4",
"eslint": "^5.10.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-react": "^7.11.1",
"react-select": "^2.1.2",
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2"
}
}

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
Django==1.11.29
jsonfield==1.0.3
gunicorn==19.9.0
psycopg2==2.7.6.1
PyYAML==5.4
enum34==1.1.6
setuptools==65.5.1

View File

@ -0,0 +1,11 @@
#!/bin/bash
# Run the initial migrations, creating the database
docker-compose run --rm app /bin/sh -c "cd /app; ./manage.py migrate"
docker-compose run --rm app /bin/sh -c "cd /app; ./manage.py loaddata orbit_class"
# Run the pipeline
docker-compose run --rm app /bin/bash -c "cd /app; ./data/run_pipeline.sh"
# Create an admin login
docker-compose run --rm app /bin/sh -c "cd /app; ./manage.py createsuperuser"

13
scripts/prod_update.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash -e
pushd $(dirname $0) &>/dev/null
cd ..
git pull
docker-compose build
docker-compose down
docker-compose up -d
docker system prune -af
popd &>/dev/null

4
scripts/run_migration.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Run new migrations
docker-compose run --rm app /bin/sh -c "cd /app && ./manage.py migrate && ./manage.py loaddata orbit_class"

4
scripts/run_pipeline.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Run the pipeline
time docker-compose run --rm app /bin/bash -c "cd /app; ./data/run_pipeline.sh"

0
spacedb/__init__.py Normal file
View File

130
spacedb/settings.py Normal file
View File

@ -0,0 +1,130 @@
"""
Django settings for spacedb project.
Generated by 'django-admin startproject' using Django 1.11.16.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!f-34x!n5^0az@59f=$(hjbk$-x!z8dkb&e1#h8=6w)q3j$r%*'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django.contrib.sitemaps',
'spaceobjects',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'spacedb.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'spacedb.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static/')
]
# TODO(ian): Uncomment this and comment the above before running collectstatic.
#STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

View File

@ -0,0 +1,126 @@
"""
Django settings for spacedb project.
Generated by 'django-admin startproject' using Django 1.11.16.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!f-34x!n5^0az@59f=$(hjbk$-x!z8dkb&e1#h8=6w)q3j$r%*'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'spaceobjects',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'spacedb.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'spacedb.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]

130
spacedb/settings_prod.py Normal file
View File

@ -0,0 +1,130 @@
"""
Django settings for spacedb project.
Generated by 'django-admin startproject' using Django 1.11.16.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!f-34x!n5^0az@59f=$(hjbk$-x!z8dkb&e1#h8=6w)q3j$r%*'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = ['www.spacereference.org', 'localhost']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django.contrib.sitemaps',
'spaceobjects',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'spacedb.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'spacedb.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': '',
'HOST': 'db',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]

22
spacedb/urls.py Normal file
View File

@ -0,0 +1,22 @@
"""spacedb URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url, include
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'', include('spaceobjects.urls')),
]

16
spacedb/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for spacedb project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "spacedb.settings")
application = get_wsgi_application()

0
spaceobjects/__init__.py Normal file
View File

6
spaceobjects/admin.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
# Register your models here.

8
spaceobjects/apps.py Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import AppConfig
class SpaceobjectsConfig(AppConfig):
name = 'spaceobjects'

315
spaceobjects/description.py Normal file
View File

@ -0,0 +1,315 @@
'''
Helpers for building an asteroid description.
'''
COMET_CLASSES = set(['COM', 'CTc', 'ETc', 'HTC', 'HYP', 'JFc', 'JFC', 'PAR'])
def get_diameter_comparison(roid):
diameter = roid.get_diameter_estimate()
if not diameter:
return None
diameter_sq = diameter * diameter
# http://www.decisionsciencenews.com/2015/02/20/put-size-countries-perspective-comparing-us-states/
# https://en.wikipedia.org/wiki/List_of_United_States_cities_by_area
if diameter_sq < 0.018:
return 'a school bus or smaller'
if diameter_sq < 0.029:
return 'a basketball court'
if diameter_sq < 0.110:
return 'a football field'
if diameter_sq < 0.0728434:
return 'the U.S. White House'
if diameter_sq < 0.234718:
return 'the U.S. Capitol building'
if diameter_sq < 1.280:
return 'the Golden Gate Bridge'
if diameter_sq < 2.35932:
return 'the U.S. Pentagon'
if diameter_sq < 8.848:
return 'Mount Everest'
if diameter_sq < 21:
return 'the island of Manhattan'
if diameter_sq < 97:
return 'the San Francisco Bay'
if diameter_sq < 124:
return 'the city of Boston'
if diameter_sq < 214:
return 'the city of Cleveland, Ohio'
if diameter_sq < 239:
return 'the city of Baltimore'
if diameter_sq < 370:
return 'the city of Philadelphia'
if diameter_sq < 400:
return 'the city of Denver'
if diameter_sq < 953:
return 'the city of Indianapolis'
if diameter_sq < 999:
return 'the city of Dallas'
if diameter_sq < 1213:
return 'the city of New York'
if diameter_sq < 1302:
return 'the city of Los Angeles'
if diameter_sq < 1625:
return 'the city of Houston'
if diameter_sq < 5000:
return 'the U.S. state of Rhode Island'
if diameter_sq < 14000:
return 'the U.S. state of Delaware'
if diameter_sq < 22000:
return 'the U.S. state of Connecticut'
if diameter_sq < 24000:
return 'the U.S. state of New Jersey'
if diameter_sq < 27000:
return 'the U.S. state of Vermont'
if diameter_sq < 32000:
return 'the U.S. state of Massachusetts'
if diameter_sq < 62000:
return 'the U.S. state of Maryland'
if diameter_sq < 82000:
return 'the U.S. state of West Virginia'
if diameter_sq < 91000:
return 'the U.S. state of South Carolina'
if diameter_sq < 94000:
return 'Portugal'
if diameter_sq < 104000:
return 'South Korea'
if diameter_sq < 109000:
return 'Iceland'
if diameter_sq < 119000:
return 'the U.S. state of Virginia'
if diameter_sq < 125000:
return 'the U.S. state of Pennsylvania'
if diameter_sq < 134000:
return 'the U.S. state of Mississippi'
if diameter_sq < 170000:
return 'the U.S. state of Iowa'
if diameter_sq < 200000:
return 'the U.S. state of South Dakota'
if diameter_sq < 300000:
return 'Great Britain'
if diameter_sq < 400000:
return 'Japan'
if diameter_sq < 500000:
return 'France'
if diameter_sq < 700000:
return 'the U.S. state of Texas'
return 'the U.S. state of Alaska'
# Approximate mapping from Tholen spectral type to SMASS, from Asterank
# https://github.com/typpo/asterank/blob/master/data/pipeline/run/10_sbdb/horizon.py
THOLEN_MAPPINGS = {
'M': 'M',
'E': 'M',
'P': 'P',
'B': 'B',
'C': 'C',
'F': 'C',
'G': 'Cgh',
'Q': 'Q',
'R': 'R',
'V': 'V',
'T': 'T',
'D': 'D',
'A': 'A',
}
# Keys are asteroid spectra type. Values are maps from a material
# to the percent mass of each material.
SPECTRA_INDEX = {
'?': {},
'A': {},
'B': {
'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
'iron': 10,
},
'C': {# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .2,
'iron': .166,
'nickel': .014,
'cobalt': .002,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'Ch': {# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .2,
'iron': .166,
'nickel': .014,
'cobalt': .002,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'Cg': {# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .2,
'iron': .166,
'nickel': .014,
'cobalt': .002,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'Cgh': {# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .2,
'iron': .166,
'nickel': .014,
'cobalt': .002,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'C type': {# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .2,
'iron': .166,
'nickel': .014,
'cobalt': .002,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'Cb': {# transition object between C and B# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .1,
'iron': .083,
'nickel': .007,
'cobalt': .001,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'D': {
'water': 0.000023,
},
'E': {
},
'K': {# cross between S and C# from Keck report at http: //www.kiss.caltech.edu/study/asteroid/asteroid_final_report.pdf
'water': .1,
'iron': .083,
'nickel': .007,
'cobalt': .001,
#volatiles 'hydrogen': 0.235,
'nitrogen': 0.001,
'ammonia': 0.001,
},
'L': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
'aluminum': 7
},
'Ld': {# copied from S
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'M': {
'iron': 88,
'nickel': 10,
'cobalt': 0.5,
},
'O': {
'nickel-iron': 2.965,
'platinum': 1.25,
},
'P': {# correspond to CI, CM carbonaceous chondrites
'water': 12.5,
},
'R': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'S': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
#Sa, Sq, Sr, Sk, and Sl all transition objects(assume half / half)
'Sa': {
'magnesium silicate': 5e-31,
'iron silicate': 0,
},
'Sq': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'Sr': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'Sk': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'Sl': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'S(IV)': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'Q': {
'nickel-iron': 13.315,
},
'R': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
'T': {
'iron': 6,
},
'U': {
},
'V': {
'magnesium silicate': 1e-30,
'iron silicate': 0,
},
#TODO use density to decide what kind of X the object is ?
'X' : {# TODO these vals only apply to M - type within X
'iron': 88,
'nickel': 10,
'cobalt': 0.5,
},
'Xe': {# TODO these vals only apply to M - type within X
'iron': 88,
'nickel': 10,
'cobalt': 0.5,
},
'Xc': {# TODO these vals only apply to M - type within X
'iron': 88,
'nickel': 10,
'cobalt': 0.5,
'platinum': 0.005,
},
'Xk': {# TODO these vals only apply to M - type within X
'iron': 88,
'nickel': 10,
'cobalt': 0.5,
},
'comet': {# no estimates for now, because assumed mass, etc.would be off
},
}
def composition(roid):
ret = []
spec = roid.sbdb_entry.get('spec_B')
if not spec:
# Try to convert Tholen to SMASS spectral classification
spec = roid.sbdb_entry.get('spec_T')
if not spec:
return []
spec = THOLEN_MAPPINGS.get(spec)
if not spec:
return []
return SPECTRA_INDEX.get(spec, {}).keys()

View File

@ -0,0 +1,190 @@
# https://pdssbn.astro.umd.edu/data_other/objclass.shtml
- model: spaceobjects.OrbitClass
pk: 1
fields:
abbrev: COM
name: Unclassified Comet
slug: unclassified-comets
desc: Comets whose orbits do not match any defined orbit class
orbit_sentence: whose orbit does not match any defined comet orbit class
- model: spaceobjects.OrbitClass
pk: 2
fields:
abbrev: CTc
name: Chiron-type Comet
slug: chiron-type-comets
desc: Chiron-type comet, as defined by Levison and Duncan (TJupiter > 3; a > aJupiter)
orbit_sentence: whose orbit is approximately between Jupiter and Neptune
- model: spaceobjects.OrbitClass
pk: 3
fields:
abbrev: ETc
name: Encke-type Comet
slug: encke-type-comets
desc: Encke-type comet, as defined by Levison and Duncan (TJupiter > 3; a < aJupiter)
orbit_sentence: whose orbit brings it closer to the sun than Jupiter
- model: spaceobjects.OrbitClass
pk: 4
fields:
abbrev: HTC
name: Halley-type Comet
slug: halley-type-comets
desc: Halley-type comet, classical definition (20 y < P < 200 y)
orbit_sentence: with a medium-length orbit that is highly inclined to the ecliptic plane of the solar system
- model: spaceobjects.OrbitClass
pk: 5
fields:
abbrev: HYP
name: Hyperbolic Comet
slug: hyperbolic-comets
desc: Comets on hyperbolic orbits (e > 1.0)
orbit_sentence: with a trajectory through the solar system likely originating from the Oort Cloud
- model: spaceobjects.OrbitClass
pk: 6
fields:
abbrev: JFc
name: Jupiter-family Comet
slug: jupiter-family-comets
desc: Jupiter-family comets, as defined by Levison and Duncan (2 < TJupiter < 3)
orbit_sentence: whose orbit features a relatively short period, low inclination, and is controlled by Jupiter's gravitational effects
- model: spaceobjects.OrbitClass
pk: 8
fields:
abbrev: PAR
name: Parabolic Comet
slug: parabolic-comets
desc: Comets on parabolic orbits (e = 1.0)
orbit_sentence: with an extremely long orbital period, likely originating from the Oort Cloud
- model: spaceobjects.OrbitClass
pk: 9
fields:
abbrev: AMO
name: Amor-class Asteroid
slug: amor-class-asteroids
desc: Near-Earth asteroid whose orbits are similar to that of 1221 Amor (a > 1.0 AU; 1.017 AU < q < 1.3 AU)
orbit_sentence: whose orbit approaches the orbit of Earth but does not cross it
- model: spaceobjects.OrbitClass
pk: 10
fields:
abbrev: APO
name: Apollo-class Asteroid
slug: apollo-class-asteroids
desc: Near-Earth asteroids whose orbits cross the Earth's orbit similar to that of 1862 Apollo (a > 1.0 AU; q < 1.017 AU).
orbit_sentence: whose orbit crosses the orbit of Earth
- model: spaceobjects.OrbitClass
pk: 11
fields:
abbrev: AST
name: Asteroid
slug: asteroids
desc: Asteroid orbit not matching any defined orbit class
orbit_sentence: whose orbit does not match any defined asteroid orbital class
- model: spaceobjects.OrbitClass
pk: 12
fields:
abbrev: ATE
name: Aten-class Asteroid
slug: aten-class-asteroids
desc: Near-Earth asteroid orbits similar to that of 2062 Aten (a < 1.0 AU; Q > 0.983 AU)
orbit_sentence: whose orbit could bring it in close proximity to Earth
- model: spaceobjects.OrbitClass
pk: 13
fields:
abbrev: CEN
name: Centaur-class Asteroid
slug: centaur-class-asteroids
desc: Objects with orbits between Jupiter and Neptune (5.5 AU < a < 30.1 AU)
orbit_sentence: with an orbit between Jupiter and Neptune
- model: spaceobjects.OrbitClass
pk: 14
fields:
abbrev: HYA
name: Hyperbolic Asteroid
slug: hyperbolic-asteroids
desc: Asteroids on hyperbolic orbits (e > 1.0)
orbit_sentence: with an orbit not bound to the sun
- model: spaceobjects.OrbitClass
pk: 15
fields:
abbrev: IEO
name: Interior-Earth Asteroid
slug: interior-earth-asteroids
desc: Asteroids with orbits contained entirely within the orbit of the Earth (Q < 0.983 AU)
orbit_sentence: with an orbit that is entirely confined within Earth's orbit
- model: spaceobjects.OrbitClass
pk: 16
fields:
abbrev: IMB
name: Inner Main-belt Asteroid
slug: inner-main-belt-asteroids
desc: Asteroids with orbital elements constrained by (a < 2.0 AU; q > 1.666 AU)
orbit_sentence: orbiting between Mars and Jupiter within the inner portion of the asteroid belt
- model: spaceobjects.OrbitClass
pk: 17
fields:
abbrev: MBA
name: Main-belt Asteroid
slug: main-belt-asteroids
desc: Asteroids with orbital elements constrained by (2.0 AU < a < 3.2 AU; q > 1.666 AU)
orbit_sentence: orbiting between Mars and Jupiter in the main portion of the asteroid belt
- model: spaceobjects.OrbitClass
pk: 18
fields:
abbrev: MCA
name: Mars-crossing Asteroid
slug: mars-crossing-asteroids
desc: Asteroids that cross the orbit of Mars constrained by (1.3 AU < q < 1.666 AU; a < 3.2 AU)
orbit_sentence: with an orbit that crosses the orbit of Mars
- model: spaceobjects.OrbitClass
pk: 19
fields:
abbrev: OMB
name: Outer Main-belt Asteroid
slug: outer-main-belt-asteroids
desc: Asteroids with orbital elements constrained by (3.2 AU < a < 4.6 AU)
orbit_sentence: that orbits between Mars and Jupiter in the outer reaches of the main asteroid belt
- model: spaceobjects.OrbitClass
pk: 20
fields:
abbrev: PAA
name: Parabolic Asteroid
slug: parabolic-asteroids
desc: Asteroids on parabolic orbits (e = 1.0)
orbit_sentence: with an unusual orbit that brings it far beyond the normal boundaries of asteroids in the solar system
- model: spaceobjects.OrbitClass
pk: 21
fields:
abbrev: TJN
name: Jupiter Trojan
slug: jupiter-trojans
desc: Asteroids trapped in Jupiter's L4/L5 Lagrange points (4.6 AU < a < 5.5 AU; e < 0.3)
orbit_sentence: that shares Jupiter's orbit around the sun
- model: spaceobjects.OrbitClass
pk: 22
fields:
abbrev: TNO
name: Trans-Neptunian Object
slug: trans-neptunian-objects
desc: Objects with orbits outside Neptune (a > 30.1 AU)
orbit_sentence: whose orbit extends beyond the orbit of Neptune

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-02 07:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SpaceObject',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=500)),
],
),
]

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-08 23:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CloseApproach',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('dist_min', models.FloatField()),
],
),
migrations.RenameField(
model_name='spaceobject',
old_name='name',
new_name='fullname',
),
migrations.AddField(
model_name='closeapproach',
name='space_object',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.SpaceObject'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-08 23:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0002_auto_20181208_2313'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='name',
field=models.CharField(default=1, max_length=500),
preserve_default=False,
),
]

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-09 22:25
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0003_spaceobject_name'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='a',
field=models.FloatField(default=2),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='e',
field=models.FloatField(default=2),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='epoch',
field=models.FloatField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='i',
field=models.FloatField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='ma',
field=models.FloatField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='om',
field=models.FloatField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='w',
field=models.FloatField(default=1),
preserve_default=False,
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-13 01:59
from __future__ import unicode_literals
from django.db import migrations
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0004_auto_20181209_2225'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='sbdb_entry',
field=jsonfield.fields.JSONField(default={}),
preserve_default=False,
),
]

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-14 23:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0005_spaceobject_sbdb_entry'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='slug',
field=models.CharField(default='', max_length=200),
preserve_default=False,
),
migrations.AlterField(
model_name='spaceobject',
name='fullname',
field=models.CharField(max_length=200),
),
migrations.AlterField(
model_name='spaceobject',
name='name',
field=models.CharField(max_length=200),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-16 21:16
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0006_auto_20181214_2300'),
]
operations = [
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['fullname'], name='spaceobject_fullnam_51b141_idx'),
),
]

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-16 21:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0007_auto_20181216_2116'),
]
operations = [
migrations.AddField(
model_name='closeapproach',
name='dist',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='closeapproach',
name='h_mag',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='closeapproach',
name='time_jd',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='closeapproach',
name='v_rel',
field=models.FloatField(default=0),
preserve_default=False,
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-17 02:52
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0008_auto_20181216_2135'),
]
operations = [
migrations.CreateModel(
name='SentryEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('energy_mt', models.FloatField()),
('dist_km', models.FloatField()),
('dist_err', models.FloatField()),
('palermo_scale', models.FloatField()),
('torino_scale', models.FloatField()),
('prob', models.FloatField()),
('space_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.SpaceObject')),
],
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-17 05:56
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0009_sentryevent'),
]
operations = [
migrations.RemoveField(
model_name='closeapproach',
name='time_jd',
),
migrations.AddField(
model_name='closeapproach',
name='date',
field=models.DateField(default=datetime.datetime(2018, 12, 17, 5, 56, 9, 89760)),
preserve_default=False,
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-17 06:15
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0010_auto_20181217_0556'),
]
operations = [
migrations.RenameField(
model_name='closeapproach',
old_name='dist',
new_name='dist_au',
),
migrations.RenameField(
model_name='closeapproach',
old_name='dist_min',
new_name='dist_min_au',
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-21 15:12
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0011_auto_20181217_0615'),
]
operations = [
migrations.CreateModel(
name='NhatsObject',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('min_dv', models.FloatField()),
('min_dv_duration', models.FloatField()),
('min_diameter', models.FloatField()),
('max_diameter', models.FloatField()),
('space_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.SpaceObject')),
],
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-21 15:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0012_nhatsobject'),
]
operations = [
migrations.AddField(
model_name='nhatsobject',
name='num_trajectories',
field=models.IntegerField(default=0),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2018-12-27 17:30
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0013_nhatsobject_num_trajectories'),
]
operations = [
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['slug'], name='spaceobject_slug_f1242a_idx'),
),
]

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 07:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0014_auto_20181227_1730'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='H',
field=models.FloatField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='diameter',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='spaceobject',
name='neo',
field=models.BooleanField(default=True),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='orbit_class',
field=models.CharField(default='A', max_length=200),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='pha',
field=models.BooleanField(default=True),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='spec_B',
field=models.CharField(default='C', max_length=200),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='spec_T',
field=models.CharField(default='C', max_length=200),
preserve_default=False,
),
]

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 17:16
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0015_auto_20190105_0740'),
]
operations = [
migrations.RemoveField(
model_name='spaceobject',
name='neo',
),
migrations.RemoveField(
model_name='spaceobject',
name='pha',
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 17:27
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0016_auto_20190105_1716'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='is_neo',
field=models.BooleanField(default=False),
preserve_default=False,
),
migrations.AddField(
model_name='spaceobject',
name='is_pha',
field=models.BooleanField(default=False),
preserve_default=False,
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 17:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0017_auto_20190105_1727'),
]
operations = [
migrations.CreateModel(
name='OrbitClass',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('slug', models.CharField(max_length=200, unique=True)),
('abbrev', models.CharField(max_length=10)),
('desc', models.CharField(max_length=500)),
('orbit_sentence', models.CharField(max_length=500)),
],
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 18:18
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0018_orbitclass'),
]
operations = [
migrations.AlterField(
model_name='spaceobject',
name='orbit_class',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.OrbitClass'),
),
]

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-05 18:20
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0019_auto_20190105_1818'),
]
operations = [
migrations.AddIndex(
model_name='orbitclass',
index=models.Index(fields=['slug'], name='spaceobject_slug_610d6e_idx'),
),
migrations.AddIndex(
model_name='orbitclass',
index=models.Index(fields=['abbrev'], name='spaceobject_abbrev_697070_idx'),
),
]

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-29 03:44
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0020_auto_20190105_1820'),
]
operations = [
migrations.CreateModel(
name='ShapeModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('shape_path', models.CharField(max_length=200)),
('render_path', models.CharField(max_length=200)),
('spin_latitude', models.FloatField()),
('spin_longitude', models.FloatField()),
('spin_angle', models.FloatField()),
('period_hr', models.FloatField()),
('jd', models.FloatField()),
('yorp', models.FloatField()),
('equiv_diameter_km', models.FloatField()),
('quality', models.IntegerField()),
('space_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.SpaceObject')),
],
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-29 03:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0021_shapemodel'),
]
operations = [
migrations.AlterField(
model_name='shapemodel',
name='yorp',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-29 03:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0022_auto_20190129_0346'),
]
operations = [
migrations.AlterField(
model_name='shapemodel',
name='equiv_diameter_km',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-01-29 03:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0023_auto_20190129_0347'),
]
operations = [
migrations.AlterField(
model_name='shapemodel',
name='quality',
field=models.FloatField(),
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-02 03:28
from __future__ import unicode_literals
from django.db import migrations, models
import spaceobjects.models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0024_auto_20190129_0347'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='object_type',
field=models.CharField(choices=[(spaceobjects.models.ObjectType('ASTEROID'), 'ASTEROID'), (spaceobjects.models.ObjectType('COMET'), 'COMET')], default='ASTEROID', max_length=20),
preserve_default=False,
),
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['orbit_class'], name='spaceobject_orbit_c_56d89c_idx'),
),
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['object_type'], name='spaceobject_object__a115d6_idx'),
),
]

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-23 19:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0025_auto_20190202_0328'),
]
operations = [
migrations.RenameField(
model_name='spaceobject',
old_name='is_neo',
new_name='is_nea',
),
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['is_nea'], name='spaceobject_is_nea_27a9a3_idx'),
),
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['is_pha'], name='spaceobject_is_pha_3d9700_idx'),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-24 19:40
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0026_auto_20190223_1913'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='sbdb_order_id',
field=models.IntegerField(default=-1),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-24 19:45
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0027_spaceobject_sbdb_order_id'),
]
operations = [
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['sbdb_order_id'], name='spaceobject_sbdb_or_ff8d91_idx'),
),
]

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-24 20:33
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0028_auto_20190224_1945'),
]
operations = [
migrations.RemoveIndex(
model_name='spaceobject',
name='spaceobject_sbdb_or_ff8d91_idx',
),
migrations.RemoveField(
model_name='spaceobject',
name='sbdb_order_id',
),
]

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-02-26 06:04
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0029_auto_20190224_2033'),
]
operations = [
migrations.AlterModelOptions(
name='orbitclass',
options={'ordering': ['id']},
),
migrations.AlterModelOptions(
name='spaceobject',
options={'ordering': ['id']},
),
migrations.AlterField(
model_name='spaceobject',
name='H',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='spaceobject',
name='orbit_class',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='spaceobjects.OrbitClass'),
),
]

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-03-04 04:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0030_auto_20190226_0604'),
]
operations = [
migrations.AlterModelOptions(
name='closeapproach',
options={'ordering': ['id']},
),
migrations.AlterModelOptions(
name='nhatsobject',
options={'ordering': ['id']},
),
migrations.AlterModelOptions(
name='sentryevent',
options={'ordering': ['id']},
),
migrations.AddIndex(
model_name='closeapproach',
index=models.Index(fields=['date'], name='spaceobject_date_e8c089_idx'),
),
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['diameter'], name='spaceobject_diamete_a2968b_idx'),
),
migrations.AddIndex(
model_name='sentryevent',
index=models.Index(fields=['prob'], name='spaceobject_prob_b35e86_idx'),
),
migrations.AddIndex(
model_name='nhatsobject',
index=models.Index(fields=['min_dv'], name='spaceobject_min_dv_014adf_idx'),
),
]

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-05-16 22:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0031_auto_20190304_0448'),
]
operations = [
migrations.AlterField(
model_name='shapemodel',
name='jd',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='shapemodel',
name='period_hr',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='shapemodel',
name='spin_angle',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='shapemodel',
name='spin_latitude',
field=models.FloatField(blank=True, null=True),
),
migrations.AlterField(
model_name='shapemodel',
name='spin_longitude',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-05-16 22:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0032_auto_20190516_2237'),
]
operations = [
migrations.AlterField(
model_name='shapemodel',
name='quality',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-05-16 22:51
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0033_auto_20190516_2241'),
]
operations = [
migrations.AddField(
model_name='shapemodel',
name='source',
field=models.CharField(default='damit', max_length=512),
preserve_default=False,
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-07-07 06:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0034_shapemodel_source'),
]
operations = [
migrations.AddIndex(
model_name='spaceobject',
index=models.Index(fields=['a'], name='spaceobject_a_f9bd0b_idx'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-08-31 16:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0035_auto_20190707_0613'),
]
operations = [
migrations.AddField(
model_name='spaceobject',
name='diameter_estimate',
field=models.FloatField(blank=True, null=True),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.16 on 2019-08-31 19:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0036_spaceobject_diameter_estimate'),
]
operations = [
migrations.AlterField(
model_name='spaceobject',
name='object_type',
field=models.CharField(choices=[(b'ASTEROID', 'ASTEROID'), (b'COMET', 'COMET')], max_length=20),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.23 on 2020-01-17 17:22
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('spaceobjects', '0037_auto_20190831_1918'),
]
operations = [
migrations.AlterField(
model_name='spaceobject',
name='spec_B',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterField(
model_name='spaceobject',
name='spec_T',
field=models.CharField(blank=True, max_length=200, null=True),
),
]

View File

494
spaceobjects/models.py Normal file
View File

@ -0,0 +1,494 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import math
from datetime import datetime, date
from enum import Enum
from django.contrib import admin
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from jsonfield import JSONField
from spaceobjects.description import get_diameter_comparison, composition, COMET_CLASSES
class ObjectType(Enum):
ASTEROID = 'ASTEROID'
COMET = 'COMET'
@classmethod
def from_class(self, classname):
if classname in COMET_CLASSES:
return self.COMET
return self.ASTEROID
def __str__(self):
return self.value
class OrbitClass(models.Model):
name = models.CharField(max_length=200)
slug = models.CharField(max_length=200, unique=True)
abbrev = models.CharField(max_length=10)
desc = models.CharField(max_length=500)
orbit_sentence = models.CharField(max_length=500)
def get_absolute_url(self):
return reverse('category', args=[self.slug])
def __str__(self):
return self.abbrev
class Meta:
ordering = ['id']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['abbrev']),
]
class SpaceObject(models.Model):
fullname = models.CharField(max_length=200)
name = models.CharField(max_length=200)
slug = models.CharField(max_length=200)
orbit_class = models.ForeignKey(OrbitClass, blank=True, null=True)
object_type = models.CharField(max_length = 20,
choices=[(tag.name, tag.value) for tag in ObjectType])
# Basic orbital elements
a = models.FloatField()
e = models.FloatField()
i = models.FloatField()
om = models.FloatField()
w = models.FloatField()
ma = models.FloatField()
epoch = models.FloatField()
is_nea = models.BooleanField()
is_pha = models.BooleanField()
spec_B = models.CharField(max_length=200, null=True, blank=True)
spec_T = models.CharField(max_length=200, null=True, blank=True)
H = models.FloatField(null=True, blank=True)
# For now this is only SBDB diameter. See get_diameter_estimate below.
diameter = models.FloatField(null=True, blank=True)
# Automatically filled via get_diameter_estimate
diameter_estimate = models.FloatField(null=True, blank=True)
# sbdb blob
sbdb_entry = JSONField()
def get_absolute_url(self):
if self.object_type == ObjectType.COMET.value:
return reverse('detail_comet', args=[self.slug])
return reverse('detail_asteroid', args=[self.slug])
@cached_property
def shorthand(self):
if self.name.find('(') > -1:
return self.name[self.name.find('(') + 1 : self.name.find(')')]
return self.name
@cached_property
def has_shorthand(self):
return self.shorthand() != self.name
@cached_property
def get_object_type(self):
if self.fullname == '1 Ceres':
return 'object'
if self.is_comet:
return 'comet'
if self.is_asteroid:
return 'asteroid'
# This should never happen...
return 'object'
@cached_property
def composition(self):
return composition(self)
@cached_property
def perihelion(self):
return self.a * (1 - self.e)
@cached_property
def aphelion(self):
return self.a * (1 + self.e)
@cached_property
def moid(self):
entry = self.sbdb_entry.get('moid')
if not entry:
return None
return float(entry)
@cached_property
def firstobs_date(self):
firstobs = self.sbdb_entry.get('first_obs')
return datetime.strptime(firstobs, '%Y-%m-%d')
@cached_property
def lastobs_date(self):
lastobs = self.sbdb_entry.get('last_obs')
return datetime.strptime(lastobs, '%Y-%m-%d')
@cached_property
def size_adjective(self):
diameter = self.get_diameter_estimate()
if not diameter:
return None
if diameter < 0.5:
return 'very small'
if diameter < 1:
return 'small'
if diameter < 10:
return 'mid-sized'
if diameter < 100:
return 'large'
if diameter < 200:
return 'large'
if diameter < 300:
return 'very large'
if self.object_type == ObjectType.ASTEROID.value:
return 'dwarf planet'
return 'large'
@cached_property
def avg_orbital_speed(self):
# in km/s
if self.period_in_days > 0:
return (2 * math.pi * self.a * 149597870.7) / (self.period_in_days * 86400)
return None
@cached_property
def is_dwarf_planet(self):
return self.size_adjective == 'dwarf planet'
@cached_property
def is_asteroid(self):
return self.object_type == ObjectType.ASTEROID.value
@cached_property
def is_comet(self):
return self.object_type == ObjectType.COMET.value
@cached_property
def has_size_info(self):
diam = self.sbdb_entry.get('diameter')
return diam != '' and diam is not None
@cached_property
def has_size_info_estimate(self):
return self.get_diameter_estimate() is not None
@cached_property
def get_size_rough_comparison(self):
diameter = self.get_diameter_estimate()
if not diameter:
return None
if diameter > 900:
return 'the largest asteroid/dwarf planet'
if diameter > 300:
return 'one of the largest objects'
if diameter > 1:
return 'larger than 99% of asteroids'
if diameter > 0.5:
return 'larger than ~97% of asteroids but small compared to large asteroids'
if diameter > 0.3:
return 'larger than 90% of asteroids but tiny compared to large asteroids'
return 'a small to average asteroid'
@cached_property
def get_diameter_comparison(self):
return get_diameter_comparison(self)
@cached_property
def get_similar_orbits(self, n=3):
a_range = [self.a - 0.01, self.a + 0.01]
similar = SpaceObject.objects.filter(a__range=a_range).exclude(pk=self.pk)
return similar[:n]
@cached_property
def period_in_days(self):
try:
return float(self.sbdb_entry['per'])
except:
return None
@cached_property
def period_in_years(self):
try:
return float(self.sbdb_entry['per']) / 365.25
except:
return None
@cached_property
def ordered_close_approaches(self):
return self.closeapproach_set.all().order_by('date')
@cached_property
def future_close_approaches(self):
return self.closeapproach_set.filter(date__gte=date.today()).order_by('date')
@cached_property
def ordered_sentry_events(self):
return self.sentryevent_set.all().order_by('-prob')
@cached_property
def ordered_shape_models(self):
return self.shapemodel_set.all().order_by('-quality')
def get_diameter_estimate_low(self):
return self.get_diameter_estimate(method='LOW')
def get_diameter_estimate_high(self):
return self.get_diameter_estimate(method='HIGH')
def get_diameter_estimate(self, method='MID'):
'''Diameter estimate in km, using either SBDB-supplied estimate
or estimate based on magnitude/albedo'''
diameter = self.diameter
if diameter:
return diameter
# NHATS also has a diameter estimate
nhats_set = self.nhatsobject_set.all()
if len(nhats_set):
nhats = nhats_set[0]
if method == 'MID':
return (nhats.min_diameter + nhats.max_diameter) / 2.0 / 1000.
elif method == 'HIGH':
return nhats.max_diameter / 1000.
else:
# method == LOW
return nhats.min_diameter / 1000.
try:
if 'H' in self.sbdb_entry:
mag = float(self.sbdb_entry['H'])
if method == 'MID':
albedo_default = 0.15
elif method == 'HIGH':
albedo_default = 0.05
else:
# method == LOW
albedo_default = 0.25
elif 'M2' in self.sbdb_entry:
# Comet nuclear magnitude
mag = float(self.sbdb_entry['M2'])
# Typical albedo for a comet
# https://en.wikipedia.org/wiki/Comet_nucleus#Albedo
albedo_default = 0.04
albedo_known = self.sbdb_entry.get('albedo')
albedo = float(albedo_known) if albedo_known else albedo_default
# Estimate diameter in km
# http://www.physics.sfasu.edu/astro/asteroids/sizemagnitude.html
return 1329 / math.sqrt(albedo) * math.pow(10, -0.2 * mag)
except ValueError:
pass
except KeyError:
pass
except TypeError:
pass
return None
@cached_property
def get_1wtc_pct(self):
# Helper function used to get size ratio compared to 1 WTC in New York City.
diam_km = self.get_diameter_estimate()
return diam_km / 0.5413 * 100.0
@cached_property
def get_everest_pct(self):
# Helper function used to get size ratio compared to Mt Everest prominence of photo.
# Photo shows elevation from ~16417 ft to ~29032 ft = 3.845052 km * adjustment.
diam_km = self.get_diameter_estimate()
return diam_km / (3.845052 * 2.25) * 100.0
def to_search_result(self):
q = self.sbdb_entry.get('q', None)
if q:
q = float(q)
tp = self.sbdb_entry.get('tp', None)
if tp:
tp = float(tp)
n = self.sbdb_entry.get('n', None)
if n:
n = float(n)
return {
'fullname': self.fullname,
'name': self.name,
'slug': self.slug,
'ephem': {
'a': self.a,
'e': self.e,
'i': self.i,
'om': self.om,
'w': self.w,
'ma': self.ma,
'q': q,
'n': n,
'tp': tp,
'epoch': self.epoch,
}
}
def to_orbit_obj(self):
q = self.sbdb_entry.get('q', None)
if q:
q = float(q)
tp = self.sbdb_entry.get('tp', None)
if tp:
tp = float(tp)
return {
'a': self.a,
'e': self.e,
'i': self.i,
'om': self.om,
'w': self.w,
'ma': self.ma,
'q': q,
'tp': tp,
'epoch': self.epoch,
}
def save(self):
self.diameter_estimate = self.get_diameter_estimate()
super(SpaceObject, self).save()
def __str__(self):
return self.fullname
class Meta:
ordering = ['id']
indexes = [
models.Index(fields=['fullname']),
models.Index(fields=['slug']),
models.Index(fields=['orbit_class']),
models.Index(fields=['object_type']),
models.Index(fields=['is_nea']),
models.Index(fields=['is_pha']),
models.Index(fields=['diameter']),
models.Index(fields=['a']),
]
class CloseApproach(models.Model):
space_object = models.ForeignKey(SpaceObject)
date = models.DateField()
v_rel = models.FloatField()
h_mag = models.FloatField()
# Distances in AU
dist_au = models.FloatField()
dist_min_au = models.FloatField()
@cached_property
def dist_km(self):
return self.dist_au * 1.496e8
class Meta:
ordering = ['id']
indexes = [
models.Index(fields=['date']),
]
class SentryEvent(models.Model):
space_object = models.ForeignKey(SpaceObject)
date = models.DateField()
energy_mt = models.FloatField()
palermo_scale = models.FloatField()
torino_scale = models.FloatField()
prob = models.FloatField()
@cached_property
def prob_percentage(self):
return self.prob * 100.0
@cached_property
def energy_with_units(self):
if self.energy_mt > 1:
return '%s megatons' % (self.energy_mt)
return '%s kilotons' % (self.energy_mt * 1000)
class Meta:
ordering = ['id']
indexes = [
models.Index(fields=['prob']),
]
class NhatsObject(models.Model):
space_object = models.ForeignKey(SpaceObject)
num_trajectories = models.IntegerField()
min_dv = models.FloatField()
min_dv_duration = models.FloatField()
# Diameter in meters
min_diameter = models.FloatField()
max_diameter = models.FloatField()
def __str__(self):
return self.space_object.fullname
class Meta:
ordering = ['id']
indexes = [
models.Index(fields=['min_dv']),
]
class ShapeModel(models.Model):
space_object = models.ForeignKey(SpaceObject)
source = models.CharField(max_length=512)
# Path to shapefile
shape_path = models.CharField(max_length=200)
# Path to image
render_path = models.CharField(max_length=200)
# Ecliptic lat/lng of the spin axis direction (beta and lambda
# respectively).
spin_latitude = models.FloatField(null=True, blank=True)
spin_longitude = models.FloatField(null=True, blank=True)
# Initial rotation angle (phi)
spin_angle = models.FloatField(null=True, blank=True)
# Period in hours
period_hr = models.FloatField(null=True, blank=True)
# Initial julian date
jd = models.FloatField(null=True, blank=True)
# Linear change in the rotation rate (dω / dt) caused by the
# Yarkovsky-O'Keefe-Radzievskii-Paddack effect (rad / day2)
yorp = models.FloatField(null=True, blank=True)
# References string
# reference = models.CharField(max_length=1000)
# Equivalent diameter - diameter of sphere that has the same volume as
# model.
equiv_diameter_km = models.FloatField(null=True, blank=True)
# Quality level
quality = models.FloatField(null=True, blank=True)
admin.site.register(SpaceObject)
admin.site.register(CloseApproach)
admin.site.register(SentryEvent)
admin.site.register(NhatsObject)
admin.site.register(OrbitClass)
admin.site.register(ShapeModel)

21
spaceobjects/sitemap.py Normal file
View File

@ -0,0 +1,21 @@
from django.contrib.sitemaps import Sitemap
from spaceobjects.models import SpaceObject, OrbitClass
class SpaceObjectSitemap(Sitemap):
protocol = 'https'
def items(self):
return SpaceObject.objects.filter(is_pha=True) | SpaceObject.objects.filter(diameter__gt=100)
def priority(self, obj):
if obj.is_pha:
return 0.25
return 0.5
class OrbitClassSitemap(Sitemap):
protocol = 'https'
priority = 0.6
def items(self):
return OrbitClass.objects.all()

View File

@ -0,0 +1,143 @@
{% extends 'layout.html' %}
{% load humanize %}
{% load static %}
{% block title%}{{page_name}} | Space Reference{% endblock %}
{% block scripts %}
<script src="{% static "js/lib/three.r98.min.js" %}"></script>
<script src="{% static "js/lib/TrackballControls.js" %}"></script>
<script src="{% static "js/lib/spacekit.js" %}"></script>
<script src="{% static "js/orbitDiagram.js" %}"></script>
<script src="{% static "js/lib/d3.v3.js" %}"></script>
<!--
<script src="http://localhost:8001/src/lib/three.r98.min.js"></script>
<script src="http://localhost:8001/src/lib/TrackballControls.js"></script>
<script src="http://localhost:8001/build/spacekit.js"></script>
-->
<script>
window.OBJECT_DEFINITIONS = [
{% for object in objects %}
{
name: unescape("{{object.name}}"),
slug: "{{object.slug}}",
ephem: {
a: {{object.a}},
e: {{object.e}},
i: {{object.i}},
om: {{object.om}},
w: {{object.w}},
ma: {{object.ma}},
q: {{object.sbdb_entry.q}},
n: {{object.sbdb_entry.n}} + 0,
tp: {{object.sbdb_entry.tp}},
epoch: {{object.epoch}}
}
},
{% endfor %}
];
window.BACKGROUND_QUERY_URL = '/api/category/{{category_slug}}/orbits';
window.VIZ_SIMULATION_OPTS = {
jdPerSecond: 5,
};
window.VIZ_OBJECT_OPTS = {
ecliptic: {
displayLines: false,
},
};
var diagram = new OrbitDiagram('.orbit-diagram', {
// object's semimajor axis should be 40px
pixels_per_au: 40 / {{objects.0.a}},
});
diagram.prepareRender();
diagram.renderPlanets(true /* useSmartLabels */, {{objects.0.a}});
{% for object in objects %}
diagram.plotOrbit({{object.a}}, {{object.e}}, {{object.w}}, '#fff');
{% endfor %}
function selectOrbit(domElement) {
window.spaceobjects[domElement.dataset.slug].getOrbit().setHexColor(0xff0000);
}
function deselectOrbit(domElement) {
window.spaceobjects[domElement.dataset.slug].getOrbit().setHexColor(0xffffff);
}
</script>
<script src="{% static "js/main.js" %}"></script>
<script>
init3dVis();
</script>
{% endblock %}
{% block header %}
<header class="site-header">
<div class="container">
<h1 class="mainheading">{{page_name}}</h1>
<div class="orbit-diagram"></div>
</div>
</header>
{% endblock %}
{% block content %}
<div>
<div class="breadcrumbs">
<a href="/">Space Reference</a>
&raquo;
{{page_name}}
</div>
<div class="item-container col-sm-12">
{% if orbit_class %}
{{orbit_class.name}}s ({{orbit_class.abbrev}}) are objects {{orbit_class.orbit_sentence}}. There are {{count|intcomma}} {{orbit_class.name|capfirst}}s in this database out of {{total_count|intcomma}} total, accounting for {{population_pct|floatformat:1}}% of objects.
{% else %}
There are {{count|intcomma}} {{page_name|lower}} of this type in the database out of {{total_count|intcomma}} total, accounting for {{population_pct|floatformat:1}}% of objects.
{% endif %}
</div>
<div class="item-container col-sm-4">
<div class="item-container__inner">
<h3>Search</h3>
<div class="react-search"></div>
</div>
<div class="item-container__inner tile-list">
{% for object in objects %}
<div class="tile" data-slug="{{object.slug}}"
onmouseover="if (typeof selectOrbit !== 'undefined') selectOrbit(this)"
onmouseout="if (typeof deselectOrbit !== 'undefined') deselectOrbit(this)"
onclick="window.location.href='/asteroid/{{object.slug}}'"
>
<div>
<h5><a href="/asteroid/{{object.slug}}">{{object.name}}</a></h5>
<div class="tile-content">
<span class="label label-danger">{{object.size_adjective}}</span>
{% if object.is_pha %}
<span class="label label-warning">Potentially Hazardous</span>
{% else %}
<span class="label label-info">Not Hazardous</span>
{% endif %}
<br>
<div class="tile-desc">
{{object.shorthand}} orbits the sun every {{object.sbdb_entry.per|floatformat:0|intcomma}} days, coming as close as {{object.perihelion|floatformat:2}} AU and reaching as far as {{object.aphelion|floatformat:2}} AU from the sun.
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="item-container item-container__rightside col-sm-8">
<div class="item-container__inner">
<div class="vis-panel">
<div class="vis-controls">
<button class="vis-controls__slower">Slower</button>
<button class="vis-controls__faster">Faster</button>
<button class="vis-controls__set-date">Set Date</button>
<span class="vis-status"></span>
<span class="vis-fullscreen-shortcut"><a href="/solar-system#cat={{category_slug}}"></a></span>
</div>
<div id="orbit-sim" class="vis-container vis-container__category"></div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,504 @@
{% extends 'layout.html' %}
{% load humanize %}
{% load static %}
{% block title%}{% if object.is_asteroid %}Asteroid {% elif object.is_comet %}Comet {% endif %}{{object.name}} | Space Reference{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static "css/lib/celestial0.6.css" %}" />
{% endblock %}
{% block scripts %}
<script src="{% static "js/lib/three.r98.min.js" %}"></script>
<script src="{% static "js/lib/TrackballControls.js" %}"></script>
<script src="{% static "js/lib/spacekit.js" %}"></script>
<script src="{% static "js/orbitDiagram.js" %}"></script>
<script src="{% static "js/sizeComparison.js" %}"></script>
<script src="{% static "js/lib/d3.v3.js" %}"></script>
<script src="{% static "js/lib/d3.geo.zoom.js" %}"></script>
<script src="{% static "js/lib/d3.geo.projection.js" %}"></script>
<script src="{% static "js/lib/celestial0.6.js" %}"></script>
<!--
<script src="http://localhost:8001/src/lib/three.r98.min.js"></script>
<script src="http://localhost:8001/src/lib/TrackballControls.js"></script>
<script src="http://localhost:8001/build/spacekit.js"></script>
-->
<script>
window.OBJECT_DEFINITIONS = [{
name: unescape("{{object.name}}"),
slug: "{{object.slug}}",
ephem: {
a: {{object.a}},
e: {{object.e}},
i: {{object.i}},
om: {{object.om}},
w: {{object.w}},
ma: {{object.ma}},
q: {{object.sbdb_entry.q}},
n: {{object.sbdb_entry.n}} + 0,
tp: {{object.sbdb_entry.tp}},
epoch: {{object.epoch}}
}
}];
window.VIZ_SIMULATION_OPTS = {
jdPerSecond: 0.1,
startPaused: true,
};
window.VIZ_OBJECT_OPTS = {
ecliptic: {
displayLines: {{object.i}} < 20,
},
labelText: '{{object.name}}',
};
// This is here because celestial.js doesn't play nice with other canvases on
// the page. Don't load anything else until celestial is done loading.
window.pageReady = false;
</script>
<script src="{% static "js/main.js" %}"></script>
<script src="{% static "js/detail.js" %}"></script>
<script>
let onPageReadyHandlers = [];
onPageReadyHandlers.push(() => {
if ({{object.a}} < 0) {
return;
}
const diagram = new OrbitDiagram('.orbit-diagram', {
// object's semimajor axis should be 40px
pixels_per_au: 40 / {{object.a}},
});
diagram.prepareRender();
diagram.renderPlanets(true /* useSmartLabels */, {{object.a}});
diagram.plotOrbit({{object.a}}, {{object.e}}, {{object.w}}, '#fff', '{{object.shorthand}}');
});
onPageReadyHandlers.push(init3dVis);
onPageReadyHandlers.push(initReferenceTables);
{% if object.has_size_info_estimate %}
(function() {
var overlay = document.getElementById('size-comparison-overlay');
overlay.onclick = function() {
const sizeComparison = new SizeComparison('#size-comparison', {});
sizeComparison.render({{object.get_diameter_estimate}});
};
})();
{% endif %}
const t = setInterval(function() {
if (window.pageReady) {
onPageReadyHandlers.forEach(fn => fn());
clearInterval(t);
}
}, 500);
</script>
{% if object.has_size_info_estimate %}
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCGGGEBu3lHIz-sfzh-OB6PUe3-6mBteQI">
</script>
{% endif %}
{% endblock %}
{% block header %}
<!-- Spaceref OBJECT ID: {{object.id}} -->
<header class="site-header">
<div class="container">
<h1 class="mainheading">{{object.name}}</h1>
<h2 class="subheading">
{% if object.fullname == object.name %}
{% if object.has_size_info_estimate %}
{{object.size_adjective|capfirst}}
{% endif %}
{{object.orbit_class.name}}
{% else %}
{{object.fullname}}
{% endif %}
</h2>
{% if object.is_nea %}
<span class="label label-info"><a href="/category/near-earth-asteroids">Near-Earth</a></span>
{% endif %}
{% if object.is_pha %}
<span class="label label-warning"><a href="/category/potentially-hazardous-asteroids">Potentially Hazardous</a></span>
{% endif %}
<div class="orbit-diagram"></div>
</div>
</header>
{% endblock %}
{% block content %}
<div class="breadcrumbs">
<a href="/">Space Reference</a>
&raquo;
<a href="/category/{{object.orbit_class.slug}}">{{object.orbit_class.name}}s</a>
&raquo;
{{object.name}}
</div>
<div class="item-container col-sm-8">
<div class="item-container__inner">
<h3>Key Facts</h3>
<ul class="keyfacts">
<li>Categorized as a <a href="/category/{{object.orbit_class.slug}}">{{object.orbit_class.name}}</a></li>
{% if object.has_size_info %}
<li>Comparable in size to {{object.get_diameter_comparison}} ({{object.sbdb_entry.diameter | floatformat:2}} km diameter)</li>
{% elif object.has_size_info_estimate %}
<li>Comparable in size to {{object.get_diameter_comparison}}</li>
{% endif %}
{% if object.closeapproach_set.all|length %}
{% with approach=object.closeapproach_set.all.0 %}
<li>Will pass within {{approach.dist_km|floatformat:0|intcomma}} km of Earth in {{approach.date|date:"Y"}}</li>
{% endwith %}
{% endif %}
{% if object.is_nea %}
<li>Classified as a Near Earth Asteroid (NEA)</li>
{% else %}
<li>Not a Near Earth Object</li>
{% endif %}
{% if object.is_pha %}
<li>Classified as a Potentially Hazardous Asteroid (PHA)</li>
{% else %}
<li>Not a Potentially Hazardous Object</li>
{% endif %}
<li><a href="/solar-system#ob={{object.slug}}">See orbit simulation</a></li>
</ul>
<h3>Overview</h3>
<p>
{% if object.is_dwarf_planet %}
{{object.name}} is a dwarf planet {{object.orbit_class.orbit_sentence}}.
{% elif object.has_size_info_estimate %}
{{object.name}} is a {{object.size_adjective}} {{object.get_object_type}} {{object.orbit_class.orbit_sentence}}.
{% else %}
{{object.get_object_type|capfirst}} {{object.name}} is an object {{object.orbit_class.orbit_sentence}}.
{% endif %}
{% if object.is_pha and object.is_nea %}
NASA JPL has classified {{object.shorthand}} as a "Potentially Hazardous Asteroid" due to its predicted close pass(es) with Earth.
{% elif object.is_nea %}
NASA JPL has classified {{object.shorthand}} as a "Near Earth Asteroid" due to its orbit's proximity to Earth, but it is not considered potentially hazardous because computer simulations have not indicated any imminent likelihood of future collision.
{% else %}
NASA JPL has not classified {{object.shorthand}} as potentially hazardous because its orbit does not bring it close to Earth.
{% endif %}
</p>
<p>
{{object.shorthand}} orbits the sun every {{object.period_in_days|floatformat:0|intcomma}} days ({{object.period_in_years|floatformat:2|intcomma}} years), coming as close as {{object.perihelion|floatformat:2}} AU and reaching as far as {{object.aphelion|floatformat:2}} AU from the sun.
{% if object.e > .7 %}
Its orbit is highly elliptical.
{% endif %}
{% if object.has_size_info %}
{{object.shorthand}} is about {{object.sbdb_entry.diameter | floatformat:1}} kilometers in diameter, making it {{object.get_size_rough_comparison}}, comparable in size to {{object.get_diameter_comparison}}.
{% elif object.has_size_info_estimate %}
Based on its brightness and the way it reflects light, {{object.shorthand}} is <a href="https://cneos.jpl.nasa.gov/tools/ast_size_est.html">probably</a> between {{object.get_diameter_estimate_low | floatformat:3}} to {{object.get_diameter_estimate_high | floatformat:3}} kilometers in diameter, making it {{object.get_size_rough_comparison}}, very roughly comparable in size to {{object.get_diameter_comparison}}.
{% endif %}
</p>
{% if object.sbdb_entry.rot_per %}
<p>
The rotation of {{object.shorthand}} has been observed. It completes a rotation on its axis every {{object.sbdb_entry.rot_per|floatformat:2}} hours.
</p>
{% endif %}
{% if object.composition %}
<p>
{{object.shorthand}}'s spectral type {{object.sbdb_entry.spec_T}} (<a href="http://adsabs.harvard.edu/abs/1989aste.conf..298T">Tholen</a>) / {{object.sbdb_entry.spec_B}} (<a href="http://smass.mit.edu/smass.html">SMASSII</a>) indicates that it is likely to contain
{% include 'spaceobjects/partials/composition.html' %}
</p>
{% endif %}
{% if object.ordered_close_approaches|length < 1 and object.ordered_sentry_events|length < 1 %}
<h3>No Close Approaches</h3>
{% else %}
<h3>Close Approaches</h3>
{% endif %}
{% if object.moid %}
<p>
{{object.shorthand}}'s orbit is {{object.moid | floatformat:2}} AU from Earth's orbit at its closest point.
{% if object.moid > 1 %}
This means that there is an extremely wide berth between this asteroid and Earth at all times.
{% elif object.moid > 0.5 %}
This means that there is a very wide berth between this asteroid and Earth at all times.
{% elif object.moid > 0.01 %}
This means that there is a wide berth between this asteroid and Earth at all times.
{% elif object.moid < 0.001 %}
This means that its orbit is very close to Earth's orbit.
{% elif object.moid < 0.0001 %}
This means that its orbit is extremely close to Earth's orbit.
{% else %}
This means that its orbit is relatively close to Earth's orbit.
{% endif %}
</p>
{% endif %}
{% if object.sbdb_entry.moid_jup < 0.5 %}
<p>
The orbit of {{object.shorthand}} brings it close to Jupiter's orbit.
</p>
{% endif %}
<p>
{% if object.ordered_close_approaches|length < 1 %}
Orbital simulations conducted by NASA JPL's CNEOS do not show any close approaches to Earth.
{% else %}
<p>
{{object.shorthand}} has {{object.ordered_close_approaches|length}} close approaches predicted in the coming decades:
</p>
<div class="reference-table">
<div class="reference-table-content">
<table class="table table-striped table-hover table-condensed">
<thead>
<tr>
<th>Date</th>
<th>Distance from Earth (km)</th>
<th>Velocity (km/s)</th>
</tr>
</thead>
<tbody>
{% for close_approach in object.ordered_close_approaches %}
<tr>
<td>{{close_approach.date | date}}</td>
<td>{{close_approach.dist_km|floatformat:0|intcomma}}</td>
<td>{{close_approach.v_rel|floatformat:3}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if object.ordered_close_approaches|length > 10 %}
<div class="reference-table-gradient"></div>
{% endif %}
</div>
{% endif %}
{% if object.ordered_sentry_events %}
<p>
<a href="https://cneos.jpl.nasa.gov/sentry/details.html#?des={{object.sbdb_entry.pdes}}">NASA Sentry</a> has assessed impact risk for {{object.ordered_sentry_events|length}} very close approach scenarios. Here are the top scenarios ordered by probability of impact:
<ul>
</ul>
<div class="reference-table">
<div class="reference-table-content">
<table class="table table-striped table-hover table-condensed">
<thead>
<tr>
<th>Date</th>
<th>Probability of Impact (%)</th>
<th>Impact Energy (Mt)</th>
</tr>
</thead>
<tbody>
{% for event in object.ordered_sentry_events %}
<tr>
<td>{{event.date|date}}</td>
<td>{{event.prob_percentage|floatformat:5}}</td>
<td>{{event.energy_mt}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if object.ordered_sentry_events|length > 10 %}
<div class="reference-table-gradient"></div>
{% endif %}
</div>
</p>
{% endif %}
</p>
<h3>Images and Observations</h3>
<p>
{{object.shorthand}}'s orbit is determined by observations dating back to {{object.firstobs_date | date}}. It was last officially observed on {{object.lastobs_date | date}}. The IAU Minor Planet Center records {{object.sbdb_entry.n_obs_used|intcomma}} observations used to determine its orbit.
</p>
{% if object.ordered_shape_models|length %}
<p>
Scientists have been able to determine this object's shape:
</p>
<p>
<a href="/asteroid/{{object.slug}}/shape"><img loading="lazy" class="img-responsive" src="{% static object.ordered_shape_models.0.render_path %}" /></a>
</p>
<p>
<strong>View <a href="/asteroid/{{object.slug}}/shape">{{object.get_object_type}} {{object.name}} in 3D</a>.</strong>
</p>
{% endif %}
<h3>Accessibility and Exploration</h3>
{% if object.nhatsobject_set.all|length %}
{% with nhats=object.nhatsobject_set.all.0 %}
<p>
{{object.shorthand}} can be reached with a journey of {{nhats.min_dv_duration|floatformat:0|intcomma}} days. This trajectory would require a <a href="https://en.wikipedia.org/wiki/Delta-v_budget">delta-v</a> of {{nhats.min_dv}} km/s. To put this into perspective, the delta-v to launch a rocket to Low-Earth Orbit is 9.7 km/s. There are {{nhats.num_trajectories|intcomma}} potential trajectories and launch windows to this {{object.get_object_type}}.
</p>
<p>
See more at the <a href="https://cneos.jpl.nasa.gov/nhats/details.html#?des={{object.sbdb_entry.pdes}}">NHATS Mission Trajectories</a> table for {{object.shorthand}}.
</p>
{% endwith %}
{% else %}
This {{object.get_object_type}} is not considered a viable target for human exploration by the <a href="https://cneos.jpl.nasa.gov/nhats/">NHATS study</a>.
{% endif %}
{% if object.get_similar_orbits|length %}
<h3>Similar Objects</h3>
These objects have orbits that share similar characteristics to the orbit of {{object.shorthand}}:
<ul>
{% for similar in object.get_similar_orbits %}
<li>
<a rel="nofollow" href="/asteroid/{{similar.slug}}">{{similar.fullname}}</a>
</li>
{% endfor %}
</ul>
{% endif %}
<h3>References</h3>
<ul>
<li><a rel="nofollow" href="https://ssd.jpl.nasa.gov/sbdb.cgi?sstr={{object.sbdb_entry.pdes}}">JPL Small Body Database</a></li>
<li><a rel="nofollow" href="https://ssd.jpl.nasa.gov/?mdesign_server&des={{object.sbdb_entry.pdes}}">Mission Design</a></li>
<li><a rel="nofollow" href="https://in-the-sky.org/findercharts.php?objtxt=A{{object.sbdb_entry.pdes}}&duration=5">Sky Finder Chart</a></li>
</ul>
</div>
</div>
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<h4>Search</h4>
<div class="react-search"></div>
&nbsp;&nbsp; or view a <a rel="nofollow" href="/asteroid/random">random</a> object
</div>
</div>
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<h4>Orbital Elements</h4>
<ul class="keyfacts">
<li>Epoch: {{object.epoch}} JD</li>
<li>Semi-major axis: {{object.a}} AU</li>
<li>Eccentricity: {{object.e}}</li>
<li>Inclination: {{object.i}}°</li>
<li>Longitude of Ascending Node: {{object.om}}°</li>
<li>Argument of Periapsis: {{object.w}}°</li>
<li>Mean Anomaly: {{object.ma}}°</li>
</ul>
</div>
</div>
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<h4>Physical Characteristics</h4>
<ul class="keyfacts">
{% if object.has_size_info %}
<li>Diameter: {{object.sbdb_entry.diameter | floatformat:5}} km</li>
{% elif object.has_size_info_estimate %}
<li>Diameter: ~{{object.get_diameter_estimate | floatformat:3}} km</li>
{% endif %}
{% if object.H %}
<li>Magnitude: {{object.H}}</li>
{% endif %}
{% if object.sbdb_entry.albedo %}
<li>Albedo: {{object.sbdb_entry.albedo}}</li>
{% endif %}
{% if object.sbdb_entry.spec_T %}
<li>Spectral type (Tholen): {{object.spec_T}}</li>
{% endif %}
{% if object.sbdb_entry.spec_B %}
<li>Spectral type (SMASS): {{object.spec_B}}</li>
{% endif %}
</ul>
</div>
</div>
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<h4>Derived Characteristics</h4>
<ul class="keyfacts">
{% if object.period_in_days %}
<li>Orbit Period: {{object.period_in_days|floatformat:0|intcomma}} days ({{object.period_in_years|floatformat:2|intcomma}} years)</li>
{% endif %}
{% if object.avg_orbital_speed %}
<li>Avg. Orbit Speed: {{object.avg_orbital_speed|floatformat:2|intcomma}} km/s</li>
{% endif %}
{% if object.aphelion %}
<li>Aphelion Distance: {{object.aphelion|floatformat:2}} AU</li>
{% endif %}
<li>Perihelion Distance: {{object.perihelion|floatformat:2}} AU</li>
{% if object.sbdb_entry.rot_per %}
<li>Rotation Period: {{object.sbdb_entry.rot_per|floatformat:2|intcomma}} hours</li>
{% endif %}
{% if object.composition %}
<li>Approx. Composition: {% include 'spaceobjects/partials/composition.html' %}</li>
{% endif %}
</ul>
</div>
</div>
{% if object.has_size_info_estimate %}
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<h4>Map Comparison</h4>
<div id="size-comparison">
<div class="size-comparison-placeholder">
<div id="size-comparison-overlay" class="size-comparison-overlay">
Click to load map
</div>
</div>
</div>
<!--
<div id="height-comparison">
<div>
<img src="{% static "images/empire-state-building.svg" %}" />
Empire State Building
</div>
<div>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" style="fill:#ccc" />
<circle cx="25" cy="25" r="4" style="fill:#999" />
<circle cx="65" cy="15" r="5" style="fill:#999" />
<circle cx="35" cy="70" r="4" style="fill:#999" />
<circle cx="45" cy="35" r="3" style="fill:#999" />
<circle cx="80" cy="60" r="5" style="fill:#999" />
</svg>
{{object.name}}
</div>
</div>
-->
</div>
</div>
{% endif %}
<div class="item-container col-sm-12">
<div class="item-container__inner">
<h3>Orbit Simulation</h3>
<div class="vis-panel">
<div class="vis-controls">
<button class="vis-controls__slower">Slower</button>
<button class="vis-controls__faster">Faster</button>
<button class="vis-controls__set-date">Set Date</button>
<span class="vis-status"></span>
<span class="vis-fullscreen-shortcut"><a href="/solar-system#ob={{object.slug}}"></a></span>
</div>
<div id="orbit-sim" class="vis-container vis-container__detail"></div>
</div>
</div>
</div>
<div class="item-container col-sm-12">
<div class="item-container__inner">
<h3>Sky Map</h3>
<p>
The position of {{object.name}} is indicated by a <span style="color:rgba(255, 0, 204, 1);font-weight:bold">◯ pink circle</span>. Note that the object may not be in your current field of view. Use the controls below to adjust position, location, and time.
</p>
<div class="skymap-container">
<div id="celestial-map" class="skymap-panel"></div>
<div id="celestial-form"></div>
</div>
</div>
</div>
{% if object.get_1wtc_pct > 5 and object.get_1wtc_pct <= 100.0 %}
<div class="item-container col-sm-12">
<div class="item-container__inner">
<h3>Size Rendering</h3>
<p>
The below comparison is an artistic rendering that uses available data on the diameter of {{object.shorthand}} to create an approximate landscape rendering with New York City in the background. This approximation is built for full-resolution desktop browsers. Shape, color, and texture of asteroid are imagined.
</p>
<div id="artistic-comparison" class="artistic-comparison">
<img loading="lazy" class="background" src="{% static "images/nyc-skyline.png" %}" />
<img loading="lazy" class="object" src="{% static "images/generic-asteroid.png" %}" style="height:{{object.get_1wtc_pct}}%" />
</div>
</div>
</div>
{% elif object.get_everest_pct > 5 and object.get_everest_pct <= 80 %}
<div class="item-container col-sm-12">
<div class="item-container__inner">
<h3>Size Rendering</h3>
<p>
The above comparison is an artistic rendering that uses available data on the diameter of {{object.shorthand}} to create an approximate landscape rendering with Mount Everest in the background. This approximation is built for full-resolution desktop browsers. Shape, color, and texture of asteroid are imagined.
</p>
<div id="artistic-comparison" class="artistic-comparison">
<img loading="lazy" class="background" src="{% static "images/everest-background3.png" %}" />
<img loading="lazy" class="object" src="{% static "images/generic-asteroid.png" %}" style="height:{{object.get_everest_pct}}%; z-index:999; box-shadow: 0 0 110px 0 rgba(0, 0, 0, 1); border-radius: 50%; border: 1px solid #ffa5006b; box-sizing: content-box" />
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,199 @@
{% extends 'layout.html' %}
{% load humanize %}
{% load static %}
{% block title%}Comprehensive Asteroid and Comet Database | Space Reference{% endblock %}
{% block scripts %}
<script src="{% static "js/lib/three.r98.min.js" %}"></script>
<script src="{% static "js/lib/TrackballControls.js" %}"></script>
<script src="{% static "js/lib/spacekit.js" %}"></script>
<!--
<script src="http://localhost:8001/src/lib/three.r98.min.js"></script>
<script src="http://localhost:8001/src/lib/TrackballControls.js"></script>
<script src="http://localhost:8001/build/spacekit.js"></script>
-->
<script>
window.OBJECT_DEFINITIONS= [
{% for object_set in object_sets %}
{% for object in object_set.data %}
{
name: unescape("{{object.name}}"),
slug: "{{object.slug}}",
ephem: {
a: {{object.a}},
e: {{object.e}},
i: {{object.i}},
om: {{object.om}},
w: {{object.w}},
ma: {{object.ma}},
epoch: {{object.epoch}}
}
},
{% endfor %}
{% endfor %}
];
window.VIZ_SIMULATION_OPTS = {
jdPerSecond: 2,
};
window.VIZ_OBJECT_OPTS = {
particleSize: 10,
};
function selectOrbit(domElement) {
Object.keys(window.spaceobjects).forEach(function(key) {
const ob = window.spaceobjects[key];
const obOrbit = ob.getOrbit();
if (key === domElement.dataset.slug) {
obOrbit.setVisibility(true);
obOrbit.setHexColor(0xff0000);
ob.setLabelVisibility(true);
} else {
obOrbit.setVisibility(false);
ob.setLabelVisibility(false);
}
});
}
function deselectOrbit(domElement) {
window.spaceobjects[domElement.dataset.slug].getOrbit().setHexColor(0xffffff);
}
</script>
<script src="{% static "js/main.js" %}"></script>
<script>
init3dVis();
Object.keys(window.spaceobjects).forEach(function(key) {
// TODO(ian): Until there is a better way to lazily create labels and orbits,
// we have to create them and then hide them...
const ob = window.spaceobjects[key];
ob.setLabelVisibility(false);
ob.getOrbit().setVisibility(false);
});
</script>
{% endblock %}
{% block header %}
<header class="site-header">
<div class="container">
<div class="row">
<h1 class="mainheading">Space Reference</h1>
<h4 class="subheading">Compiled data &amp; simulations for {{object_count|intcomma}} celestial objects</h4>
</div>
</div>
</header>
{% endblock %}
{% block content %}
<div class="row">
<div class="item-container col-sm-8">
<div class="item-container__inner">
<p>
Welcome to SpaceReference.org. The purpose of this site is to catalog and showcase every known object in space. We've started with asteroids and comets but this <a href="https://www.github.com/judymou/spacedb">open-source</a> project is being quickly expanded.
</p>
<p>
SpaceDB compiles data from the NASA/JPL Small Body Database, the IAU Minor Planet Center, and the NASA/JPL Center for Near Earth Object Studies.
</p>
<p>
If you'd like to create a customized solar system model, take a look at the full-screen interactive <a href="/solar-system">solar system</a> view.
</p>
</div>
</div>
<div class="item-container item-container__rightside col-sm-4">
<div class="item-container__inner">
<div class="react-search"></div>
&nbsp;&nbsp; or view a <a rel="nofollow" href="/asteroid/random">random</a> object
</div>
</div>
</div>
<div class="row">
<div class="item-container">
<h2>Asteroids and Comets</h2>
<p>
We've organized hundreds of thousands celestial objects into these categories below for your perusal.
</p>
<div class="sticky vis-container vis-container__home"></div>
<p>
Mouseover objects in the lists below to highlight them in the orbit view. Click or tap to learn more about each object.
</p>
{% for object_set in object_sets %}
<div class="vis-selector" data-showvis="{{object_set.name}}">
<h3>{{object_set.name}}</h3>
<p>
{{object_set.description}} <!--<a href="/solar-system#cat={{object_set.category}}">See all</a>-->
</p>
<div class="carousel-flex-container">
<div class="carousel-flex-inner">
{% for object in object_set.data %}
<div class="tile carousel-item" data-slug="{{object.slug}}" onmouseover="if (typeof selectOrbit !== 'undefined') selectOrbit(this)"
onmouseout="if (typeof deselectOrbit !== 'undefined') deselectOrbit(this)">
<a href="{{object.get_absolute_url}}">
<div>
<h5>{{object.name}}</h5>
<div class="tile-content">
<span class="label label-default">{{object.size_adjective|title}}</span>
{% if object.is_nea %}
<span class="label label-info">Near-Earth</span>
{% endif %}
{% if object.future_close_approaches|length %}
<span class="label label-warning">Close Approach</span>
{% endif %}
{% if object.is_pha %}
<span class="label label-danger">Potentially Hazardous</span>
{% endif %}
<br>
<div class="tile-desc">
{{object.shorthand}} orbits the sun every {{object.period_in_years|floatformat:2|intcomma}} years and is about {{object.get_diameter_estimate|floatformat:3}} km in diameter, comparable in size to {{object.get_diameter_comparison}}.
{% if object.ordered_sentry_events|length and not object_set.hide_impact_probability %}
{% with object.ordered_sentry_events.0 as event %}
<em>It will pass near Earth with an impact probability of {{event.prob_percentage|floatformat:3}}% in {{event.date | date:"Y"}}.</em>
{% endwith %}
{% elif object.future_close_approaches|length %}
{% with object.future_close_approaches.0 as event %}
<em>It will pass by Earth on {{event.date | date}}.</em>
{% endwith %}
{% endif %}
</div>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div> <!-- end item-container -->
</div> <!-- end row -->
<div class="row">
<div class="item-container">
<div class="item-container__inner">
<h2>Object Classifications</h2>
The two most noteworthy classifications for asteroids are "Near Earth Asteroid" (NEA) and "Potentially Hazardous Asteroid" (PHA). Despite the names, neither classification indicates any direct danger to Earth.
<ul>
<li><a href="/category/near-earth-asteroids">Near Earth Asteroids</a> - asteroids whose orbit perihelion is less than 1.3 AU</li>
<li><a href="/category/potentially-hazardous-asteroids">Potentially Hazardous Asteroids</a> - near-Earth asteroids whose orbits passes within 0.05 AU of Earth's and whose absolute magnitude H is 22.0 or brighter</li>
</ul>
Learn more about various <a href="https://cneos.jpl.nasa.gov/about/neo_groups.html">NEO groups</a> here.
</div>
</div>
</div>
<div class="row">
<div class="item-container">
<div class="item-container__inner">
<h2>Categories</h2>
<ul>
<li><a href="/category/asteroid-shapes">Asteroids with known shapes</a> - these asteroids have been mapped by light curve inversion, radar, or flybys. Click through to see 3D models.</li>
</ul>
<p>
The categories below are orbital classifications as defined by <a href="https://pdssbn.astro.umd.edu/data_other/objclass.shtml">NASA PDS</a>:
</p>
<ul>
{% for orbit_class in orbit_classes %}
<li><a href="/category/{{orbit_class.slug}}">{{orbit_class.name}}</a> - {{orbit_class.desc}}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% if object.composition|length == 1 %}
{{object.composition.0}}.
{% elif object.composition|length == 2 %}
{{object.composition.0}} and {{object.composition.1}}.
{% else %}
{% for material in object.composition %}
{% if forloop.last %}
and {{material}}.
{% else %}
{{material}},
{% endif %}
{% endfor %}
{% endif %}

View File

@ -0,0 +1,115 @@
{% extends 'layout.html' %}
{% load humanize %}
{% load static %}
{% block title%}{{object.name}} Shape Model | Space Reference{% endblock %}
{% block scripts %}
<script>
window.SHAPE_PATH = "{% static shape_models.0.shape_path %}";
</script>
<script src="{% static "js/lib/three.r98.min.js" %}"></script>
<script src="{% static "js/lib/TrackballControls.js" %}"></script>
<script src="{% static "js/lib/OBJLoader.js" %}"></script>
<script src="{% static "js/lib/spacekit.js" %}"></script>
<script src="{% static "js/shape.js" %}"></script>
{% endblock %}
{% block header %}
<header class="site-header">
<div class="container">
<h1 class="mainheading">{{object.name}} Shape Model</h1>
<h2 class="subheading">
{% if object.fullname == object.name %}
{{object.size_adjective|capfirst}} {{object.orbit_class.name}}
{% else %}
{{object.fullname}}
{% endif %}
{% if object.is_nea %}
&nbsp;
<span class="label label-info">Near-Earth</span>
{% endif %}
{% if object.is_pha %}
<span class="label label-warning">Potentially Hazardous</span>
{% endif %}
</h2>
<div class="orbit-diagram"></div>
</div>
</header>
{% endblock %}
{% block content %}
<div>
<div class="breadcrumbs">
<a href="/">Space Reference</a>
&raquo;
<a href="/category/{{object.orbit_class.slug}}">{{object.orbit_class.name}}s</a>
&raquo;
<a href="/asteroid/{{object.slug}}">{{object.name}}</a>
&raquo;
Shape Model
</div>
<div class="item-container col-sm-4">
<p>
<h3>Orbital Elements</h3>
<ul class="keyfacts">
<li>Epoch: {{object.epoch}} JD</li>
<li>Semi-major axis: {{object.a}} AU</li>
<li>Eccentricity: {{object.e}}</li>
<li>Inclination: {{object.i}} deg</li>
<li>Longitude of Ascending Node: {{object.om}} deg</li>
<li>Argument of Periapsis: {{object.w}} deg</li>
<li>Mean Anomaly: {{object.ma}} deg</li>
</ul>
</p>
<p>
<h3>Physical Characteristics</h3>
<ul class="keyfacts">
{% if object.sbdb_entry.diameter %}
<li>Diameter: {{object.sbdb_entry.diameter | floatformat:5}} km</li>
{% elif object.get_diameter_estimate %}
<li>Diameter: ~{{object.get_diameter_estimate | floatformat:2}} km</li>
{% endif %}
{% if object.H %}
<li>Magnitude: {{object.sbdb_entry.H}}</li>
{% endif %}
{% if object.sbdb_entry.albedo %}
<li>Albedo: {{object.sbdb_entry.albedo}}</li>
{% endif %}
{% if object.sbdb_entry.spec_T %}
<li>Spectral type (Tholen): {{object.spec_T}}</li>
{% endif %}
{% if object.sbdb_entry.spec_B %}
<li>Spectral type (SMASS): {{object.spec_B}}</li>
{% endif %}
</ul>
</p>
{% if shape_models.0.source == 'damit' %}
<p>
<h3>Shape Model Characteristics</h3>
At {{shape_models.0.jd}} JD:
<ul class="keyfacts">
<li>Spin period in hours: {{shape_models.0.period_hr}}</li>
<li>Spin latitude: {{shape_models.0.spin_latitude}}</li>
<li>Spin longitude: {{shape_models.0.spin_longitude}}</li>
<li>Spin angle: {{shape_models.0.spin_angle}}</li>
<li>Quality level: {{shape_models.0.quality}}</li>
</ul>
</p>
<p>
This shape model of {{object.get_object_type}} {{object.name}} is provided by the <a href="http://astro.troja.mff.cuni.cz/projects/asteroids3D/web.php">DAMIT database</a> from The Astronomical Institute of the Charles University in Prague, Czechia.
</p>
{% else %}
<p>
This shape model of {{object.get_object_type}} {{object.name}} is sourced from {{shape_models.0.source}}.
</p>
{% endif %}
<p>
The model is rotated on its Z axis for the purpose of this visualization, which may not reflect actual rotation.
</p>
</div>
<div class="item-container col-sm-8">
<div id="shape-container"></div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,65 @@
{% extends 'layout_fullscreen.html' %}
{% load static %}
{% block title%}Solar System | Space Reference{% endblock %}
{% block css %}
<link rel="stylesheet" href="{% static "css/fullscreen.css" %}" />
{% endblock %}
{% block scripts %}
<script src="{% static "js/lib/three.r98.min.js" %}"></script>
<script src="{% static "js/lib/TrackballControls.js" %}"></script>
<script src="{% static "js/lib/spacekit.js" %}"></script>
<!--
<script src="http://localhost:8001/src/lib/three.r98.min.js"></script>
<script src="http://localhost:8001/src/lib/TrackballControls.js"></script>
<script src="http://localhost:8001/build/spacekit.js"></script>
-->
<script>
window.OBJECT_DEFINITIONS = [
];
window.VIZ_SIMULATION_OPTS = {
{% verbatim %}
particleTextureUrl: '{{assets}}/sprites/fuzzyparticle.png',
{% endverbatim %}
particleDefaultSize: 20,
maxNumParticles: 1000000,
jdPerSecond: 0.1
};
window.VIZ_OBJECT_OPTS = {
particleSize: 20,
ecliptic: {
displayLines: true,
},
};
function selectOrbit(domElement) {
window.spaceobjects[domElement.dataset.slug].getOrbit().setHexColor(0xff0000);
}
function deselectOrbit(domElement) {
window.spaceobjects[domElement.dataset.slug].getOrbit().setHexColor(0xffffff);
}
</script>
<script src="{% static "js/main.js" %}"></script>
<script>
init3dVis();
</script>
{% endblock %}
{% block content %}
<div class="add-object-panel">
<h4>Add Objects</h4>
<div class="react-search-and-visualize"></div>
</div>
<div class="vis-panel">
<div class="vis-controls">
<a href="/"><button>&laquo; Main Site</button></a>
<button class="vis-controls__slower">Slower</button>
<button class="vis-controls__faster">Faster</button>
<button class="vis-controls__set-date">Set Date</button>
<span class="vis-status"></span>
</div>
<div id="orbit-sim" class="vis-container"></div>
</div>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More