commit 4cf04a3b719db91dfdf1014f437dfd10c8964bf5 Author: Junwei Zhao Date: Wed Sep 18 22:54:38 2024 +1000 Init map diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +3.12 diff --git a/have-been-to/__pycache__/database.cpython-312.pyc b/have-been-to/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..13b5eb0 Binary files /dev/null and b/have-been-to/__pycache__/database.cpython-312.pyc differ diff --git a/have-been-to/app.py b/have-been-to/app.py new file mode 100644 index 0000000..6b204d1 --- /dev/null +++ b/have-been-to/app.py @@ -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) diff --git a/have-been-to/countries.db b/have-been-to/countries.db new file mode 100644 index 0000000..415ec76 Binary files /dev/null and b/have-been-to/countries.db differ diff --git a/have-been-to/database.py b/have-been-to/database.py new file mode 100644 index 0000000..80fda62 --- /dev/null +++ b/have-been-to/database.py @@ -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 diff --git a/have-been-to/requirements.txt b/have-been-to/requirements.txt new file mode 100644 index 0000000..3dab290 --- /dev/null +++ b/have-been-to/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.3.2 +Werkzeug==2.3.6 +requests==2.26.0 +flask-cors==3.0.10 diff --git a/have-been-to/static/css/style.css b/have-been-to/static/css/style.css new file mode 100644 index 0000000..3600f7d --- /dev/null +++ b/have-been-to/static/css/style.css @@ -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; +} diff --git a/have-been-to/static/images/world.topo.bathy.200412.3x5400x2700.jpg b/have-been-to/static/images/world.topo.bathy.200412.3x5400x2700.jpg new file mode 100644 index 0000000..55b715d Binary files /dev/null and b/have-been-to/static/images/world.topo.bathy.200412.3x5400x2700.jpg differ diff --git a/have-been-to/static/js/globe.js b/have-been-to/static/js/globe.js new file mode 100644 index 0000000..f2a9477 --- /dev/null +++ b/have-been-to/static/js/globe.js @@ -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); +} diff --git a/have-been-to/templates/index.html b/have-been-to/templates/index.html new file mode 100644 index 0000000..be32c86 --- /dev/null +++ b/have-been-to/templates/index.html @@ -0,0 +1,49 @@ + + + + + + 我去过的国家 + + + + +
+
+

国家列表

+
    +
    + + +
    + +

    +
    +

    高亮颜色

    +
    +
    + +
    +
    +
    + + +