În acest tutorial, vă voi arăta cum să faceți o hartă SVG și să o proiectați pe un glob, ca vector. Pentru a efectua transformările matematice necesare pentru proiectarea hărții pe o sferă, trebuie să utilizăm scriptarea Python pentru a citi datele de pe hartă și pentru a le transpune într-o imagine a globului. Acest tutorial presupune că rulați Python 3.4, cel mai recent Python disponibil.
Inkscape are un fel de Python API care poate fi folosit pentru a face o varietate de lucruri. Cu toate acestea, deoarece suntem interesați doar de transformarea formelor, este mai ușor să scriem doar un program independent care citește și imprimă fișierele SVG pe cont propriu.
Tipul de hartă pe care îl dorim se numește o hartă echidirectangulară. Într-o hartă echidirectangulară, longitudinea și latitudinea unui loc corespund acesteia X și y poziție pe hartă. O hartă planetară echiterangulară poate fi găsită pe Wikimedia Commons (aici este o versiune cu statele din S.U.A.).
Corpurile SVG pot fi definite într-o varietate de moduri. De exemplu, ele pot fi relativ la punctul definit anterior sau definite absolut de la origine. Pentru a ne ușura viața, vrem să convertim coordonatele în hartă în forma absolută. Inkscape poate face acest lucru. Accesați preferințele Inkscape (sub Editați | × meniu) și sub Intrare ieșire > SVG ieșire, a stabilit Formatul șirului de caractere la Absolut.
Inkscape nu va converti automat coordonatele; trebuie să efectuați un fel de transformare pe căi pentru a obține acest lucru. Cel mai simplu mod de a face acest lucru este doar să selectați totul și să îl deplasați în sus și înapoi cu o singură apăsare pe fiecare săgeată sus și jos. Apoi re-salvați fișierul.
Creați un nou fișier Python. Importați următoarele module:
importare sys import import import math import timp import datetime import numpy ca np import xml.etree.ElementTree as ET
Va trebui să instalați NumPy, o bibliotecă care vă permite să efectuați anumite operații vectoriale cum ar fi produsul punctat și produsul încrucișat.
Proiectarea unui punct în spațiul tridimensional într-o imagine 2D implică găsirea unui vector de la aparatul de fotografiat până la punct și apoi împărțirea acelui vector în trei vectori perpendiculari.
Cele două vectori parțiali perpendiculați pe vectorul camerei (direcția spre care se află camera) devin X și y coordonatele unei imagini ortogonale proiectate. Vectorul parțial paralel cu vectorul camerei devine ceva numit z distanța punctului. Pentru a converti o imagine ortogonală într-o imagine în perspectivă, împărțiți fiecare X și y coordonat de către z distanţă.
În acest moment, este bine să definiți anumiți parametri ai camerei. În primul rând, trebuie să știm unde se află camera în spațiul 3D. Stocați-i X, y, și z coordonate într-un dicționar.
camera = 'x': -15, 'y': 15, 'z': 30
Globul va fi localizat la origine, deci are sens să orientați aparatul foto spre el. Aceasta înseamnă că vectorul de direcție al camerei va fi opus poziției camerei.
* camera '[' y '],' z ': -1 * camera [' z '] cameraForward = ' x ': -1 * camera [' x '],' y '
Nu este suficient să determinați direcția în care se află camera - trebuie să înnebuniți și o rotație a camerei. Faceți asta prin definirea unui vector perpendicular pe cameraForward
vector.
camera '' '', 'z': 0 cameraPerpendicular = 'x': cameraForward ['y'
Va fi foarte util să aveți anumite funcții vectoriale definite în programul nostru. Definiți o funcție de magnitudine vectorială:
#magnitudinea vectorului 3D def sumOfSquares (vector): vectorul de întoarcere ['x'] ** 2 + vector ['y'] ** 2 + vector ['z'] ** 2 magnitudinea def (vector) .sqrt (sumOfSquares (vector))
Trebuie să putem proiecta un vector pe altul. Deoarece această operațiune implică un produs punct, este mult mai ușor să utilizați biblioteca NumPy. NumPy, cu toate acestea, ia vectori în formă de listă, fără identificatorii explicit "x", "y", "z", deci avem nevoie de o funcție pentru a converti vectorii noștri în vectori NumPy.
#converte vectorul dicționarului pentru a afișa vectorul vectorului defTolist (vector): return [vectorul 'x'], vectorul ['y'], vectorul ['z']]
#projects u pe vectorul v defProiect (u, v): returnați np.dot (vectorTolist (v), vectorTolist (u)) / magnitudine (v)
Este frumos să avem o funcție care ne va da un vector unic în direcția unui vector dat:
(vector): magVector = magnitudine (vector) return 'x': vector ['x'] / 'z'] / magVector
În cele din urmă, trebuie să putem lua două puncte și să găsim un vector între ele:
#Calculă vectorul din două puncte, formatul dicționarului def findVector (origine, punct): retur 'x': punctul ['x'] - origine ['x'], 'y'], 'z': punctul ['z'] - origine ['z']
Acum trebuie doar să terminăm definirea axelor camerei. Avem deja două dintre aceste axe-cameraForward
și cameraPerpendicular
, care corespunde z distanța și X coordonarea imaginii camerei.
Acum avem nevoie doar de a treia axă, definită de un vector reprezentând y coordonarea imaginii camerei. Putem găsi această a treia axă prin preluarea produsului încrucișat al acestor două vectori, folosind NumPy-np.cross (vectorToList (cameraForward), vectorTolist (cameraPerpendicular))
.
Primul element din rezultat corespunde cu X componente; al doilea la y componente, iar al treilea la z componentă, astfel încât vectorul produs este dat de:
#Calculează vectorul planului orizontului (puncte în sus) cameraHorizon = 'x': np.cross (vectorToList (cameraForward), vectorTolist (cameraPerpendicular)) [0], 'y': np.cross (vectorToList (cameraForward), vectorToList )) [1], "z": np.cross (vectorToList (cameraForward), vectorTolist (cameraPerpendicular)) [2]
Pentru a găsi ortogonală X, y, și z distanța, vom găsi mai întâi vectorul care leagă camera și punctul în cauză și apoi îl proiectăm pe fiecare din cele trei axe ale camerei definite anterior:
def vectorProjection (point): pointVector = findVector (camera, punct) #pointVector este un vector care pornește de la aparatul de fotografiat și se termină într-un punct în cauză return 'x': vectorProject (pointVector, cameraPerpendicular) , cameraHorizon), 'z': vectorProject (pointVector, cameraForward)
Un punct (gri închis) fiind proiectat pe cele trei axe ale camerei (gri). X este rosu, y este verde și z este albastru.
Perspectiva proiecție ia pur și simplu X și y a proiecției ortogonale și împarte fiecare coordonate cu z distanţă. Acest lucru face ca lucrurile care sunt mai departe să pară mai mici decât lucrurile care sunt mai aproape de cameră.
Deoarece împărțirea prin z obține coordonate foarte mici, înmulțim fiecare coordonate cu o valoare corespunzătoare lungimii focale a camerei.
focalLength = 1000
# atrage puncte pe senzorul camerei folosind xDistance, yDistance și zDistance def perspectiveProjecția (pCoords): scaleFactor = focalLength / pCoords ['z'] retur 'x': pCoords ['x' 'y'] * scaleFactor
Pământul este o sferă. Astfel coordonatele noastre - latitudine și longitudine - sunt coordonatele sferice. Așa că trebuie să scriem o funcție care să transforme coordonatele sferice în coordonate dreptunghiulare (precum și să definească o rază a Pământului și să furnizeze π constant):
raza = 10 pi = 3,14159
# convertește coordonatele sferice la coordonatele dreptunghiulare def sferăToRect (r, a, b): returnează 'x': r * math.sin (b * pi / 180) * math.cos (a * pi / 180) : r * math.sin (b * pi / 180) * math.sin (a * pi / 180), 'z': r * math.cos (b * pi / 180)
Putem obține performanțe mai bune prin stocarea unor calcule utilizate mai mult decât o dată:
#convertează coordonatele sferice la coordonatele dreptunghiulare def sphereToRect (r, a, b): aRad = math.radians (a) bRad = math.radians (b) r_sin_b = r * math.sin (bRad) întoarcere 'x': r_sin_b * math.cos (aRad), 'y': r_sin_b * math.sin (aRad), 'z': r * math.cos (bRad)
Putem scrie câteva funcții compuse care vor combina toți pașii anteriori într-o singură funcție - mergând direct de la coordonatele sferice sau rectangulare la imaginile perspectivei:
#funcții pentru plotarea punctelor def rectPlot (coordonate): perspectivă întoarcereProjecție (physicalProjection (coordinate)) def spherePlot (coordonate, sRadius): return rectPlot (sferaToRect (sRadius, coordonate [long '], coordinate [
Scriptul nostru trebuie să poată scrie într-un fișier SVG. Deci ar trebui să înceapă cu:
f = deschis ('globe.svg', 'w') f.write ('\ n
Și se termină cu:
f.write (“„)
Producerea unui fișier SVG gol, dar valabil. În acest fișier scriptul trebuie să poată crea obiecte SVG, deci vom defini două funcții care îi vor permite să deseneze puncte SVG și poligoane:
# Curbele Obiect cerc SVG def svgCircle (coordonate, circleRadius, color): f.write ('\ n ') # Curbele nodul poligonal SVG def polyNode (coordonate): f.write (str (coordonate [' x '] + 400) +', '+ str (coordonate [' y '
Putem testa acest lucru oferind o rețea sferică de puncte:
#DRAW GRID pentru x în intervalul (72): pentru y în intervalul (36): svgCircle (sferaPlot ('lung': 5 * x, lat ': 5 * y )
Acest script, atunci când este salvat și rulat, ar trebui să producă ceva de genul:
Pentru a citi un fișier SVG, un script trebuie să poată citi un fișier XML, deoarece SVG este un tip de XML. De aceea am importat xml.etree.ElementTree
. Acest modul vă permite să încărcați XML / SVG într-un script ca listă imbricată:
copac = ET.parse ('BlankMap Equirectangular states.svg') root = tree.getroot ()
Puteți naviga la un obiect din SVG prin intermediul indexurilor listei (de obicei, trebuie să aruncați o privire la codul sursă al fișierului hărții pentru a înțelege structura acestuia). În cazul nostru, fiecare țară este situată la rădăcină [4] [0] [X] [n]
, Unde X este numărul țării, începând cu 1 și n reprezintă diferitele subpagini care conturează țara. Contururile reale ale țării sunt stocate în d atribut, accesibil prin rădăcină [4] [0] [X] [n] .Attrib [ 'd']
.
Nu putem repeta doar prin această hartă, deoarece conține un element "inactiv" la început, care trebuie să fie omis. Deci, trebuie să numărăm numărul de obiecte "de țară" și să scădem unul pentru a scăpa de manechinul. Apoi bifăm obiectele rămase.
țările = len (rădăcină [4] [0]) - 1 pentru x în intervalul (țări): rădăcină [4] [0] [x + 1]
Unele obiecte de țară includ mai multe căi, motiv pentru care apoi iterăm prin fiecare cale din fiecare țară:
țările = len (rădăcină [4] [0]) - 1 pentru x în intervalul (țările): pentru calea în rădăcină [4] [0] [x + 1]:
În cadrul fiecărei căi există contururi disjuncte separate de personajele "Z M" din d șir, așa că am împărțit d șirul de-a lungul acelui delimitator și repetarea acestea.
(1) pentru x în intervalul (țările): pentru calea în rădăcină [4] [0] [x + 1]: pentru k în re.split ('Z M' path.attrib [ 'd']):
Apoi împărțim fiecare contur cu delimitatorii "Z", "L" sau "M" pentru a obține coordonatele fiecărui punct al căii:
pentru x în intervalul (țările): pentru calea în rădăcină [4] [0] [x + 1]: pentru k în re.split ('Z M', path.attrib ['d' .split ("Z | M | L ', k):
Apoi eliminăm toate coordonatele non-numerice din coordonate și le împărțim în jumătate de-a lungul virgulelor, dând latitudini și longitudini. Dacă ambele există, le stocăm într-un sphereCoordinates
dicționar (în hartă, coordonatele latitudinii variază de la 0 la 180 °, dar dorim ca acestea să meargă de la -90 ° la 90 ° spre nord și spre sud, așa că scădem 90 °).
pentru x în intervalul (țările): pentru calea în rădăcină [4] [0] [x + 1]: pentru k în re.split ('Z M', path.attrib ['d' .split ('Z | M | L', k): breakup = re.split (',', re.sub ("[^ - 0123456789. [1]: sferăCoordonate ['lat'] = float (breakup [1]) - 90
Apoi, dacă o vom testa prin trasarea unor puncte (svgCircle (sferaPlot (sfera Coordonate, rază), 1, '# 333')
), obținem ceva de genul:
Acest lucru nu face distincția între punctele de pe latura apropiată a globului și punctele din partea îndepărtată a globului. Dacă vrem să tipăm doar puncte pe partea vizibilă a planetei, trebuie să fim capabili să dăm seama care parte a planetei este un punct dat.
Putem face acest lucru prin calcularea celor două puncte ale sferei în care o rază de la aparatul de fotografiat până la punct se va intersecta cu sfera. Această funcție implementează formula pentru rezolvarea distanțelor la aceste două puncte-dNear și DFAR:
cameraDistanceSquare = sumaOfSquares (camera) #distance de la centrul globului până la camera def distanceToPoint (spherePoint): point = sphereToRect (raza, spherePoint ['long'], spherePoint ['lat']) ray, aparat de fotografiatForward)
def occlude (spherePoint): point = sphereToRect (raza, spherePoint ['long'], spherePoint ['lat']) ray = findVector (camera, punct) d1 = magnitudinea (d), vectorToList (camera)) #dot produs al vectorului unității de la camera la punct și determinantul vectorului de cameră = math.sqrt (abs ((dot_l) ** 2 - cameraDistanceSquare + rază ** 2)) dNear = - (dot_l) + determinant dFar = - (dot_l)
Dacă distanța efectivă față de punct, d1, este mai mică sau egală cu ambii din aceste distanțe, atunci punctul se află pe partea apropiată a sferei. Din cauza erorilor de rotunjire, în această operațiune este încorporată o mică cameră de gheață:
dacă d1 - 0.0000000001 <= dNear and d1 - 0.0000000001 <= dFar : return True else: return False
Utilizarea acestei funcții ca o condiție ar trebui să restricționeze redarea în puncte apropiate:
dacă occlude (sfera Coordonate): svgCircle (sferăPlot (sferă Coordonate, rază), 1, '# 333')
Desigur, punctele nu sunt adevărate închise, forme pline - ele dau doar iluzia formelor închise. Desenarea țărilor încărcate efectiv necesită un pic mai mult sofisticare. Mai întâi de toate, trebuie să tipărim toate țările vizibile.
Putem face acest lucru prin crearea unui comutator care se activează ori de câte ori o țară conține un punct vizibil, în timp ce stochează temporar coordonatele țării respective. Dacă comutatorul este activat, țara este trasă, folosind coordonatele stocate. Vom desena poligoane în locul punctelor.
pentru x în intervalul (țările): pentru calea în rădăcină [4] [0] [x + 1]: pentru k în re.split ('Z M', path.attrib ['d']: countryIsVisible = = [] pentru i în re.split ('Z | M | L', k): breakup = re.split (',', re.sub (" ) dacă ruperea [0] și ruperea [1]: sphereCoordinates = sferăCoordonate ['long'] = float (breakup [0]) sferăCoordonate ['lat'] = ([sfera Coordonate, rază]]) countryIsVisible = Adevărat altfel: country.append ([sferă Coordonate, rază]) dacă countryIsVisible: f.write ('\ N \ n ')
Este greu de spus, dar țările de pe marginea globului se îndoaie pe ele însele, pe care nu le vrem (aruncăm o privire la Brazilia).
Pentru a face ca țările să facă corect la marginea globului, trebuie mai întâi să urmărim discul globului cu un poligon (discul pe care îl vedeți din puncte este o iluzie optică). Discul este evidențiat de marginea vizibilă a globului - un cerc. Următoarele operații calculează raza și centrul acestui cerc, precum și distanța dintre planul care conține cercul din cameră și centrul globului.
#RACE LIMB limbRadius = math.sqrt (raza ** 2 - raza ** 4 / cameraDistanceSquare) cx = camera ['x'] * raza ** 2 / cameraDistanceSquare cy = camera ['y' cameraDistanceSquare cz = camera ['z'] * raza ** 2 / cameraDistanceSquare planeDistance = magnitudinea (camera) * (1 - raza ** 2 / cameraDistanceSquare) planeDisplacement = math.sqrt (cx ** 2 + cy ** 2 + cz ** 2)
Pământul și camera (punct gri închis) văzute de sus. Linia roz reprezintă marginea vizibilă a pământului. Numai sectorul umbrite este vizibil pentru camera foto.
Apoi pentru a arăta un cerc în planul respectiv, construim două axe paralele cu acel avion:
#trade & negate x și y pentru a obține un vector vector perpendicularVectorCamera = unitVector (cameră) aV = unitVector ('x': -unitVectorCamera ['y'], 'y': unitVectorCamera ['x' 0) bV = np.cross (vectorTolist (aV), vectorTolist (unitVectorCamera))
Apoi am doar grafice pe acele axe cu incremente de 2 grade pentru a complot un cerc în acel avion cu acea rază și centru (a se vedea această explicație pentru matematică):
pentru t în intervalul (180): theta = math.radians (2 * t) cosT = math.cos (theta) sinT = math.sin (theta) limbPoint = 'x': cx + limbRadius * 'x'] + sinT * bV [0]), 'y': cy + limbRadius * (cosT * aV ['y'] + sinT * bV [ aV ['z'] + sinT * bV [2]
Apoi vom încapsule toate cu codul de desen poligon:
f.write (“„)
Creați, de asemenea, o copie a obiectului respectiv, care să fie utilizată mai târziu ca o mască de tăiere pentru toate țările noastre:
f.write (“„)
Aceasta ar trebui să vă dea acest lucru:
Folosind discul nou calculat, ne putem modifica altfel
instrucțiunea în codul țării (dacă coordonatele se află pe partea ascunsă a globului) pentru a compila acele puncte undeva în afara discului:
altfel: tangentscale = (raza + planDisplacement) / (pi * 0.5) rr = 1 + abs (math.tan (distantaToPoint (sferaCoordonate) - planDistance) / tangentscale) country.append ([
Aceasta utilizează o curbă tangentă pentru a ridica punctele ascunse deasupra suprafeței Pământului, dând aspectul că acestea sunt răspândite în jurul lui:
Acest lucru nu este în întregime matematic (se descompune dacă aparatul nu este orientat aproape în centrul planetei), dar este simplu și funcționează de cele mai multe ori. Apoi, prin adăugarea pur și simplu clip-path = "url (#clipglobe)"
la codul de desen poligon, putem clipa cu usurinta tarile la marginea globului:
dacă countryIsVisible: f.write ('Sper că vă place acest tutorial! Distrează-te cu globurile tale vectoriale!