This commit is contained in:
2024-09-18 22:54:38 +10:00
commit 4cf04a3b71
10 changed files with 582 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
3.12
Binary file not shown.
+60
View File
@@ -0,0 +1,60 @@
from flask import Flask, render_template, request, jsonify
from flask_cors import CORS
from database import init_db, add_country, remove_country, get_all_countries, save_highlight_color, get_highlight_color
import requests
app = Flask(__name__)
CORS(app) # 启用CORS
init_db()
@app.route('/')
def index():
app.logger.info('访问主页')
return render_template('index.html')
@app.route('/countries', methods=['GET'])
def get_countries():
app.logger.info('获取国家列表')
countries = get_all_countries()
app.logger.info(f'返回的国家列表: {countries}')
return jsonify(countries)
@app.route('/add_country', methods=['POST'])
def add_country_route():
app.logger.info('添加国家')
country = request.json['country']
app.logger.info(f'尝试添加国家: {country}')
# 验证国家名称
response = requests.get(f"https://restcountries.com/v3.1/name/{country}")
if response.status_code == 200:
add_country(country)
app.logger.info(f'成功添加国家: {country}')
return jsonify({"success": True, "message": "国家添加成功"})
else:
app.logger.warning(f'无效的国家名称: {country}')
return jsonify({"success": False, "message": "无效的国家名称"}), 400
@app.route('/remove_country', methods=['POST'])
def remove_country_route():
app.logger.info('删除国家')
country = request.json['country']
remove_country(country)
return jsonify({"success": True})
@app.route('/test')
def test():
return "测试路由正常工作"
@app.route('/save_highlight_color', methods=['POST'])
def save_highlight_color_route():
color = request.json['color']
save_highlight_color(color)
return jsonify({"success": True})
@app.route('/get_highlight_color', methods=['GET'])
def get_highlight_color_route():
color = get_highlight_color()
return jsonify({"color": color})
if __name__ == '__main__':
app.run(debug=True, port=5001)
Binary file not shown.
+48
View File
@@ -0,0 +1,48 @@
import sqlite3
def init_db():
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS countries
(name TEXT PRIMARY KEY)''')
c.execute('''CREATE TABLE IF NOT EXISTS settings
(key TEXT PRIMARY KEY, value TEXT)''')
conn.commit()
conn.close()
def add_country(country):
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute("INSERT OR IGNORE INTO countries (name) VALUES (?)", (country,))
conn.commit()
conn.close()
def remove_country(country):
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute("DELETE FROM countries WHERE name = ?", (country,))
conn.commit()
conn.close()
def get_all_countries():
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute("SELECT name FROM countries")
countries = [row[0] for row in c.fetchall()]
conn.close()
return countries
def save_highlight_color(color):
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", ('highlight_color', color))
conn.commit()
conn.close()
def get_highlight_color():
conn = sqlite3.connect('countries.db')
c = conn.cursor()
c.execute("SELECT value FROM settings WHERE key = 'highlight_color'")
result = c.fetchone()
conn.close()
return result[0] if result else None
+4
View File
@@ -0,0 +1,4 @@
Flask==2.3.2
Werkzeug==2.3.6
requests==2.26.0
flask-cors==3.0.10
+51
View File
@@ -0,0 +1,51 @@
body {
margin: 0;
font-family: Arial, sans-serif;
}
#container {
display: flex;
height: 100vh;
}
#country-list {
width: 300px;
padding: 20px;
background-color: #f0f0f0;
overflow-y: auto;
}
#map { /* 将 #globe 改为 #map */
flex-grow: 1;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin-bottom: 10px;
}
button {
margin-left: 10px;
}
#error-message {
margin-top: 10px;
font-size: 14px;
}
#color-picker {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.color-box {
width: 30px;
height: 30px;
border: 1px solid #000;
cursor: pointer;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

+369
View File
@@ -0,0 +1,369 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
let scene, camera, renderer, map, controls;
const countries = [];
const highlightedCountries = [];
let allCountries = []; // 存储所有国家名称
let worldData; // 存储世界地理数据
let worldDataLoaded = false; // 标记世界数据是否已加载
let highlightColor = 0xFFFF00; // 默认高亮颜色
export { init, addCountry, removeCountry, exportMap };
function init() {
if (scene) {
console.log('Scene already initialized');
return;
}
scene = new THREE.Scene();
const aspect = (window.innerWidth - 300) / window.innerHeight;
const frustumSize = 180;
camera = new THREE.OrthographicCamera(
frustumSize * aspect / -2, frustumSize * aspect / 2,
frustumSize / 2, frustumSize / -2,
0.1, 1000
);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth - 300, window.innerHeight);
document.getElementById('map').appendChild(renderer.domElement);
const mapTexture = new THREE.TextureLoader().load('/static/images/world.topo.bathy.200412.3x5400x2700.jpg');
const material = new THREE.MeshBasicMaterial({ map: mapTexture });
map = new THREE.Mesh(new THREE.PlaneGeometry(360, 180), material);
scene.add(map);
camera.position.z = 100;
controls = new OrbitControls(camera, renderer.domElement);
controls.enableRotate = false;
controls.enablePan = true;
controls.enableZoom = true;
controls.panSpeed = 0.5; // 调整平移速度
controls.zoomSpeed = 0.5; // 调整缩放速度
controls.screenSpacePanning = true; // 添加这行
controls.mouseButtons = {
LEFT: THREE.MOUSE.PAN,
MIDDLE: THREE.MOUSE.DOLLY,
RIGHT: THREE.MOUSE.ROTATE
}; // 添加这个配置
setupColorPicker();
loadHighlightColor(); // 从服务器加载保存的高亮颜色
loadWorldData();
animate();
loadCountries();
loadAllCountries();
setupAutocomplete();
window.addEventListener('resize', onWindowResize, false);
}
function onWindowResize() {
const aspect = (window.innerWidth - 300) / window.innerHeight;
const frustumSize = 180;
camera.left = -frustumSize * aspect / 2;
camera.right = frustumSize * aspect / 2;
camera.top = frustumSize / 2;
camera.bottom = -frustumSize / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth - 300, window.innerHeight);
}
function loadWorldData() {
fetch('https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson')
.then(response => response.json())
.then(data => {
worldData = data;
console.log('Loaded country names:', data.features.map(f => ({
ADMIN: f.properties.ADMIN,
NAME: f.properties.NAME,
SOVEREIGNT: f.properties.SOVEREIGNT
})));
drawCountryBorders();
worldDataLoaded = true;
updateHighlights();
loadAllCountries();
setupAutocomplete();
});
}
function drawCountryBorders() {
const material = new THREE.LineBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.3 });
worldData.features.forEach(feature => {
if (feature.geometry.type === "Polygon") {
drawPolygon(feature.geometry.coordinates[0], material);
} else if (feature.geometry.type === "MultiPolygon") {
feature.geometry.coordinates.forEach(polygon => {
drawPolygon(polygon[0], material);
});
}
});
}
function drawPolygon(coords, material) {
const points = coords.map(coord => new THREE.Vector3(coord[0], coord[1], 0.1));
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geometry, material);
scene.add(line);
}
function highlightCountry(countryName) {
if (!worldDataLoaded) {
console.log('World data not loaded yet. Skipping highlight for:', countryName);
return;
}
const aliasName = getCountryAlias(countryName);
const feature = worldData.features.find(f => {
const admin = (f.properties.ADMIN || '').toLowerCase();
const name = (f.properties.NAME || '').toLowerCase();
const sovereignt = (f.properties.SOVEREIGNT || '').toLowerCase();
const aliasLower = aliasName.toLowerCase();
return admin === aliasLower || name === aliasLower || sovereignt === aliasLower ||
admin.includes(aliasLower) || name.includes(aliasLower) || sovereignt.includes(aliasLower);
});
if (feature) {
const material = new THREE.MeshBasicMaterial({
color: highlightColor,
transparent: true,
opacity: 0.5,
side: THREE.DoubleSide,
depthWrite: false,
depthTest: false
});
if (feature.geometry.type === "Polygon") {
const mesh = createPolygonMesh(feature.geometry.coordinates[0], material);
mesh.name = countryName;
scene.add(mesh);
highlightedCountries.push(mesh);
} else if (feature.geometry.type === "MultiPolygon") {
feature.geometry.coordinates.forEach(polygon => {
const mesh = createPolygonMesh(polygon[0], material);
mesh.name = countryName;
scene.add(mesh);
highlightedCountries.push(mesh);
});
}
} else {
console.log('Country not found in world data:', countryName, 'Alias:', aliasName);
}
}
function createPolygonMesh(coords, material) {
const shape = new THREE.Shape();
coords.forEach((coord, index) => {
if (index === 0) {
shape.moveTo(coord[0], coord[1]);
} else {
shape.lineTo(coord[0], coord[1]);
}
});
const geometry = new THREE.ShapeGeometry(shape);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.z = 0.1;
return mesh;
}
function addCountry() {
const input = document.getElementById('new-country');
const country = input.value.trim();
if (country) {
fetch('/add_country', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ country }),
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadCountries();
input.value = '';
} else {
alert(data.message);
}
})
.catch(error => {
console.error('Error adding country:', error);
});
}
}
function removeCountry(country) {
fetch('/remove_country', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ country }),
})
.then(() => {
loadCountries();
});
}
function loadCountries() {
fetch('/countries')
.then(response => response.json())
.then(data => {
countries.length = 0;
countries.push(...data);
updateCountryList();
if (worldDataLoaded) {
updateHighlights();
}
});
}
function updateCountryList() {
const ul = document.getElementById('countries');
ul.innerHTML = '';
countries.forEach(country => {
const li = document.createElement('li');
li.className = 'flex justify-between items-center bg-gray-100 p-2 rounded';
const span = document.createElement('span');
span.textContent = country;
li.appendChild(span);
const removeButton = document.createElement('button');
removeButton.textContent = '删除';
removeButton.className = 'bg-red-500 text-white px-2 py-1 rounded text-sm hover:bg-red-600 transition duration-200';
removeButton.onclick = () => removeCountry(country);
li.appendChild(removeButton);
ul.appendChild(li);
});
}
function animate() {
requestAnimationFrame(animate);
controls.update(); // 确保这行存在
renderer.render(scene, camera);
}
function updateHighlights() {
if (!worldDataLoaded) {
console.log('World data not loaded yet. Skipping update highlights.');
return;
}
// 清除之前的高亮
highlightedCountries.forEach(mesh => {
scene.remove(mesh);
});
highlightedCountries.length = 0;
// 重新高亮所有国家
countries.forEach(country => {
highlightCountry(country);
});
}
function getCountryAlias(countryName) {
const aliases = {
"United States": "United States of America",
"USA": "United States of America",
"US": "United States of America",
"UK": "United Kingdom",
"Britain": "United Kingdom",
"Great Britain": "United Kingdom",
"Russia": "Russian Federation",
"China": "China",
"People's Republic of China": "China",
"Taiwan": "Taiwan",
"North Korea": "Korea, Democratic People's Republic of",
"South Korea": "Korea, Republic of",
// 可以根据需要添加更多别名
};
return aliases[countryName] || countryName;
}
function loadAllCountries() {
fetch('https://restcountries.com/v3.1/all')
.then(response => response.json())
.then(data => {
allCountries = data.map(country => country.name.common).sort();
updateCountrySuggestions(allCountries);
})
.catch(error => {
console.error('Error loading all countries:', error);
});
}
function updateCountrySuggestions(suggestions) {
const datalist = document.getElementById('country-suggestions');
datalist.innerHTML = '';
suggestions.forEach(country => {
const option = document.createElement('option');
option.value = country;
datalist.appendChild(option);
});
}
function setupAutocomplete() {
const input = document.getElementById('new-country');
input.addEventListener('input', function() {
const value = this.value.toLowerCase();
const filteredCountries = allCountries.filter(country =>
country.toLowerCase().startsWith(value)
);
updateCountrySuggestions(filteredCountries);
});
}
function setupColorPicker() {
const colors = [0xFFFF00, 0xFF0000, 0x00FF00, 0x0000FF, 0xFF00FF, 0x00FFFF];
const colorPicker = document.getElementById('color-picker');
colors.forEach(color => {
const colorBox = document.createElement('div');
colorBox.className = 'w-8 h-8 rounded-full cursor-pointer border-2 border-gray-300 hover:border-gray-500 transition duration-200';
colorBox.style.backgroundColor = '#' + color.toString(16).padStart(6, '0');
colorBox.addEventListener('click', () => setHighlightColor(color));
colorPicker.appendChild(colorBox);
});
}
function setHighlightColor(color) {
highlightColor = color;
updateHighlights();
saveHighlightColor(color);
}
function loadHighlightColor() {
fetch('/get_highlight_color')
.then(response => response.json())
.then(data => {
if (data.color) {
highlightColor = parseInt(data.color, 16);
updateHighlights();
}
});
}
function saveHighlightColor(color) {
fetch('/save_highlight_color', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ color: color.toString(16) }),
});
}
function exportMap() {
// Render the scene
renderer.render(scene, camera);
// Convert the rendered image to a data URL
const dataURL = renderer.domElement.toDataURL('image/png');
// Create a temporary link element
const link = document.createElement('a');
link.href = dataURL;
link.download = 'world_map.png';
// Programmatically click the link to trigger the download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh" class="h-full bg-gray-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我去过的国家</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.138.0/build/three.module.js",
"three/examples/jsm/controls/OrbitControls": "https://unpkg.com/three@0.138.0/examples/jsm/controls/OrbitControls.js"
}
}
</script>
</head>
<body class="h-full">
<div class="flex h-full">
<div class="w-80 bg-white shadow-lg p-6 overflow-y-auto flex flex-col">
<h2 class="text-2xl font-bold mb-4">国家列表</h2>
<ul id="countries" class="mb-4 space-y-2 flex-grow overflow-y-auto"></ul>
<div class="mb-4">
<input type="text" id="new-country" placeholder="输入国家名称" list="country-suggestions" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<datalist id="country-suggestions"></datalist>
</div>
<button id="add-country-btn" class="w-full bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition duration-200 mb-4">添加国家</button>
<p id="error-message" class="text-red-500 mt-2 mb-4"></p>
<div class="mb-4">
<h3 class="text-lg font-semibold mb-2">高亮颜色</h3>
<div id="color-picker" class="flex justify-around"></div>
</div>
<button id="export-btn" class="w-full bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 transition duration-200">导出地图</button>
</div>
<div id="map" class="flex-grow"></div>
</div>
<script type="module">
import { addCountry, removeCountry, init, exportMap } from "{{ url_for('static', filename='js/globe.js') }}";
window.addCountry = addCountry;
window.removeCountry = removeCountry;
window.exportMap = exportMap;
document.getElementById('add-country-btn').addEventListener('click', addCountry);
document.getElementById('export-btn').addEventListener('click', exportMap);
// 在页面加载完成后初始化地图
window.addEventListener('load', init);
</script>
</body>
</html>